├── .firebaserc ├── .gitignore ├── .opensource └── project.json ├── LICENSE ├── README.md ├── answerBrowser.js ├── config ├── app-config.json ├── cloud-firestore.rules ├── content-security-policy.json ├── firebase-config.json ├── firebase-deploy.js └── firebase-storage.rules ├── docs ├── applibrary.js.md ├── authentication.js.md ├── fbstorage.js.md ├── fileutils.js.md ├── firestore.js.md ├── localstorage.js.md ├── mainapp.js.md ├── webserver.js.md └── windows.js.md ├── electron-firebase.js ├── firebase.json ├── functions ├── .gitignore ├── index.js └── package.json ├── install_app ├── install-tools.js ├── package-update.json ├── postinstall.js └── preinstall.js ├── lib ├── applibrary.js ├── authentication.js ├── fbstorage.js ├── fileutils.js ├── firestore.js ├── localstorage.js ├── mainapp.js ├── webserver.js └── windows.js ├── main.js ├── package.json ├── pages ├── electron-logo.png ├── firebase-logo.png ├── index.html ├── logincomplete.html ├── loginstart.html └── splashpage.html ├── release-notes.md ├── scripts ├── indexpage_script.js ├── indexpage_style.css ├── logincomplete.js ├── loginstart.js ├── setmodule_off.js ├── setmodule_on.js ├── splashpage_style.css ├── weblocal.js ├── weblogin.js ├── webmodal.js └── webutils.js ├── setupApp.js └── tests ├── generated.json ├── main_test_all.js ├── test_all_modules.js ├── test_applibrary.js ├── test_fbstorage.js ├── test_fileutils.js ├── test_firestore.js ├── test_localstorage.js └── testpage_local.html /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": null 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # comment the following to update the files 3 | config/firebase-config.json 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Compiled binary addons (https://nodejs.org/api/addons.html) 14 | build/Release 15 | 16 | # Dependency directories 17 | node_modules/ 18 | 19 | # project-specific ignores 20 | developer/ 21 | package-lock.json 22 | .gitignore 23 | .DS_Store 24 | settings.json 25 | .firebaserc 26 | firebase.json 27 | package.old.json 28 | .npmrc 29 | -------------------------------------------------------------------------------- /.opensource/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-firebase", 3 | "platforms": [ 4 | "Web", 5 | "node.js", 6 | "Electron" 7 | ], 8 | "content": "README.md", 9 | "pages" : { 10 | "docs/applibrary.js.md": "Application Library", 11 | "docs/authentication.js.md": "Authentication", 12 | "docs/fbstorage.js.md": "Firebase Storage", 13 | "docs/fileutils.js.md": "File Utilities", 14 | "docs/firestore.js.md": "Firestore Database", 15 | "docs/localstorage.js.md": "Web Local Storage", 16 | "docs/mainapp.js.md": "Main Application Support", 17 | "docs/webserver.js.md": "TLS Web Server", 18 | "docs/windows.js.md": "Window Manager" 19 | }, 20 | "related": [ 21 | ], 22 | "tabs": [ 23 | { 24 | "title": "NPM Project", 25 | "href": "https://www.npmjs.com/package/electron-firebase" 26 | }, 27 | { 28 | "title": "GitHub Repo", 29 | "href": "https://github.com/david-asher/electron-firebase" 30 | }, 31 | { 32 | "title": "Medium Article", 33 | "href": "https://medium.com/@dja.asher/writing-cloud-connected-cross-platform-pc-apps-8bf003e2412f" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019 David Asher, https://github.com/david-asher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /answerBrowser.js: -------------------------------------------------------------------------------- 1 | /* answerBrowser.js 2 | * Copyright (c) 2019-2021 by David Asher, https://github.com/david-asher 3 | * 4 | * This is a quickstart template for building Firebase authentication workflow into an electron app 5 | * This module contains functions that respond to queries from the Browser 6 | * @module answerBrowser 7 | */ 8 | 'use strict'; 9 | 10 | const { mainapp, firestore, fbstorage, fbwindow } = loadModule( 'electron-firebase' ) 11 | const urlParser = require('url').parse 12 | 13 | const docAboutmeFolder = "aboutme/" 14 | 15 | function buildProfile( user ) 16 | { 17 | return { 18 | "Name": user.displayName, 19 | "Email": user.email, 20 | "User ID": user.uid, 21 | "Photo:": user.photoURL, 22 | "Last Login": (new Date( parseInt(user.lastLoginAt,10) )).toString() 23 | } 24 | } 25 | 26 | function getUser( parameter ) 27 | { 28 | switch( parameter ) 29 | { 30 | case 'profile': return buildProfile( global.user ) 31 | case 'provider': return global.user.providerData[0] 32 | case 'context': return global.appContext 33 | } 34 | } 35 | 36 | async function getDocs( filename ) 37 | { 38 | var docContent 39 | try { 40 | docContent = await firestore.doc.read( docAboutmeFolder + filename ) 41 | } 42 | catch (error) { 43 | console.error( "getDocs: ", filename, error ) 44 | docContent = {} 45 | } 46 | return docContent 47 | } 48 | 49 | async function listFolders( domain = "file" ) 50 | // domain is file | app | public 51 | { 52 | var folderList 53 | try { 54 | folderList = await fbstorage[ domain ].folders() 55 | } 56 | catch (error) { 57 | console.error( "listFolders: ", domain, error ) 58 | folderList = {} 59 | } 60 | return folderList 61 | } 62 | 63 | async function listFiles( folderPath, domain = "file" ) 64 | { 65 | var fileList 66 | try { 67 | fileList = await fbstorage[ domain ].list( folderPath ) 68 | } 69 | catch (error) { 70 | console.error( "listFiles: ", domain, error ) 71 | fileList = {} 72 | } 73 | return fileList 74 | } 75 | 76 | async function infoRequest( request, ...parameters ) 77 | { 78 | var sendContent 79 | switch( request ) { 80 | case 'user': 81 | sendContent = await getUser( parameters[0] ) 82 | break 83 | case 'docs': 84 | sendContent = await getDocs( parameters[0] ) 85 | break 86 | case 'folder-list': 87 | sendContent = await listFolders( parameters[0] ) 88 | break 89 | case 'file-list': 90 | sendContent = await listFiles( parameters[0], parameters[1] ) 91 | break 92 | } 93 | mainapp.sendToBrowser( 'info-request', sendContent ) 94 | } 95 | 96 | async function getContent( filepath, domain = "file" ) 97 | { 98 | if ( !filepath || filepath.length == 0 ) return {} 99 | return await fbstorage[ domain ].download( filepath ) 100 | } 101 | 102 | function openWithUrl( url, contentType ) 103 | { 104 | // see BrowserWindow options: https://www.electronjs.org/docs/api/browser-window#new-browserwindowoptions 105 | const urlParts = urlParser( url ) 106 | const lastPart = urlParts.pathname.split( "/" ).pop() 107 | const resource = decodeURIComponent( lastPart ) 108 | const openOptions = { 109 | show: true, 110 | title: resource, 111 | skipTaskbar: true, 112 | parent: global.mainWindow, 113 | autoHideMenuBar: true 114 | } 115 | return new fbwindow.open( url, openOptions ) 116 | } 117 | 118 | async function showFile( request, ...parameters ) 119 | { 120 | switch( request ) { 121 | case 'path': 122 | mainapp.sendToBrowser( 'show-file', await getContent( parameters[0], parameters[1] || undefined ) ) 123 | break; 124 | case 'url': 125 | openWithUrl( parameters[0], parameters[1] ) 126 | break; 127 | } 128 | } 129 | 130 | module.exports = { 131 | infoRequest: infoRequest, 132 | showFile: showFile 133 | } 134 | -------------------------------------------------------------------------------- /config/app-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "debugMode": false, 3 | "webapp": { 4 | "port": "3090", 5 | "hostPort": "localhost:3090", 6 | "hostUrl": "https://localhost:3090", 7 | "folderPath": "/pages/", 8 | "loginStart": "pages/loginstart.html", 9 | "loginRedirect": "pages/logincomplete.html", 10 | "mainPage": "pages/index.html", 11 | "splashPage": "pages/splashpage.html", 12 | "splashPageTimeout": 2, 13 | "firstWidth": 1000, 14 | "firstHeight": 800, 15 | "persistentUser": true 16 | }, 17 | "webFolders": [ 18 | "pages", 19 | "scripts", 20 | "lib", 21 | "node_modules/firebase", 22 | "node_modules/firebaseui/dist" 23 | ], 24 | "apis": { 25 | "firebaseconfig": "/api/firebaseconfig", 26 | "logintoken": "/api/logintoken", 27 | "loginready": "/api/loginready" 28 | }, 29 | "logout": { 30 | "google.com": "https://accounts.google.com/Logout", 31 | "facebook.com": "https://www.facebook.com/logout.php", 32 | "twitter.com": "https://twitter.com/logout", 33 | "github.com": "https://github.com/logout" 34 | }, 35 | "providers": [ 36 | "google.com", 37 | "github.com", 38 | "twitter.com", 39 | "facebook.com", 40 | "password", 41 | "phone" 42 | ] 43 | } -------------------------------------------------------------------------------- /config/cloud-firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service cloud.firestore { 3 | match /databases/{database}/documents { 4 | match /users/{userId}/{allPaths=**} { 5 | allow create, read, update, delete: if request.auth.uid == userId; 6 | } 7 | match /apps/{projectId}/{allPaths=**} { 8 | allow create, read, update, delete: if request.auth != null && request.auth.token.aud == projectId; 9 | } 10 | match /apps/public/{allPaths=**} { 11 | allow create, update, delete: if request.auth != null 12 | allow read: if true 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /config/content-security-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "default-src": [ 3 | "'self'" 4 | ], 5 | "script-src": [ 6 | "'self'", 7 | "*.bootstrapcdn.com", 8 | "code.jquery.com", 9 | "*.google.com", 10 | "*.firebaseapp.com", 11 | "*.cloudfunctions.net", 12 | "*.youtube.com", 13 | "*.fbcdn.net", 14 | "*.facebook.com" 15 | ], 16 | "style-src": [ 17 | "'self'", 18 | "*.google.com", 19 | "*.firebaseapp.com", 20 | "*.googleusercontent.com", 21 | "*.bootstrapcdn.com", 22 | "*.googleapis.com", 23 | "*.gstatic.com", 24 | "*.fbcdn.net", 25 | "'unsafe-hashes'", 26 | "'sha256-O9ChnrQJngUlTYptX2rHTyPwYa4VlQslTnAyr1r9/XE='", 27 | "'sha256-KPTVW5oJwjIe0y2cEU5idixe+0eH/ARZMQXuQgteCBw='", 28 | "'sha256-72k6lx3PMqTD7y6xr91xeDYIj51JhoVJTrinJBHEt4I='", 29 | "'sha256-jcChtzjXxOs5V2A5l2c5UkgoGcYO+8GLAdzsZqlWsq4='", 30 | "'sha256-nriuFMNMno0iiUNQX9SLMKxBTaxm9WGUhrVusxfqDGA='", 31 | "'sha256-0nnP5wTs7LWXCQGBE25MSDmq1ZSHQEHAnowTPyUoHXQ='" 32 | ], 33 | "img-src": [ 34 | "'self'", 35 | "*.gstatic.com", 36 | "*.googleusercontent.com", 37 | "*.githubusercontent.com", 38 | "*.twimg.com", 39 | "*.fbcdn.net", 40 | "*.facebook.com" 41 | ], 42 | "font-src": [ 43 | "'self'", 44 | "*.bootstrapcdn.com", 45 | "*.googleapis.com", 46 | "*.gstatic.com", 47 | "*.fbcdn.net" 48 | ], 49 | "connect-src": [ 50 | "'self'", 51 | "*.google.com", 52 | "*.googleapis.com", 53 | "*.firebaseapp.com", 54 | "*.youtube.com", 55 | "*.facebook.com" 56 | ], 57 | "frame-src": [ 58 | "'self'", 59 | "*.google.com", 60 | "*.googleapis.com", 61 | "*.firebaseapp.com", 62 | "*.facebook.com" 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /config/firebase-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiKey": "", 3 | "authDomain": "", 4 | "databaseURL": "", 5 | "projectId": "", 6 | "storageBucket": "", 7 | "messagingSenderId": "", 8 | "appId": "", 9 | "hostingUrl": "", 10 | "serviceAccountId": "" 11 | } -------------------------------------------------------------------------------- /config/firebase-deploy.js: -------------------------------------------------------------------------------- 1 | /* 2 | * firebase-deploy.js 3 | * Copyright (c) 2019-2020 by David Asher, https://github.com/david-asher 4 | * 5 | * This script is run from the project root "npm run deploy" 6 | * and performs firebase deploy for database and storage security rules 7 | * as well as cloud function deploy 8 | */ 9 | 10 | const { execSync } = require( 'child_process' ) 11 | const { exit } = require('process') 12 | const { writeFileSync, readFileSync } = require('fs') 13 | const path = require('path') 14 | const { env } = require('process') 15 | const os = require('os') 16 | 17 | const fbrcFile = "./.firebaserc" 18 | const fbDeployFile = "./firebase.json" 19 | const fbConfigFile = "firebase-config.json" 20 | 21 | function writeJson( filePath, jsonContent ) 22 | { 23 | writeFileSync( filePath, JSON.stringify( jsonContent, null, 2 ) ) 24 | } 25 | 26 | function readJson( filePath ) 27 | { 28 | try { 29 | return JSON.parse( readFileSync( filePath ) ) 30 | } 31 | catch (error) { 32 | return null 33 | } 34 | } 35 | 36 | function fbDeploy( command ) 37 | { 38 | execSync( "firebase deploy --only " + command, {stdio:'inherit'} ) 39 | } 40 | 41 | function addToPath( newPath ) 42 | { 43 | env.PATH = `${newPath}${path.delimiter}${env.PATH}` 44 | } 45 | 46 | (function() 47 | { 48 | var fbConfig = readJson( `./developer/${fbConfigFile}` ) 49 | if ( !fbConfig ) fbConfig = readJson( `./config/${fbConfigFile}` ) 50 | if ( !fbConfig ) { 51 | console.error( `ERROR: cannot find ${fbConfigFile}` ) 52 | exit(10) 53 | } 54 | if ( !fbConfig.storageBucket || !fbConfig.projectId ) { 55 | console.error( "ERROR: ${fbConfigFile} must be configured for your firebase project." ) 56 | exit(11) 57 | } 58 | 59 | console.log( "** login to firebase-tools with the Google account that you use for Firebase" ) 60 | const npmGlobal = path.join( os.homedir(), ".npm-global" ) 61 | const npmGlobalBin = path.join( npmGlobal, "bin" ) 62 | addToPath( npmGlobal ) 63 | addToPath( npmGlobalBin ) 64 | execSync( `npm config set prefix "${npmGlobal}"` ) 65 | execSync( "firebase login", {stdio:'inherit'} ) 66 | 67 | console.log( "** configure .firebaserc file" ) 68 | var fbrcJson = readJson( fbrcFile ) 69 | fbrcJson.projects.default = fbConfig.projectId 70 | writeJson( fbrcFile, fbrcJson ) 71 | 72 | console.log( "** deploy firestore:rules" ) 73 | fbDeploy( "firestore:rules" ) 74 | 75 | console.log( "** deploy storage:rules" ) 76 | const fbDeployJson = readJson( fbDeployFile ) 77 | fbDeployJson.storage[0].bucket = fbConfig.storageBucket 78 | writeJson( fbDeployFile, fbDeployJson, null ) 79 | fbDeploy( "storage:rules" ) 80 | 81 | console.log( "** deploy firebase functions" ) 82 | execSync( "npm install", { cwd: "./functions" } ) 83 | fbDeploy( "functions" ) 84 | })() 85 | -------------------------------------------------------------------------------- /config/firebase-storage.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service firebase.storage { 3 | match /b/{bucket}/o { 4 | match /users/{userId}/{allPaths=**} { 5 | allow read, write: if request.auth.uid == userId; 6 | } 7 | match /apps/{projectId}/{allPaths=**} { 8 | allow read, write: if request.auth != null && request.auth.token.aud == projectId; 9 | } 10 | match /apps/public/{allPaths=**} { 11 | allow read: if true 12 | allow write: if request.auth != null 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /docs/applibrary.js.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## applib 4 | Collection of utilities for JSON, objects, events, web request. 5 | 6 | 7 | * [applib](#module_applib) 8 | * [isJSON(s)](#exp_module_applib--isJSON) ⇒ boolean ⏏ 9 | * [isObject(obj)](#exp_module_applib--isObject) ⇒ boolean ⏏ 10 | * [parseJSON(inputSerialized)](#exp_module_applib--parseJSON) ⇒ object ⏏ 11 | * [stringifyJSON(inputObject)](#exp_module_applib--stringifyJSON) ⇒ string ⏏ 12 | * [compactJSON(inputObject)](#exp_module_applib--compactJSON) ⇒ string ⏏ 13 | * [mergeObjects(...objects)](#exp_module_applib--mergeObjects) ⇒ object ⏏ 14 | * [request(options)](#exp_module_applib--request) ⇒ Promise.<object> ⏏ 15 | 16 | 17 | 18 | ### isJSON(s) ⇒ boolean ⏏ 19 | Tests whether the input looks like a JSON string. 20 | 21 | **Kind**: Exported function 22 | **Returns**: boolean - True if the input is likely a JSON string 23 | 24 | | Param | Type | Description | 25 | | --- | --- | --- | 26 | | s | \* | a parameter to be tested | 27 | 28 | 29 | 30 | ### isObject(obj) ⇒ boolean ⏏ 31 | Tests whether the input is an object. 32 | 33 | **Kind**: Exported function 34 | **Returns**: boolean - True if the input is an object 35 | 36 | | Param | Type | Description | 37 | | --- | --- | --- | 38 | | obj | \* | a parameter to be tested | 39 | 40 | 41 | 42 | ### parseJSON(inputSerialized) ⇒ object ⏏ 43 | Converts a JSON string to an object, handling errors so this won't throw an exception. 44 | 45 | **Kind**: Exported function 46 | **Returns**: object - Null if there is an error, else a valid object 47 | 48 | | Param | Type | Description | 49 | | --- | --- | --- | 50 | | inputSerialized | string | A JSON string | 51 | 52 | 53 | 54 | ### stringifyJSON(inputObject) ⇒ string ⏏ 55 | Converts an object into a JSON string with space/newline formatting, handling errors so it won't throw an exception. 56 | 57 | **Kind**: Exported function 58 | **Returns**: string - Null if there is an error, else a JSON string 59 | 60 | | Param | Type | Description | 61 | | --- | --- | --- | 62 | | inputObject | object | a valid JavaScript object | 63 | 64 | 65 | 66 | ### compactJSON(inputObject) ⇒ string ⏏ 67 | Same as stringifyJSON except the result is compact without spaces and newlines. 68 | 69 | **Kind**: Exported function 70 | **Returns**: string - Null if there is an error, else a JSON string 71 | 72 | | Param | Type | Description | 73 | | --- | --- | --- | 74 | | inputObject | object | a valid JavaScript object | 75 | 76 | 77 | 78 | ### mergeObjects(...objects) ⇒ object ⏏ 79 | Performs a deep merge of the input objects. 80 | 81 | **Kind**: Exported function 82 | **Returns**: object - A JavaScript object 83 | 84 | | Param | Type | Description | 85 | | --- | --- | --- | 86 | | ...objects | any | A parameter set (comma-separated) of objects | 87 | 88 | 89 | 90 | ### request(options) ⇒ Promise.<object> ⏏ 91 | Interface for the npm request HTTP client. The response object from the returned promise contains these important properties: .status, .statusText, .headers, .data 92 | 93 | **Kind**: Exported function 94 | **Returns**: Promise.<object> - Promise object represents the HTTP response 95 | **See** 96 | 97 | - [https://www.npmjs.com/package/axios](https://www.npmjs.com/package/axios) 98 | - [https://nodejs.org/api/https.html#https_https_request_options_callback](https://nodejs.org/api/https.html#https_https_request_options_callback) 99 | - [https://nodejs.org/api/http.html#http_class_http_serverresponse](https://nodejs.org/api/http.html#http_class_http_serverresponse) 100 | 101 | 102 | | Param | Type | Description | 103 | | --- | --- | --- | 104 | | options | object | Parameters that define this request | 105 | 106 | -------------------------------------------------------------------------------- /docs/authentication.js.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## auth 4 | Authentication workflow for Google Firebase. 5 | 6 | **See**: [FirebaseUI for Web](https://github.com/firebase/FirebaseUI-Web) 7 | 8 | * [auth](#module_auth) 9 | * [initializeFirebase()](#exp_module_auth--initializeFirebase) ⏏ 10 | * [firestore()](#exp_module_auth--firestore) ⇒ Firestore ⏏ 11 | * [userPath()](#exp_module_auth--userPath) ⇒ string ⏏ 12 | * [gcpApi(requestOptions)](#exp_module_auth--gcpApi) ⇒ Promise ⏏ 13 | * [signInNewUser(newUser)](#exp_module_auth--signInNewUser) ⇒ Promise ⏏ 14 | * [startNewSignIn(mainWindow)](#exp_module_auth--startNewSignIn) ⇒ Promise ⏏ 15 | * [getProvider()](#exp_module_auth--getProvider) ⇒ string ⏏ 16 | * [getSignOutUrl(provider)](#exp_module_auth--getSignOutUrl) ⇒ string ⏏ 17 | * [signOutUser()](#exp_module_auth--signOutUser) ⏏ 18 | * [signOutProvider(provider, mainWindow)](#exp_module_auth--signOutProvider) ⇒ BrowserWindow ⏏ 19 | 20 | 21 | 22 | ### initializeFirebase() ⏏ 23 | Must be called before any operations on Firebase API calls. 24 | 25 | **Kind**: Exported function 26 | 27 | 28 | ### firestore() ⇒ Firestore ⏏ 29 | Firestore is a Google NoSQL datastore. This function returns a reference that can be used with the Firestore API. 30 | 31 | **Kind**: Exported function 32 | **Returns**: Firestore - An interface to Firestore 33 | **See**: [Firestore](https://firebase.google.com/docs/firestore/) 34 | 35 | 36 | ### userPath() ⇒ string ⏏ 37 | Return the unique path prefix for a user. 38 | 39 | **Kind**: Exported function 40 | **Returns**: string - A path string 41 | 42 | 43 | ### gcpApi(requestOptions) ⇒ Promise ⏏ 44 | Executes an API call to Google Cloud, taking care of user authentication and token refresh. 45 | 46 | **Kind**: Exported function 47 | **Returns**: Promise - Promise object represents the payload response of the API call (string|object|buffer) 48 | **See**: [Request Options](https://github.com/request/request#requestoptions-callback) 49 | 50 | | Param | Type | Description | 51 | | --- | --- | --- | 52 | | requestOptions | | A set of option parameters for the API request | 53 | | requestOptions.url | string \| object | HTTP(S) endpoint to call, string or object in the format of url.parse() | 54 | | requestOptions.method | string | HTTP verb, e.g. GET, POST, etc. | 55 | | requestOptions.headers | object | An object with any additional request headers | 56 | 57 | 58 | 59 | ### signInNewUser(newUser) ⇒ Promise ⏏ 60 | Completes the authentication workflow for a new user. The user credential will be saved in as a web browser identity persistence so it can be recovered on a subsequent session without forcing the user to log in again. 61 | 62 | **Kind**: Exported function 63 | **Returns**: Promise - A Promise object representing the user object 64 | 65 | | Param | Type | Description | 66 | | --- | --- | --- | 67 | | newUser | object | This is an object passed from the Web UI for authentication after a successful registration of a new user | 68 | 69 | 70 | 71 | ### startNewSignIn(mainWindow) ⇒ Promise ⏏ 72 | Initiates the Firebase UI authentication workflow. nodeIntegration must be set to false because it would expose the login page to hacking through the IPC interface. 73 | 74 | **Kind**: Exported function 75 | **Returns**: Promise - A Promise object representing the new modal window for authentication workflow 76 | 77 | | Param | Type | Description | 78 | | --- | --- | --- | 79 | | mainWindow | BrowserWindow | The parent (or main) window, so that the workflow window can be modal | 80 | 81 | 82 | 83 | ### getProvider() ⇒ string ⏏ 84 | Gets the identity provider that was used to authenticate the current user. 85 | 86 | **Kind**: Exported function 87 | **Returns**: string - The firebase representation of the identity provider, can be any of: "google.com", "github.com", "twitter.com", "facebook.com", "password", "phone" 88 | 89 | 90 | ### getSignOutUrl(provider) ⇒ string ⏏ 91 | Firebase UI doesn't have a workflow for logging out from the identity provider, so this function returns a URL that can be used to log out directly -- if the identity provider doesn't change the URL. 92 | 93 | **Kind**: Exported function 94 | **Returns**: string - A URL that can be called to log out of the identity provider 95 | 96 | | Param | Type | Description | 97 | | --- | --- | --- | 98 | | provider | string | The name of the identity provider, from getProvider() | 99 | 100 | 101 | 102 | ### signOutUser() ⏏ 103 | Logs out the user from Firebase, but not from the identity provider. 104 | 105 | **Kind**: Exported function 106 | 107 | 108 | ### signOutProvider(provider, mainWindow) ⇒ BrowserWindow ⏏ 109 | Performs a complete signout from Firebase and the identity provider. 110 | 111 | **Kind**: Exported function 112 | **Returns**: BrowserWindow - A new window that was used for the identity provider logout 113 | 114 | | Param | Type | Description | 115 | | --- | --- | --- | 116 | | provider | string | The identity provider, from getProvider() | 117 | | mainWindow | BrowserWindow | A parent window, so the logout window can be modal | 118 | 119 | -------------------------------------------------------------------------------- /docs/fbstorage.js.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## fbstorage 4 | Interface to Google Cloud Storage in the security context of the authenticated user. Keep track of every file add/remove in firestore because firebase cloud storage does not allow listing/searching for files. Use the REST API directly because the node.js interface does not include storage. After initialization the fbstorage module will contain 3 objects: * .file - file access limited to the current signed-in user * .app - file access limited to any user of this app but not other apps * .public - file access without restriction 5 | 6 | **See** 7 | 8 | - [Firebase Storage](https://firebase.google.com/docs/storage/) 9 | - [Object Naming Guidelines](https://cloud.google.com/storage/docs/naming#objectnames) 10 | 11 | **Example** 12 | ```js 13 | const { fbstorage } = require( 'electron-firebase' ) // get list of folders only accessible to the signed-in user const fileFolderList = await fbstorage.file.folders() 14 | ``` 15 | 16 | * [fbstorage](#module_fbstorage) 17 | * [initialize()](#exp_module_fbstorage--initialize) ⏏ 18 | * [~fileStore](#module_fbstorage--initialize..fileStore) 19 | * [new fileStore(firestoreRoot, storeName, setPrefix)](#new_module_fbstorage--initialize..fileStore_new) 20 | * [.find(filepath, queryMatch)](#module_fbstorage--initialize..fileStore+find) ⇒ object 21 | * [.list(folderpath, queryMatch)](#module_fbstorage--initialize..fileStore+list) ⇒ object 22 | * [.folders(filepath, content)](#module_fbstorage--initialize..fileStore+folders) 23 | * [.upload(filepath, content)](#module_fbstorage--initialize..fileStore+upload) ⇒ object 24 | * [.update(filepath, metadata)](#module_fbstorage--initialize..fileStore+update) ⇒ object 25 | * [.download(filepath)](#module_fbstorage--initialize..fileStore+download) ⇒ string \| JSON \| buffer \| object \| array 26 | * [.about(filepath)](#module_fbstorage--initialize..fileStore+about) ⇒ Promise 27 | * [.delete(filepath)](#module_fbstorage--initialize..fileStore+delete) ⇒ null \| string 28 | 29 | 30 | 31 | ### initialize() ⏏ 32 | Firebase Storage interfaces are defined when your app starts (this function must be called after firestore is initialized). 33 | 34 | **Kind**: Exported function 35 | 36 | 37 | #### initialize~fileStore 38 | **Kind**: inner class of [initialize](#exp_module_fbstorage--initialize) 39 | 40 | * [~fileStore](#module_fbstorage--initialize..fileStore) 41 | * [new fileStore(firestoreRoot, storeName, setPrefix)](#new_module_fbstorage--initialize..fileStore_new) 42 | * [.find(filepath, queryMatch)](#module_fbstorage--initialize..fileStore+find) ⇒ object 43 | * [.list(folderpath, queryMatch)](#module_fbstorage--initialize..fileStore+list) ⇒ object 44 | * [.folders(filepath, content)](#module_fbstorage--initialize..fileStore+folders) 45 | * [.upload(filepath, content)](#module_fbstorage--initialize..fileStore+upload) ⇒ object 46 | * [.update(filepath, metadata)](#module_fbstorage--initialize..fileStore+update) ⇒ object 47 | * [.download(filepath)](#module_fbstorage--initialize..fileStore+download) ⇒ string \| JSON \| buffer \| object \| array 48 | * [.about(filepath)](#module_fbstorage--initialize..fileStore+about) ⇒ Promise 49 | * [.delete(filepath)](#module_fbstorage--initialize..fileStore+delete) ⇒ null \| string 50 | 51 | 52 | 53 | ##### new fileStore(firestoreRoot, storeName, setPrefix) 54 | Create a new fileStore interface. 55 | 56 | 57 | | Param | Type | Description | 58 | | --- | --- | --- | 59 | | firestoreRoot | string | a database object defined in firestore.js | 60 | | storeName | string | just a moniker | 61 | | setPrefix | string | the first two segments of the file path, e.g. user/userid | 62 | 63 | 64 | 65 | ##### fileStore.find(filepath, queryMatch) ⇒ object 66 | Search the storage records in the Firestore database for a file matching the specific filepath given. The newest document matching the search criteria will be returned. 67 | 68 | **Kind**: instance method of [fileStore](#module_fbstorage--initialize..fileStore) 69 | **Returns**: object - - metafile descriptor for the requested file 70 | 71 | | Param | Type | Default | Description | 72 | | --- | --- | --- | --- | 73 | | filepath | string | | Path and filename to store the file in the Cloud | 74 | | queryMatch | string | "path" | optional match parameter to query for something other than path | 75 | 76 | 77 | 78 | ##### fileStore.list(folderpath, queryMatch) ⇒ object 79 | Search the storage records in the Firestore database for all files where their folder matches the specific path given, and return an array with the metadata for each file. 80 | 81 | **Kind**: instance method of [fileStore](#module_fbstorage--initialize..fileStore) 82 | **Returns**: object - - metafile descriptor for the requested file 83 | 84 | | Param | Type | Default | Description | 85 | | --- | --- | --- | --- | 86 | | folderpath | string | | Path to query file storage | 87 | | queryMatch | string | "folder" | optional match parameter to query for something other than folder | 88 | 89 | 90 | 91 | ##### fileStore.folders(filepath, content) 92 | Return a list of all folders. Folders don't really exist, they are just a slash-separated path construct, the parent of the file path. 93 | 94 | **Kind**: instance method of [fileStore](#module_fbstorage--initialize..fileStore) 95 | 96 | | Param | Type | 97 | | --- | --- | 98 | | filepath | \* | 99 | | content | \* | 100 | 101 | 102 | 103 | ##### fileStore.upload(filepath, content) ⇒ object 104 | Uploads local content and creates a file in Google Cloud Storage for Firebase, and a record of the file will be kept in the Cloud Firestore database, for easy reference and searching. Accepts contents as string, JSON string, object (serializable), array, or buffer. Returns a Promise containing file metadata, as: 105 | 106 | **Kind**: instance method of [fileStore](#module_fbstorage--initialize..fileStore) 107 | **Returns**: object - - metafile descriptor for the requested file 108 | 109 | | Param | Type | Description | 110 | | --- | --- | --- | 111 | | filepath | string | Path and filename to store the file in the Cloud | 112 | | content | string \| JSON \| buffer \| object \| array | File content to be written, objects must be serializable | 113 | 114 | **Example** 115 | ```js 116 | { name: 'users/[user-id]/Test/FileTest', bucket: 'your-app-here.appspot.com', generation: '123456789123456', metageneration: '1', contentType: 'application/json', timeCreated: '2019-02-05T03:06:24.435Z', updated: '2019-02-05T03:06:24.435Z', storageClass: 'STANDARD', size: '1005', md5Hash: 'H3Anb534+vX2Y1HVwJxlyw==', contentEncoding: 'identity', contentDisposition: 'inline; filename*=utf-8\'\'FileTest', crc32c: 'yTf15w==', etag: 'AAAAAAA=', downloadTokens: '00000000' } 117 | ``` 118 | 119 | 120 | ##### fileStore.update(filepath, metadata) ⇒ object 121 | Change some metadata aspects of a stored file 122 | 123 | **Kind**: instance method of [fileStore](#module_fbstorage--initialize..fileStore) 124 | **Returns**: object - - metafile descriptor for the requested file 125 | 126 | | Param | Type | Description | 127 | | --- | --- | --- | 128 | | filepath | string | Path and filename to update the file in the Cloud, relative to the current user | 129 | | metadata | | One or more metadata parameters to change | 130 | | metadata.cacheControl | string | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control | 131 | | metadata.contentDisposition | string | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/content-Disposition | 132 | | metadata.contentEncoding | string | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding | 133 | | metadata.contentLanguage | string | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Language | 134 | | metadata.contentType | string | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type | 135 | 136 | 137 | 138 | ##### fileStore.download(filepath) ⇒ string \| JSON \| buffer \| object \| array 139 | Download a file from Firebase Storage 140 | 141 | **Kind**: instance method of [fileStore](#module_fbstorage--initialize..fileStore) 142 | **Returns**: string \| JSON \| buffer \| object \| array - - file content 143 | 144 | | Param | Type | Description | 145 | | --- | --- | --- | 146 | | filepath | string | Path and filename to retreive the file | 147 | 148 | 149 | 150 | ##### fileStore.about(filepath) ⇒ Promise 151 | Gets meta information about the file, including a secure download URL that can be used anywhere 152 | 153 | **Kind**: instance method of [fileStore](#module_fbstorage--initialize..fileStore) 154 | **Returns**: Promise - A Promise object representing the meta information about the file 155 | 156 | | Param | Type | Description | 157 | | --- | --- | --- | 158 | | filepath | string | Path and filename to find the file in the Cloud, relative to the current user | 159 | 160 | 161 | 162 | ##### fileStore.delete(filepath) ⇒ null \| string 163 | Delete the file from Google Cloud Storage for Firebase and remove the file's record from Cloud Firestore 164 | 165 | **Kind**: instance method of [fileStore](#module_fbstorage--initialize..fileStore) 166 | **Returns**: null \| string - - empty response unless there is an error 167 | 168 | | Param | Type | Description | 169 | | --- | --- | --- | 170 | | filepath | string | Path and filename to delete the file in the Cloud, relative to the current user | 171 | 172 | -------------------------------------------------------------------------------- /docs/fileutils.js.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## file 4 | Functions for local file I/O. All functions are synchronous. 5 | 6 | 7 | * [file](#module_file) 8 | * [readFile(fileName)](#exp_module_file--readFile) ⇒ string \| buffer ⏏ 9 | * [writeFile(fileName, fileContent)](#exp_module_file--writeFile) ⏏ 10 | * [isFile(fileName)](#exp_module_file--isFile) ⇒ boolean ⏏ 11 | * [isFolder(folderName)](#exp_module_file--isFolder) ⇒ boolean ⏏ 12 | * [makeFolder(folderName)](#exp_module_file--makeFolder) ⇒ boolean ⏏ 13 | * [listFolders(folderName)](#exp_module_file--listFolders) ⇒ array ⏏ 14 | * [listFiles(folderName)](#exp_module_file--listFiles) ⇒ array ⏏ 15 | * [deleteFolder(folderName)](#exp_module_file--deleteFolder) ⇒ boolean ⏏ 16 | * [deleteFile(fileName)](#exp_module_file--deleteFile) ⇒ boolean ⏏ 17 | * [readJSON(fileName)](#exp_module_file--readJSON) ⇒ object ⏏ 18 | * [writeJSON(fileName, fileContent)](#exp_module_file--writeJSON) ⏏ 19 | * [updateJSON(fileName, updateObject)](#exp_module_file--updateJSON) ⏏ 20 | * [checkCommand(commandString)](#exp_module_file--checkCommand) ⇒ boolean ⏏ 21 | 22 | 23 | 24 | ### readFile(fileName) ⇒ string \| buffer ⏏ 25 | Reads a local file and returns the contents. 26 | 27 | **Kind**: Exported function 28 | **Returns**: string \| buffer - - File contents, will be converted to a string if possible 29 | 30 | | Param | Type | Description | 31 | | --- | --- | --- | 32 | | fileName | string | Path to local file | 33 | 34 | 35 | 36 | ### writeFile(fileName, fileContent) ⏏ 37 | Writes buffer or string content to a local file. 38 | 39 | **Kind**: Exported function 40 | 41 | | Param | Type | Description | 42 | | --- | --- | --- | 43 | | fileName | string | Path to local file | 44 | | fileContent | string \| buffer | Content to write | 45 | 46 | 47 | 48 | ### isFile(fileName) ⇒ boolean ⏏ 49 | Check if a local file exists. 50 | 51 | **Kind**: Exported function 52 | **Returns**: boolean - True if the file exists 53 | 54 | | Param | Type | Description | 55 | | --- | --- | --- | 56 | | fileName | string | Path to local file | 57 | 58 | 59 | 60 | ### isFolder(folderName) ⇒ boolean ⏏ 61 | Check if the given path is a folder. 62 | 63 | **Kind**: Exported function 64 | **Returns**: boolean - True if the give path exists and is a folder 65 | 66 | | Param | Type | Description | 67 | | --- | --- | --- | 68 | | folderName | string | Path to local folder | 69 | 70 | 71 | 72 | ### makeFolder(folderName) ⇒ boolean ⏏ 73 | Create a new folder at the given path. 74 | 75 | **Kind**: Exported function 76 | **Returns**: boolean - True if the folder was successfully created 77 | 78 | | Param | Type | Description | 79 | | --- | --- | --- | 80 | | folderName | string | Path to local folder | 81 | 82 | 83 | 84 | ### listFolders(folderName) ⇒ array ⏏ 85 | Return a list of folders at the given path. Does not include hidden folders. 86 | 87 | **Kind**: Exported function 88 | **Returns**: array - A list of folder names 89 | 90 | | Param | Type | Description | 91 | | --- | --- | --- | 92 | | folderName | string | Path to local folder | 93 | 94 | 95 | 96 | ### listFiles(folderName) ⇒ array ⏏ 97 | Return a list of files at the given path. Does not include hidden files. 98 | 99 | **Kind**: Exported function 100 | **Returns**: array - A list of files names 101 | 102 | | Param | Type | Description | 103 | | --- | --- | --- | 104 | | folderName | string | Path to local folder | 105 | 106 | 107 | 108 | ### deleteFolder(folderName) ⇒ boolean ⏏ 109 | Delete the folder at the given path. 110 | 111 | **Kind**: Exported function 112 | **Returns**: boolean - Returns true if the folder was successfully deleted 113 | 114 | | Param | Type | Description | 115 | | --- | --- | --- | 116 | | folderName | string | Path to local folder | 117 | 118 | 119 | 120 | ### deleteFile(fileName) ⇒ boolean ⏏ 121 | Deletes the local file. 122 | 123 | **Kind**: Exported function 124 | **Returns**: boolean - True if the file exists and was deleted. 125 | 126 | | Param | Type | Description | 127 | | --- | --- | --- | 128 | | fileName | string | Path to local file | 129 | 130 | 131 | 132 | ### readJSON(fileName) ⇒ object ⏏ 133 | Reads the local JSON file and returns its object representation. 134 | 135 | **Kind**: Exported function 136 | **Returns**: object - Contents of the local file parsed as an object 137 | 138 | | Param | Type | Description | 139 | | --- | --- | --- | 140 | | fileName | string | Path to local file | 141 | 142 | 143 | 144 | ### writeJSON(fileName, fileContent) ⏏ 145 | Writes a serializable object as JSON to a local file. 146 | 147 | **Kind**: Exported function 148 | 149 | | Param | Type | Description | 150 | | --- | --- | --- | 151 | | fileName | string | Path to local file | 152 | | fileContent | object | Content to write as JSON | 153 | 154 | 155 | 156 | ### updateJSON(fileName, updateObject) ⏏ 157 | Given an object, reads a local JSON file and merges the object with file contents, writing back the merged object as JSON. 158 | 159 | **Kind**: Exported function 160 | 161 | | Param | Type | Description | 162 | | --- | --- | --- | 163 | | fileName | string | Path to local file | 164 | | updateObject | object | A serializable object to be merged with the JSON file | 165 | 166 | 167 | 168 | ### checkCommand(commandString) ⇒ boolean ⏏ 169 | Checks whether the command exists, i.e. can be run with an exec() statement. 170 | 171 | **Kind**: Exported function 172 | **Returns**: boolean - True if the command exists 173 | 174 | | Param | Type | Description | 175 | | --- | --- | --- | 176 | | commandString | string | A shell comment to be tested | 177 | 178 | -------------------------------------------------------------------------------- /docs/localstorage.js.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## local 4 | Functions that use the localStorage capability in a BrowserWindow to store persistent information. These APIs run in the main node.js process and use IPC to request and transfer information from the browser. This feature is used in conjunction with the weblocal.js file if referenced by a BrowserWindow. weblocal.js should not be loaded into more than one BrowserWindow. This API is intended to mimic the localStorage API available in every Browser, except the getItem() call must be asynchronous and replies with either a callback or a promise. 5 | 6 | **See**: [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) 7 | 8 | * [local](#module_local) 9 | * [setItem(key, value)](#exp_module_local--setItem) ⏏ 10 | * [removeItem(key)](#exp_module_local--removeItem) ⏏ 11 | * [getItem(key, [optionalCallback])](#exp_module_local--getItem) ⇒ Promise.<(string\|object)> ⏏ 12 | 13 | 14 | 15 | ### setItem(key, value) ⏏ 16 | When passed a key name and value, will add that key to the Storage object, or update that key's value if it already exists. This function will not confirm that the key and value were written to the BrowserWindow localStorage. 17 | 18 | **Kind**: Exported function 19 | **See**: [localStorage setItem()](https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem) 20 | 21 | | Param | Type | Description | 22 | | --- | --- | --- | 23 | | key | \* | The name of the key you want to create/update | 24 | | value | \* | The value you want to give the key you are creating/updating | 25 | 26 | 27 | 28 | ### removeItem(key) ⏏ 29 | When passed a key name, will remove that key from the Storage object if it exists. If there is no item associated with the given key, this function will do nothing. 30 | 31 | **Kind**: Exported function 32 | **See**: [localStorage removeItem()](https://developer.mozilla.org/en-US/docs/Web/API/Storage/removeItem) 33 | 34 | | Param | Type | Description | 35 | | --- | --- | --- | 36 | | key | string | The name of the key you want to remove | 37 | 38 | 39 | 40 | ### getItem(key, [optionalCallback]) ⇒ Promise.<(string\|object)> ⏏ 41 | When passed a key name, will return that key's value, or null if the key does not exist. 42 | 43 | **Kind**: Exported function 44 | **Returns**: Promise.<(string\|object)> - A promise which resolves to containing the value of the key. If the key does not exist, null is returned. 45 | **See**: [localStorage getItem()](https://developer.mozilla.org/en-US/docs/Web/API/Storage/getItem) 46 | 47 | | Param | Type | Description | 48 | | --- | --- | --- | 49 | | key | string | The name of the key you want to retrieve the value of | 50 | | [optionalCallback] | \* | Optional callback function to retreive the | 51 | 52 | -------------------------------------------------------------------------------- /docs/mainapp.js.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## mainapp 4 | High-level functions for quickly building the main application. 5 | 6 | 7 | * [mainapp](#module_mainapp) 8 | * [event](#exp_module_mainapp--event) ⏏ 9 | * [setupAppConfig()](#exp_module_mainapp--setupAppConfig) ⏏ 10 | * [sendToBrowser(channel, payload)](#exp_module_mainapp--sendToBrowser) ⏏ 11 | * [getFromBrowser(channel, [callback])](#exp_module_mainapp--getFromBrowser) ⇒ Promise.<(string\|number\|object\|array)> ⏏ 12 | * [closeApplication()](#exp_module_mainapp--closeApplication) ⏏ 13 | * [signoutUser()](#exp_module_mainapp--signoutUser) ⏏ 14 | * [onUserLogin(user)](#exp_module_mainapp--onUserLogin) ⇒ Promise.<object> ⏏ 15 | * [registerAPI(method, urlInvocation, apiRouteFunction)](#exp_module_mainapp--registerAPI) ⏏ 16 | * [startMainApp(options)](#exp_module_mainapp--startMainApp) ⏏ 17 | 18 | 19 | 20 | ### event ⏏ 21 | Exports a node.js Event Emitter object that can be used to send and receive messages with other parts of the application. 22 | 23 | **Kind**: Exported member 24 | **See**: [Events](https://nodejs.org/api/events.html) 25 | 26 | 27 | ### setupAppConfig() ⏏ 28 | Must be called before other APIs. Reads the two configuration files, app-config.json and firebase-config.json, and creates a global.appContext object with various information. 29 | 30 | **Kind**: Exported function 31 | 32 | 33 | ### sendToBrowser(channel, payload) ⏏ 34 | Sends a message - a payload on a specific channel - to the global.mainWindow. 35 | 36 | **Kind**: Exported function 37 | **See**: [BrowserWindow.webContents.send()](https://electronjs.org/docs/api/web-contents#contentssendchannel-arg1-arg2-) 38 | 39 | | Param | Type | Description | 40 | | --- | --- | --- | 41 | | channel | string | A topic which the BrowserWindow should be expecting | 42 | | payload | string \| number \| object \| array | The message content to send on the topic | 43 | 44 | 45 | 46 | ### getFromBrowser(channel, [callback]) ⇒ Promise.<(string\|number\|object\|array)> ⏏ 47 | Receives a message event from the global.mainWindow, with optional callback or Promise interface. The callback or Promise will fire whenever a message event is received on the channel. 48 | 49 | **Kind**: Exported function 50 | **Returns**: Promise.<(string\|number\|object\|array)> - If no callback is supplied then a Promise is returned 51 | **See**: [BrowserWindow.webContents.send()](https://electronjs.org/docs/api/web-contents#contentssendchannel-arg1-arg2-) 52 | 53 | | Param | Type | Description | 54 | | --- | --- | --- | 55 | | channel | string | A topic which the BrowserWindow should be expecting | 56 | | [callback] | function | Optional callback function to receive the message event | 57 | 58 | 59 | 60 | ### closeApplication() ⏏ 61 | Call this before the app closes to perform some app cleanup. 62 | 63 | **Kind**: Exported function 64 | 65 | 66 | ### signoutUser() ⏏ 67 | Handles the workflow for signing out the current user. The user will be presented with a dialog box asking them to confirm signout, and optionally to sign out of the current identity provider as well as the app. Fires the user-signout event when complete. 68 | 69 | **Kind**: Exported function 70 | 71 | 72 | ### onUserLogin(user) ⇒ Promise.<object> ⏏ 73 | This function is called after a user login has completed, which can happen after a new login workflow, or after a re-login from a previous session. Fires the user-login event when complete. 74 | 75 | **Kind**: Exported function 76 | **Returns**: Promise.<object> - Resolves after the CloudStore updates are completed 77 | 78 | | Param | Type | Description | 79 | | --- | --- | --- | 80 | | user | object | The user object that was returned from the login workflow | 81 | 82 | 83 | 84 | ### registerAPI(method, urlInvocation, apiRouteFunction) ⏏ 85 | Registers a function that will respond to an API request from the Browser. This will set up a route with the [express](http://expressjs.com/) middleware in the Main node.js process. For Browser pages, the /scripts/webutils.js file contains an api() function that can be used to invoke a route registered with registerAPI(). 86 | 87 | **Kind**: Exported function 88 | **See**: [Routing in Express](http://expressjs.com/en/guide/routing.html) 89 | 90 | | Param | Type | Description | 91 | | --- | --- | --- | 92 | | method | string | the HTTPS method such as 'GET', 'POST', etc. | 93 | | urlInvocation | string | the localhost URL to invoke, e.g. "/api/loginready" | 94 | | apiRouteFunction | function | API request called in express style i.e. (req,res,next) | 95 | 96 | 97 | 98 | ### startMainApp(options) ⏏ 99 | This is it, the function that kicks it all off. 100 | 101 | **Kind**: Exported function 102 | 103 | | Param | Type | Description | 104 | | --- | --- | --- | 105 | | options | object | May contain show, width, height, title, main_html | 106 | 107 | -------------------------------------------------------------------------------- /docs/webserver.js.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## server 4 | This module sets up a local webserver which is primarily used for secure communication with a BrowserWindow. Although it is possible to use IPC for this purpose, that would require enabling the nodeIntegration option for the window, which would expose the app to all manner of mischief. The webserver is an instance of express, configured for HTTPS with a self-signed cert. 5 | 6 | 7 | 8 | ### start(mainApp, staticFolders) ⏏ 9 | Start the HTTPS server for the Main node.js process. 10 | 11 | **Kind**: Exported function 12 | 13 | | Param | Type | Description | 14 | | --- | --- | --- | 15 | | mainApp | object | Reference to the Electron app | 16 | | staticFolders | array | List of folders that will be exposed from the webserver as static content | 17 | 18 | -------------------------------------------------------------------------------- /docs/windows.js.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## fbwindow 4 | This module will open and manage Electron BrowserWindow instances, and make sure that they all close when the app closes. 5 | 6 | 7 | * [fbwindow](#module_fbwindow) 8 | * [open](#exp_module_fbwindow--open) ⏏ 9 | * [new open(urlToOpen, [setOptions])](#new_module_fbwindow--open_new) 10 | * _instance_ 11 | * [.window()](#module_fbwindow--open+window) 12 | * [.waitForShow()](#module_fbwindow--open+waitForShow) ⇒ Promise.<void> 13 | * [.waitForClose()](#module_fbwindow--open+waitForClose) ⇒ Promise.<void> 14 | * [.resize()](#module_fbwindow--open+resize) ⇒ object 15 | * [.send(channel, payload)](#module_fbwindow--open+send) 16 | * [.receive(channel, [callback])](#module_fbwindow--open+receive) ⇒ Promise.<(string\|number\|object\|array)> 17 | * [.close()](#module_fbwindow--open+close) 18 | * _inner_ 19 | * [~openModal(urlToOpen, parentWindow, setOptions)](#module_fbwindow--open..openModal) ⇒ Promise.<WindowObject> 20 | 21 | 22 | 23 | ### open ⏏ 24 | Opens a BrowserWindow. 25 | 26 | **Kind**: Exported class 27 | **See**: [BrowserWindow options](https://electronjs.org/docs/api/browser-window) 28 | 29 | 30 | #### new open(urlToOpen, [setOptions]) 31 | Create a window.open object. The window will automatically track window changes in size and position and keep the bounds changes in localStorage. 32 | 33 | **Returns**: Promise.<WindowObject> - An WindowObject inhereted from BrowserWindow 34 | 35 | | Param | Type | Description | 36 | | --- | --- | --- | 37 | | urlToOpen | string | Opens the window and loads this page | 38 | | [setOptions] | options | A set of options for changing the window properties | 39 | 40 | 41 | 42 | #### open.window() 43 | Why is this function here? If you create a new window.open object and pass that to dialog.showMessageBox() for a modal doalog, it won't render the dialog content (i.e. it's a blank dialog). Even when you capture the constructor super(), the call to showMessageBox() still comes up blank. This method returns an actual BrowserWindow object that is satisfactory for building a modal dialog. 44 | 45 | **Kind**: instance method of [open](#exp_module_fbwindow--open) 46 | 47 | 48 | #### open.waitForShow() ⇒ Promise.<void> 49 | If you open the window with option show:false then call window.show(), use this function to get a Promise that returns after the window is visible. 50 | 51 | **Kind**: instance method of [open](#exp_module_fbwindow--open) 52 | 53 | 54 | #### open.waitForClose() ⇒ Promise.<void> 55 | If you close the window with window.close(), use this function to get a Promise that returns after the window is destroyed. 56 | 57 | **Kind**: instance method of [open](#exp_module_fbwindow--open) 58 | 59 | 60 | #### open.resize() ⇒ object 61 | Recalls the last saved position and shape of the window, particularly useful for the first showing of the window. 62 | 63 | **Kind**: instance method of [open](#exp_module_fbwindow--open) 64 | **Returns**: object - Returns the previous bounds object that the window will now be set to 65 | 66 | 67 | #### open.send(channel, payload) 68 | Sends a message - a payload on a specific channel - to the BrowserWindow 69 | 70 | **Kind**: instance method of [open](#exp_module_fbwindow--open) 71 | **See**: [BrowserWindow.webContents.send()](https://electronjs.org/docs/api/web-contents#contentssendchannel-arg1-arg2-) 72 | 73 | | Param | Type | Description | 74 | | --- | --- | --- | 75 | | channel | string | A topic which the BrowserWindow should be expecting | 76 | | payload | string \| number \| object \| array | The message content to send on the topic | 77 | 78 | 79 | 80 | #### open.receive(channel, [callback]) ⇒ Promise.<(string\|number\|object\|array)> 81 | Receives a message event from the BrowserWindow, with optional callback or Promise interface. The callback or Promise will fire whenever a message event is received on the channel. 82 | 83 | **Kind**: instance method of [open](#exp_module_fbwindow--open) 84 | **Returns**: Promise.<(string\|number\|object\|array)> - If no callback is supplied then a Promise is returned 85 | **See**: [BrowserWindow.webContents.send()](https://electronjs.org/docs/api/web-contents#contentssendchannel-arg1-arg2-) 86 | 87 | | Param | Type | Description | 88 | | --- | --- | --- | 89 | | channel | string | A topic which the BrowserWindow should be expecting | 90 | | [callback] | function | Optional callback function to receive the message event | 91 | 92 | 93 | 94 | #### open.close() 95 | Close the window. 96 | 97 | **Kind**: instance method of [open](#exp_module_fbwindow--open) 98 | 99 | 100 | #### open~openModal(urlToOpen, parentWindow, setOptions) ⇒ Promise.<WindowObject> 101 | Similar to window.open() except a modal window is created as a child to the parentWindow. 102 | 103 | **Kind**: inner method of [open](#exp_module_fbwindow--open) 104 | **Returns**: Promise.<WindowObject> - An WindowObject inhereted from BrowserWindow 105 | **See**: [BrowserWindow options](https://electronjs.org/docs/api/browser-window) 106 | 107 | | Param | Type | Description | 108 | | --- | --- | --- | 109 | | urlToOpen | string | Opens the window and loads this page | 110 | | parentWindow | BrowserWindow | A BrowserWindow which will contain the model window | 111 | | setOptions | options | A set of options for changing the window properties | 112 | 113 | -------------------------------------------------------------------------------- /electron-firebase.js: -------------------------------------------------------------------------------- 1 | /* electron-firebase.js 2 | * Copyright (c) 2019-2021 by David Asher, https://github.com/david-asher 3 | */ 4 | 'use strict'; 5 | 6 | /** 7 | * API interface to electron-firebase, pulls in all needed modules 8 | * @module electron-firebase 9 | */ 10 | 11 | module.exports = { 12 | applib: require('./lib/applibrary'), 13 | auth: require('./lib/authentication'), 14 | fbstorage: require('./lib/fbstorage'), 15 | file: require('./lib/fileutils'), 16 | firestore: require('./lib/firestore'), 17 | local: require('./lib/localstorage'), 18 | server: require('./lib/webserver'), 19 | fbwindow: require('./lib/windows'), 20 | mainapp: require('./lib/mainapp') 21 | } -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "source": "functions" 4 | }, 5 | "firestore": { 6 | "rules": "config/cloud-firestore.rules" 7 | }, 8 | "storage": [ 9 | { 10 | "rules": "config/firebase-storage.rules", 11 | "bucket": "" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /functions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | /* index.js 2 | * Copyright (c) 2019-2021 by David Asher, https://github.com/david-asher 3 | * 4 | * This cloud function must be deployed to the firebase account in order to 5 | * pass a custom token back to the main app (node.js) after a browser-based 6 | * login completes, so that the main app can sign in with the same user 7 | * credential. 8 | * 9 | * The firebase tools Command Line Interface (CLI) must be installed. 10 | * npm install -g firebase-tools 11 | * see: https://github.com/firebase/firebase-tools 12 | * 13 | * This file is pushed to the firebase cloud functions with the command: 14 | * firebase deploy 15 | */ 16 | 17 | // The Cloud Functions for Firebase SDK to create Cloud Functions and setup triggers. 18 | const functions = require('firebase-functions'); 19 | 20 | // The Firebase Admin SDK to access the Firebase Realtime Database. 21 | const admin = require('firebase-admin'); 22 | 23 | var App = null 24 | 25 | const customToken = functions.https.onCall( (data,context) => 26 | { 27 | try { 28 | if ( !App ) App = admin.initializeApp( { 29 | serviceAccountId: data.serviceAccountId, 30 | projectId: data.projectId 31 | }) 32 | return admin.auth().createCustomToken( data.userid ) 33 | } 34 | catch( error ) { 35 | functions.logger.log( "customToken error = ", error ); 36 | return { error: error } 37 | } 38 | }) 39 | 40 | module.exports = { 41 | customToken: customToken 42 | } -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "scripts": { 5 | "serve": "firebase serve --only functions", 6 | "shell": "firebase functions:shell", 7 | "start": "npm run shell", 8 | "deploy": "firebase deploy --only functions", 9 | "logs": "firebase functions:log" 10 | }, 11 | "engines": { 12 | "node": "10" 13 | }, 14 | "dependencies": { 15 | "firebase-admin": "^9.6.0", 16 | "firebase-functions": "^3.13.2" 17 | }, 18 | "devDependencies": { 19 | "firebase-functions-test": "^0.1.7" 20 | }, 21 | "private": true 22 | } 23 | -------------------------------------------------------------------------------- /install_app/install-tools.js: -------------------------------------------------------------------------------- 1 | /* install-tools.js 2 | * Copyright (c) 2019-2021 by David Asher, https://github.com/david-asher 3 | * 4 | * Helper functions for other install scripts. 5 | */ 6 | 'use strict'; 7 | 8 | const fs = require('fs') 9 | const os = require('os') 10 | const path = require('path') 11 | const { env } = require('process') 12 | const { execSync } = require( 'child_process' ) 13 | 14 | function getInstallPaths() 15 | { 16 | // process.cwd() is root of electron-firebase folder in node_modules 17 | // process.env.INIT_CWD is root of project folder 18 | // __dirname is postinstall script folder 19 | 20 | var moduleRoot, projectRoot 21 | // moduleRoot is the source; projectRoot is the target 22 | 23 | if ( undefined == process.env.INIT_CWD ) { 24 | // for local testing, e.g. at project root, run: 25 | // node ./node_modules/electron-firebase/install_app/postinstall.js 26 | moduleRoot = path.dirname( __dirname ) 27 | projectRoot = `${process.cwd()}${path.sep}` 28 | } 29 | else { 30 | // normal npm install case 31 | moduleRoot = `${process.cwd()}${path.sep}` 32 | projectRoot = `${process.env.INIT_CWD}${path.sep}` 33 | } 34 | return { 35 | moduleRoot: moduleRoot, 36 | projectRoot: projectRoot 37 | } 38 | } 39 | 40 | function getModified( filePath ) 41 | { 42 | try { 43 | const fileStats = fs.statSync( filePath ) 44 | return new Date( fileStats.mtime ) 45 | } 46 | catch (error) { 47 | return null 48 | } 49 | } 50 | 51 | function touchFile( filePath, timeStamp ) 52 | { 53 | if ( !timeStamp ) timeStamp = new Date() 54 | fs.utimesSync( filePath, timeStamp, timeStamp ) 55 | } 56 | 57 | /* 58 | * This function will not overwrite a file that has been modified more 59 | * recently than the lastUpdate; set lastUpdate to Date.now() to force overwrite. 60 | * After a successful copy the access and modified times will be set to timeStamp. 61 | */ 62 | function copyFile( filename, sourceFolder, targetFolder, timeStamp, lastUpdate ) 63 | { 64 | try { 65 | const sourceFile = path.join( sourceFolder, filename ) 66 | const targetFile = path.join( targetFolder, filename ) 67 | // check for user modified file and do not overwrite 68 | const mTimeTarget = getModified( targetFile ) 69 | if ( +mTimeTarget > +lastUpdate ) return 70 | // copy the file but we need to update the timestamps ourselves 71 | fs.copyFileSync( sourceFile, targetFile ) 72 | touchFile( targetFile, timeStamp ) 73 | } 74 | catch (error) { 75 | if ( error.code == 'EEXIST') return 76 | throw( error ) 77 | } 78 | } 79 | 80 | function copyFolderFiles( sourceFolder, targetFolder, timeStamp, lastUpdate ) 81 | { 82 | const dirList = fs.readdirSync( sourceFolder, { withFileTypes: true } ) 83 | dirList.forEach( (file) => { 84 | if ( !file.isFile() ) return 85 | copyFile( file.name, sourceFolder, targetFolder, timeStamp, lastUpdate ) 86 | }) 87 | } 88 | 89 | function makeFolder( folderPath ) 90 | { 91 | try { 92 | fs.mkdirSync( folderPath ) 93 | } 94 | catch( error ) { 95 | if ( error && error.code == 'EEXIST' ) return 96 | console.error( error ) 97 | } 98 | } 99 | 100 | function copyFolder( folderName, sourceParent, targetParent, timeStamp, lastUpdate ) 101 | { 102 | const sourceFolder = path.join( sourceParent, folderName ) 103 | if ( !fs.statSync( sourceFolder ).isDirectory() ) { 104 | console.error( "Source folder does not exist: ", sourceFolder ) 105 | return 106 | } 107 | 108 | const targetFolder = path.join( targetParent, folderName ) 109 | makeFolder( targetFolder ) 110 | if ( !fs.statSync( targetFolder ).isDirectory() ) { 111 | console.error( "Failed to create target folder: ", targetFolder ) 112 | return 113 | } 114 | copyFolderFiles( sourceFolder, targetFolder, timeStamp, lastUpdate ) 115 | } 116 | 117 | function isObject( it ) 118 | { 119 | return ( Object.prototype.toString.call( it ) === '[object Object]' ) 120 | } 121 | 122 | function omerge( oTarget, oUpdate ) 123 | { 124 | if ( !isObject( oUpdate ) ) return oUpdate 125 | for ( var key in oUpdate ) { 126 | oTarget[key] = omerge( oTarget[key], oUpdate[key] ) 127 | } 128 | return oTarget 129 | } 130 | 131 | function backupFile( filePath ) 132 | { 133 | var backupParts = filePath.split( '.' ) 134 | backupParts.splice( -1, 0, "old" ) 135 | const backupPath = backupParts.join( '.' ) 136 | fs.copyFileSync( filePath, backupPath ) 137 | } 138 | 139 | function updateJsonFile( jsonFile, updateJson ) 140 | { 141 | const sourceJson = require( jsonFile ) 142 | backupFile( jsonFile ) 143 | fs.writeFileSync( jsonFile, JSON.stringify( omerge( sourceJson, updateJson ), null, 2 ) ) 144 | } 145 | 146 | function checkCommand( commandString ) 147 | { 148 | var exists = true 149 | try { 150 | // stdio to pipe because we don't want to see the output 151 | execSync( `${commandString} --version`, {stdio : 'pipe' } ) 152 | } 153 | catch (error) { 154 | exists = false 155 | } 156 | return exists 157 | } 158 | 159 | function installApp( commandString, appInstallString, bQuiet ) 160 | { 161 | // check for command existence before installing 162 | if ( !checkCommand( commandString ) ) { 163 | execSync( appInstallString, { stdio: bQuiet ? 'pipe' : 'inherit' } ) 164 | } 165 | // if this failed, stop, because we can't build 166 | if ( !checkCommand( commandString ) ) { 167 | throw( "Cannot find " + commandString + " and failed to install it. " ) 168 | } 169 | } 170 | 171 | function addToPath( newPath ) 172 | { 173 | env.PATH = `${newPath}${path.delimiter}${env.PATH}` 174 | } 175 | 176 | function makeNpmGlobal( globalFolder ) 177 | { 178 | const npmGlobal = path.join( os.homedir(), globalFolder ) 179 | makeFolder( npmGlobal ) 180 | addToPath( npmGlobal ) 181 | const npmGlobalBin = path.join( npmGlobal, "bin" ) 182 | makeFolder( npmGlobalBin ) 183 | addToPath( npmGlobalBin ) 184 | execSync( `npm config set prefix "${npmGlobal}"` ) 185 | } 186 | 187 | module.exports = { 188 | getInstallPaths: getInstallPaths, 189 | getModified: getModified, 190 | touchFile: touchFile, 191 | copyFile: copyFile, 192 | copyFolderFiles: copyFolderFiles, 193 | makeFolder: makeFolder, 194 | copyFolder: copyFolder, 195 | isObject: isObject, 196 | omerge: omerge, 197 | backupFile: backupFile, 198 | updateJsonFile: updateJsonFile, 199 | checkCommand: checkCommand, 200 | installApp: installApp, 201 | addToPath: addToPath, 202 | makeNpmGlobal: makeNpmGlobal 203 | } -------------------------------------------------------------------------------- /install_app/package-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "rebuild": "./node_modules/.bin/electron-rebuild", 4 | "deploy": "node ./config/firebase-deploy.js", 5 | "start": "electron ./main.js" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /install_app/postinstall.js: -------------------------------------------------------------------------------- 1 | /* postinstall.js 2 | * Copyright (c) 2019-2021 by David Asher, https://github.com/david-asher 3 | * 4 | * post-installation script for electron-firebase 5 | */ 6 | 'use strict'; 7 | 8 | const { execSync } = require( 'child_process' ) 9 | const { exit, env } = require('process') 10 | const { join } = require('path') 11 | const it = require( './install-tools' ) 12 | 13 | const topLevelFolders = [ 14 | "config", 15 | "pages", 16 | "scripts", 17 | "functions" 18 | ] 19 | 20 | const appFileList = [ 21 | ".firebaserc", 22 | "firebase.json", 23 | "answerBrowser.js", 24 | "setupApp.js", 25 | "main.js" 26 | ] 27 | 28 | function postInstall() 29 | { 30 | // set loglevel to quiet multiple warnings that we can't control 31 | env.npm_config_loglevel = "error" 32 | const timeStamp = new Date() 33 | const { moduleRoot, projectRoot } = it.getInstallPaths() 34 | it.makeNpmGlobal( ".npm-global" ) 35 | 36 | console.log( "** Update package.json scripts" ) 37 | const packageFile = join( projectRoot, "package.json" ) 38 | const updateFile = join( moduleRoot, "install_app", "package-update.json" ) 39 | const lastUpdate = it.getModified( updateFile ) 40 | it.updateJsonFile( packageFile, require( updateFile ) ) 41 | 42 | console.log( "** Populate top-level folders" ) 43 | topLevelFolders.forEach( (folderName) => { 44 | it.copyFolder( folderName, moduleRoot, projectRoot, timeStamp, lastUpdate ) 45 | }) 46 | 47 | console.log( "** Copy example application files" ) 48 | appFileList.forEach( (fileName) => { 49 | it.copyFile( fileName, moduleRoot, projectRoot, timeStamp, lastUpdate ) 50 | }) 51 | 52 | console.log( "** Rebuilding Electron, this will take a few minutes." ) 53 | execSync( "npm run rebuild" ) 54 | 55 | // leave the package-update.json file with newer modified time so the next update can check it 56 | it.touchFile( updateFile ) 57 | 58 | console.log( "** Installing firebase-tools, required to deploy functions to the cloud." ) 59 | it.installApp( 'firebase-tools', "npm install -g firebase-tools", true ) 60 | } 61 | 62 | (function () 63 | { 64 | try { 65 | postInstall() 66 | } 67 | catch(error) { 68 | console.log( error ) 69 | } 70 | exit(0) 71 | })() 72 | -------------------------------------------------------------------------------- /install_app/preinstall.js: -------------------------------------------------------------------------------- 1 | /* preinstall.js 2 | * Copyright (c) 2019-2021 by David Asher, https://github.com/david-asher 3 | * 4 | * pre-installation script for electron-firebase 5 | */ 6 | 'use strict'; 7 | 8 | const it = require( './install-tools' ) 9 | const { env } = require( 'process' ); 10 | // yes the above needs a semicolon else the IIFE below throws an error. whatever. 11 | 12 | (function () 13 | { 14 | env.npm_config_loglevel = "error" 15 | console.log( "Please be patient, electron and firebase are large projects and installation may take a few minutes." ) 16 | it.makeNpmGlobal( ".npm-global" ) 17 | it.installApp( 'node-gyp', "npm install -g node-gyp", true ) 18 | })() 19 | -------------------------------------------------------------------------------- /lib/applibrary.js: -------------------------------------------------------------------------------- 1 | /* applibrary.js 2 | * Copyright (c) 2019-2020 by David Asher, https://github.com/david-asher 3 | */ 4 | 'use strict'; 5 | 6 | /** 7 | * Collection of utilities for JSON, objects, events, web request. 8 | * @module applib 9 | */ 10 | 11 | // this is the global event emitted that all modules and the main app should share 12 | const eventsHandler = require('events') 13 | const eventEmitter = new eventsHandler.EventEmitter() 14 | const https = require('https') 15 | const webRequest = require('axios') 16 | const querystring = require('querystring') 17 | 18 | // when making an https request to localhost (like for testing), disable self-service cert rejection 19 | const httpsAcceptAgent = new https.Agent({ rejectUnauthorized: false }) 20 | 21 | /** 22 | * Tests whether the input looks like a JSON string. 23 | * @param {*} s - a parameter to be tested 24 | * @returns {boolean} True if the input is likely a JSON string 25 | * @alias module:applib 26 | */ 27 | function isJSON( s ) 28 | { 29 | return ( typeof s == 'string' ) && ( s.charAt(0) == '{' || s.charAt(0) == '[' ) 30 | } 31 | 32 | /** 33 | * Tests whether the input is an object. 34 | * @param {*} obj - a parameter to be tested 35 | * @returns {boolean} True if the input is an object 36 | * @alias module:applib 37 | */ 38 | function isObject( obj ) 39 | { 40 | return ( typeof obj == 'object' && obj !== null ) 41 | } 42 | 43 | /** 44 | * Converts a JSON string to an object, handling errors so this won't throw an exception. 45 | * @param {string} inputSerialized - A JSON string 46 | * @return {object} Null if there is an error, else a valid object 47 | * @alias module:applib 48 | */ 49 | function parseJSON( inputSerialized ) 50 | { 51 | var outputObject = null 52 | if ( inputSerialized == null ) return null 53 | if ( !isJSON( inputSerialized ) ) { 54 | return inputSerialized 55 | } 56 | try { 57 | if( inputSerialized.content ) inputSerialized = inputSerialized.content 58 | // accept an object (for faster pass-by-reference) and assume .content property 59 | outputObject = JSON.parse( inputSerialized ) 60 | } 61 | catch (error) { 62 | console.error( "parseJSON error: ", error ) 63 | // else nothing, leave outputObject as null 64 | } 65 | return outputObject 66 | } 67 | 68 | function _formatJSON( inputObject, jsonSpaceString ) 69 | { 70 | var outputString = null 71 | if ( inputObject == null ) return outputString 72 | if ( inputObject.content ) inputObject = inputObject.content 73 | if ( typeof inputObject !== 'object' ) { 74 | console.error( "ERROR on stringifyJSON: ", inputObject ) 75 | } 76 | try { 77 | // accept an object (for faster pass-by-reference) and assume .content property 78 | outputString = JSON.stringify( inputObject, null, jsonSpaceString ) 79 | } 80 | catch (error) { 81 | console.error( "stringifyJSON error: ", error ) 82 | // else nothing, leave outputString as null 83 | } 84 | return outputString 85 | } 86 | 87 | /** 88 | * Converts an object into a JSON string with space/newline formatting, handling errors so it won't throw an exception. 89 | * @param {object} inputObject - a valid JavaScript object 90 | * @returns {string} Null if there is an error, else a JSON string 91 | * @alias module:applib 92 | */ 93 | function stringifyJSON( inputObject ) 94 | { 95 | // 4 is the number of spaces for indenting each newline level 96 | return _formatJSON( inputObject, 4 ) 97 | } 98 | 99 | /** 100 | * Same as stringifyJSON except the result is compact without spaces and newlines. 101 | * @param {object} inputObject - a valid JavaScript object 102 | * @returns {string} Null if there is an error, else a JSON string 103 | * @alias module:applib 104 | */ 105 | function compactJSON( inputObject ) 106 | { 107 | return _formatJSON( inputObject, null ) 108 | } 109 | 110 | /** 111 | * Performs a deep merge of the input objects. 112 | * @param {...any} objects - A parameter set (comma-separated) of objects 113 | * @returns {object} A JavaScript object 114 | * @alias module:applib 115 | */ 116 | function mergeObjects( ...objects ) 117 | // from: https://stackoverflow.com/questions/27936772/how-to-deep-merge-instead-of-shallow-merge 118 | { 119 | return objects.reduce((prev, obj) => { 120 | Object.keys(obj).forEach(key => { 121 | const pVal = prev[key]; 122 | const oVal = obj[key]; 123 | if (Array.isArray(pVal) && Array.isArray(oVal)) { 124 | prev[key] = pVal.concat(...oVal) 125 | return 126 | } 127 | if (isObject(pVal) && isObject(oVal)) { 128 | prev[key] = mergeObjects(pVal, oVal) 129 | return 130 | } 131 | prev[key] = oVal 132 | }); 133 | return prev 134 | }, {} ) 135 | } 136 | 137 | /** 138 | * Works like the deprecated node.js url.format() function, but no auth. 139 | * @param {*} urlObject - A URL object (as returned by url.parse() or constructed otherwise). 140 | * @returns {String} A formatted URL string derived from urlObject. 141 | * @alias module:applib 142 | * @see {@link https://nodejs.org/api/url.html#url_url_format_urlobject} 143 | */ 144 | function urlFormat( urlObject ) 145 | { 146 | var newUrl = ( urlObject.protocol || "https" ) + "://" 147 | if ( urlObject.host ) newUrl += urlObject.host 148 | else if ( urlObject.hostname ) { 149 | newUrl += urlObject.hostname 150 | if ( urlObject.port ) newUrl += ":" + urlObject.port 151 | } 152 | else return null // error, no host 153 | if ( urlObject.pathname ) newUrl += "/" + urlObject.pathname 154 | if ( !urlObject.query ) return newUrl 155 | return newUrl + "?" + querystring.stringify( urlObject.query ) 156 | } 157 | 158 | /** 159 | * Interface for the npm request HTTP client. The response object from the returned 160 | * promise contains these important properties: .status, .statusText, .headers, .data 161 | * @param {object} options - Parameters that define this request 162 | * @returns {Promise} Promise object represents the HTTP response 163 | * @alias module:applib 164 | * @see {@link https://www.npmjs.com/package/axios} 165 | * @see {@link https://nodejs.org/api/https.html#https_https_request_options_callback} 166 | * @see {@link https://nodejs.org/api/http.html#http_class_http_serverresponse} 167 | */ 168 | function request( options ) 169 | { 170 | // originally designed based on request, now deprecated 171 | // convert request options format to axios 172 | const reqOptions = { ...options } 173 | if ( isObject( options.url ) ) reqOptions.url = urlFormat( options.url ) 174 | if ( options.qs && !options.params ) reqOptions.params = options.qs 175 | if ( options.body && !options.data ) reqOptions.data = options.body 176 | 177 | // disable self-signed certificate rejection only for a localhost request 178 | if ( 0 == reqOptions.url.indexOf( "https://localhost" )) reqOptions.httpsAgent = httpsAcceptAgent 179 | 180 | // electron should look like chrome when making API requests 181 | if ( !reqOptions.headers ) reqOptions.headers = {} 182 | reqOptions.headers['User-Agent'] = "Chrome" 183 | 184 | // axios is already a promise, returning the HTTP response object 185 | return webRequest( reqOptions ) 186 | } 187 | 188 | 189 | 190 | module.exports = { 191 | isJSON: isJSON, 192 | isObject: isObject, 193 | parseJSON: parseJSON, 194 | compactJSON: compactJSON, 195 | stringifyJSON: stringifyJSON, 196 | mergeObjects: mergeObjects, 197 | urlFormat: urlFormat, 198 | event: eventEmitter, 199 | request: request 200 | } 201 | -------------------------------------------------------------------------------- /lib/authentication.js: -------------------------------------------------------------------------------- 1 | /* authentication.js 2 | * Copyright (c) 2019-2020 by David Asher, https://github.com/david-asher 3 | */ 4 | 'use strict'; 5 | 6 | /** 7 | * Authentication workflow for Google Firebase. 8 | * @see {@link https://github.com/firebase/FirebaseUI-Web|FirebaseUI for Web} 9 | * @module auth 10 | */ 11 | 12 | const firebase = require('firebase') 13 | const applib = require( './applibrary' ) 14 | const window = require( './windows' ) 15 | 16 | /** 17 | * Must be called before any operations on Firebase API calls. 18 | * @alias module:auth 19 | */ 20 | function initializeFirebase() 21 | { 22 | // we must do this before using firebase 23 | try { 24 | firebase.initializeApp( global.fbConfig ); 25 | } 26 | catch (error) { 27 | console.error( "initializeFirebase: ", error ) 28 | } 29 | } 30 | 31 | /** 32 | * Firestore is a Google NoSQL datastore. This function returns a reference that can be used with the Firestore API. 33 | * @returns {Firestore} An interface to Firestore 34 | * @alias module:auth 35 | * @see {@link https://firebase.google.com/docs/firestore/|Firestore} 36 | */ 37 | function firestore() 38 | { 39 | return firebase.firestore() 40 | } 41 | 42 | /** 43 | * Return the unique path prefix for a user. 44 | * @returns {string} A path string 45 | * @alias module:auth 46 | */ 47 | function userPath() 48 | { 49 | return `users/${firebase.auth().currentUser.uid}` 50 | } 51 | 52 | /** 53 | * Executes an API call to Google Cloud, taking care of user authentication and token refresh. 54 | * @param requestOptions - A set of option parameters for the API request 55 | * @param {string|object} requestOptions.url - HTTP(S) endpoint to call, string or object in the format of url.parse() 56 | * @param {string} requestOptions.method - HTTP verb, e.g. GET, POST, etc. 57 | * @param {object} requestOptions.headers - An object with any additional request headers 58 | * @returns {Promise} Promise object represents the payload response of the API call (string|object|buffer) 59 | * @alias module:auth 60 | * @see {@link https://github.com/request/request#requestoptions-callback|Request Options} 61 | */ 62 | async function gcpApi( requestOptions ) 63 | { 64 | try { 65 | // make a copy so that we don't munge the original request 66 | const gcOptions = { ...requestOptions } 67 | 68 | // fix the ?feature? where request won't work unless both host and hostname are set 69 | // where either uri or url can be set, as either strings or objects 70 | if ( !gcOptions.url && gcOptions.uri ) { 71 | gcOptions.url = gcOptions.uri 72 | delete gcOptions.uri 73 | } 74 | if ( typeof gcOptions.url == 'string' ) { 75 | const newurl = new URL( gcOptions.url ) 76 | gcOptions.url = { 77 | protocol: 'https:', 78 | hostname: newurl.hostname, 79 | pathname: newurl.pathname 80 | } 81 | } 82 | gcOptions.url.pathname = gcOptions.url.pathname.replace(/^\/+/, '') 83 | 84 | // only authorized users with access token, over https 85 | if ( !gcOptions.method ) gcOptions.method = 'GET' 86 | if ( !gcOptions.headers ) gcOptions.headers = {} 87 | 88 | // first check access token expiration 89 | var token = await firebase.auth().currentUser.getIdToken() 90 | gcOptions.headers.authorization = `Firebase ${token}` 91 | 92 | // synchronous API call to GCP 93 | const response = await applib.request( gcOptions ) 94 | 95 | // a good response may be a bad response, so throw a fit 96 | if ( response.status >= 400 ) { 97 | throw( { error: response.statusText, code: response.status, message: response.data } ) 98 | } 99 | 100 | // return the requested data 101 | return applib.parseJSON( response.data ) 102 | } 103 | catch (error) { 104 | console.error( "gcpApi: ", error, requestOptions ) 105 | return error 106 | } 107 | } 108 | 109 | /** 110 | * Completes the authentication workflow for a new user. The user credential will be saved in as a 111 | * web browser identity persistence so it can be recovered on a subsequent session without forcing 112 | * the user to log in again. 113 | * @param {object} newUser - This is an object passed from the Web UI for authentication after a successful registration of a new user 114 | * @returns {Promise} A Promise object representing the user object 115 | * @alias module:auth 116 | */ 117 | async function signInNewUser( newUser ) 118 | { 119 | // We will use a firebase custom token to pass credentials from the web process. 120 | // This is why a service account must be registered in firebase-config.json 121 | // see: https://firebase.google.com/docs/auth/admin/create-custom-tokens#sign_in_using_custom_tokens_on_clients 122 | // see: https://firebase.google.com/docs/reference/node/firebase.auth.Auth#signinwithcustomtoken 123 | 124 | try { 125 | // keep the new user object for global access throughout the app 126 | global.user = newUser.user 127 | 128 | // make remote call to firebase function customToken 129 | const getToken = firebase.functions().httpsCallable('customToken') 130 | const customTokenParams = { 131 | userid: newUser.user.uid, 132 | serviceAccountId: global.fbConfig.serviceAccountId, 133 | projectId: global.fbConfig.projectId 134 | } 135 | const result = await getToken( customTokenParams ) 136 | 137 | // pass the custom token to firebase to sign in 138 | const userToken = await firebase.auth().signInWithCustomToken( result.data ) 139 | 140 | // returns a UserCredential: https://firebase.google.com/docs/reference/node/firebase.auth#usercredential 141 | return userToken.user 142 | } 143 | catch (error) { 144 | console.error( "signInNewUser: ", error ) 145 | return null 146 | } 147 | 148 | /* 149 | // IF Google decides to return an oauth refresh token, we would be able to use that to 150 | // refresh the IDP token and pass that back to the main app to sign in. This works, 151 | // however the token expires an hour after the first sign in, so that would force the 152 | // user to sign in frequently, mostly defeating the auth persistence feature. Instead 153 | // we are now using a custom token to transfer credentials, as coded above. 154 | 155 | const authCredential = firebase.auth.AuthCredential.fromJSON( newUser.credential ) 156 | return firebase.auth().signInWithCredential( authCredential ) 157 | .then( (thisCredential) => { 158 | resolve( firebase.auth().currentUser ) 159 | }) 160 | .catch( (error) => { 161 | console.error( "signInWithCredential ERROR: ", error ) 162 | reject( error ) 163 | }) 164 | */ 165 | } 166 | 167 | /** 168 | * Initiates the Firebase UI authentication workflow. nodeIntegration must be set to false because it would 169 | * expose the login page to hacking through the IPC interface. 170 | * @param {BrowserWindow} mainWindow - The parent (or main) window, so that the workflow window can be modal 171 | * @returns {Promise} A Promise object representing the new modal window for authentication workflow 172 | * @alias module:auth 173 | */ 174 | function startNewSignIn( bSignOutUser ) 175 | { 176 | try { 177 | const urlParams = { 178 | protocol: "https", 179 | hostname: "localhost", 180 | port: global.appConfig.webapp.port, 181 | pathname: global.appConfig.webapp.loginStart, 182 | query: { 183 | loginRedirect: '/' + global.appConfig.webapp.loginRedirect, 184 | firebaseconfig: global.appConfig.apis.firebaseconfig, 185 | logintoken: global.appConfig.apis.logintoken, 186 | loginready: global.appConfig.apis.loginready 187 | } 188 | } 189 | if ( bSignOutUser ) urlParams.query.signoutuser = true 190 | if ( global.appConfig.webapp.persistentUser ) urlParams.query.persistentUser = true 191 | const loginUrl = applib.urlFormat( urlParams ) 192 | const windowOptions = { 193 | width: 1200, 194 | height: 800, 195 | frame: false, 196 | show: false, 197 | webPreferences: { 198 | nodeIntegration: false, 199 | enableRemoteModule: false, 200 | contextIsolation: true, 201 | sandbox:true 202 | } 203 | } 204 | const urlOptions = { 205 | userAgent: global.userAgent, 206 | } 207 | 208 | applib.event.emit( "start-new-signin" ) 209 | const newModal = window.openModal( loginUrl, null, windowOptions, urlOptions ) 210 | 211 | // Normally a browser reports the broadest possible userAgent to maximize compatibility, 212 | // but in the case of authentication we want the narrowest possible userAgent. 213 | newModal.webContents.session.webRequest.onBeforeSendHeaders((details,callback) => { 214 | details.requestHeaders["User-Agent"] = "Chrome" 215 | callback({ 216 | requestHeaders: details.requestHeaders 217 | }) 218 | }) 219 | 220 | return newModal 221 | } 222 | catch (error) { 223 | console.error( "startNewSignIn: ", error ) 224 | return null 225 | } 226 | } 227 | 228 | /** 229 | * Gets the identity provider that was used to authenticate the current user. 230 | * @returns {string} The firebase representation of the identity provider, can be any of: 231 | * "google.com", 232 | * "github.com", 233 | * "twitter.com", 234 | * "facebook.com", 235 | * "password", 236 | * "phone" 237 | * @alias module:auth 238 | */ 239 | function getProvider() 240 | { 241 | // get the current user and authentication provider 242 | return ( global.currentUser || {} ).providerId || null 243 | } 244 | 245 | /** 246 | * Firebase UI doesn't have a workflow for logging out from the identity provider, so this function 247 | * returns a URL that can be used to log out directly -- if the identity provider doesn't change the URL. 248 | * @param {string} provider - The name of the identity provider, from getProvider() 249 | * @returns {string} A URL that can be called to log out of the identity provider 250 | * @alias module:auth 251 | */ 252 | function getSignOutUrl( provider ) 253 | { 254 | if ( !provider ) provider = getProvider() 255 | if ( !provider ) return null 256 | return global.appConfig.logout[provider] || null 257 | } 258 | 259 | /** 260 | * Logs out the user from Firebase, but not from the identity provider. 261 | * @alias module:auth 262 | */ 263 | function signOutUser() 264 | { 265 | applib.event.emit( "signout-user" ) 266 | // perform the firebase logout, which must be done both here in the main app and also in the browser 267 | try { 268 | firebase.auth().signOut() 269 | return true 270 | } 271 | catch (error) { 272 | console.error( "signOutUser: ", error ) 273 | return false 274 | } 275 | } 276 | 277 | /** 278 | * Performs a complete signout from Firebase and the identity provider. 279 | * @param {string} provider - The identity provider, from getProvider() 280 | * @param {BrowserWindow} mainWindow - A parent window, so the logout window can be modal 281 | * @returns {BrowserWindow} A new window that was used for the identity provider logout 282 | * @alias module:auth 283 | */ 284 | function signOutProvider( provider, mainWindow ) 285 | { 286 | try { 287 | // clear all the browser session state, after a logout 288 | mainWindow.webContents.session.clearStorageData() 289 | mainWindow.webContents.session.clearCache( () => {} ) 290 | 291 | const signoutUrl = getSignOutUrl( provider ) 292 | if ( !signoutUrl ) return null 293 | 294 | var logoutWindow = window.openModal( signoutUrl, mainWindow, { 295 | width: 1200, 296 | height: 800, 297 | webPreferences: { 298 | nodeIntegration: false, 299 | enableRemoteModule: false 300 | } 301 | }) 302 | 303 | // logout pages will try to redirect after logout, so catch that event and close the window 304 | logoutWindow.webContents.on( 'did-navigate', (appEvent,tourl) => { 305 | logoutWindow.close() 306 | logoutWindow = null 307 | }) 308 | return logoutWindow 309 | } 310 | catch (error) { 311 | console.error( "signOutProvider: ", provider, error ) 312 | return null 313 | } 314 | } 315 | 316 | function getCurrentUser() 317 | { 318 | return firebase.auth().currentUser 319 | } 320 | 321 | module.exports = { 322 | initializeFirebase: initializeFirebase, 323 | getProvider: getProvider, 324 | getSignOutUrl: getSignOutUrl, 325 | signInNewUser: signInNewUser, 326 | startNewSignIn: startNewSignIn, 327 | signOutUser: signOutUser, 328 | signOutProvider: signOutProvider, 329 | userPath: userPath, 330 | getCurrentUser: getCurrentUser, 331 | gcpApi: gcpApi, 332 | firestore: firestore, 333 | FieldValue: firebase.firestore.FieldValue 334 | } 335 | 336 | -------------------------------------------------------------------------------- /lib/fileutils.js: -------------------------------------------------------------------------------- 1 | /* fileutils.js 2 | * Copyright (c) 2019 by David Asher, https://github.com/david-asher 3 | */ 4 | 'use strict'; 5 | 6 | /** 7 | * Functions for local file I/O. All functions are synchronous. 8 | * @module file 9 | */ 10 | 11 | const fs = require('fs') 12 | const { execSync } = require('child_process'); 13 | const applib = require('./applibrary') 14 | 15 | /** 16 | * Reads a local file and returns the contents. 17 | * @param {string} fileName - Path to local file 18 | * @return {string|buffer} - File contents, will be converted to a string if possible 19 | * @alias module:file 20 | */ 21 | function readFile( fileName ) 22 | { 23 | try { 24 | const fileRaw = fs.readFileSync( fileName ) 25 | return Buffer.isBuffer( fileRaw ) ? fileRaw.toString() : fileRaw 26 | } 27 | catch (error) { 28 | console.error( "readFile error on ", fileName, ": ", error ) 29 | return null 30 | } 31 | } 32 | 33 | /** 34 | * Writes buffer or string content to a local file. 35 | * @param {string} fileName - Path to local file 36 | * @param {string|buffer} fileContent - Content to write 37 | * @alias module:file 38 | */ 39 | function writeFile( fileName, fileContent ) 40 | { 41 | try { 42 | fs.writeFileSync( fileName, fileContent ) 43 | return true 44 | } 45 | catch (error) { 46 | console.error( "writeFile error on ", fileName, ": ", error ) 47 | return false 48 | } 49 | } 50 | 51 | /** 52 | * Check if a local file exists. 53 | * @param {string} fileName - Path to local file 54 | * @return {boolean} True if the file exists 55 | * @alias module:file 56 | */ 57 | function isFile( fileName ) 58 | { 59 | try { 60 | return fs.statSync( fileName ).isFile() 61 | } 62 | catch (error) { 63 | return false 64 | } 65 | } 66 | 67 | /** 68 | * Check if the given path is a folder. 69 | * @param {string} folderName - Path to local folder 70 | * @return {boolean} True if the give path exists and is a folder 71 | * @alias module:file 72 | */ 73 | function isFolder( folderName ) 74 | { 75 | try { 76 | return fs.statSync( folderName ).isDirectory() 77 | } 78 | catch (error) { 79 | return false 80 | } 81 | } 82 | 83 | /** 84 | * Create a new folder at the given path. 85 | * @param {string} folderName - Path to local folder 86 | * @return {boolean} True if the folder was successfully created 87 | * @alias module:file 88 | */ 89 | function makeFolder( folderName ) 90 | { 91 | try { 92 | fs.mkdirSync( folderName ) 93 | return isFolder( folderName ) 94 | } 95 | catch (error) { 96 | return false 97 | } 98 | } 99 | 100 | // filter the list of DirEnt objects 101 | function _filterFolder( dirEntList, filterType ) 102 | { 103 | const flist = [] 104 | dirEntList.forEach( (element,index) => { 105 | if ( element.name.charAt( 0 ) == '.' ) return 106 | switch( filterType ) { 107 | case 1: 108 | if ( element.isFile() ) flist.push( element.name ) 109 | break 110 | case 2: 111 | if ( element.isDirectory() ) flist.push( element.name ) 112 | break 113 | } 114 | }) 115 | return flist 116 | } 117 | 118 | /** 119 | * Return a list of folders at the given path. Does not include hidden folders. 120 | * @param {string} folderName - Path to local folder 121 | * @return {array} A list of folder names 122 | * @alias module:file 123 | */ 124 | function listFolders( folderName ) 125 | { 126 | try { 127 | const dirList = fs.readdirSync( folderName, { withFileTypes: true } ) 128 | return _filterFolder( dirList, 2 ) 129 | } 130 | catch (error) { 131 | return null 132 | } 133 | } 134 | 135 | /** 136 | * Return a list of files at the given path. Does not include hidden files. 137 | * @param {string} folderName - Path to local folder 138 | * @return {array} A list of files names 139 | * @alias module:file 140 | */ 141 | function listFiles( folderName ) 142 | { 143 | try { 144 | const dirList = fs.readdirSync( folderName, { withFileTypes: true } ) 145 | return _filterFolder( dirList, 1 ) 146 | } 147 | catch (error) { 148 | return null 149 | } 150 | } 151 | 152 | /** 153 | * Delete the folder at the given path. 154 | * @param {string} folderName - Path to local folder 155 | * @return {boolean} Returns true if the folder was successfully deleted 156 | * @alias module:file 157 | */ 158 | function deleteFolder( folderName ) 159 | { 160 | try { 161 | fs.rmdirSync( folderName ) 162 | return !isFolder( folderName ) 163 | } 164 | catch (error) { 165 | return false 166 | } 167 | } 168 | 169 | /** 170 | * Deletes the local file. 171 | * @param {string} fileName - Path to local file 172 | * @return {boolean} True if the file exists and was deleted. 173 | * @alias module:file 174 | */ 175 | function deleteFile( fileName ) 176 | { 177 | try { 178 | // unlinkSync returns nothing, even in error case 179 | if ( !isFile(fileName) ) return false 180 | fs.unlinkSync( fileName ) 181 | return !isFile(fileName) 182 | } 183 | catch (error) { 184 | console.error( "deleteFile error on ", fileName, ": ", error ) 185 | return false 186 | } 187 | } 188 | 189 | /** 190 | * Reads the local JSON file and returns its object representation. 191 | * @param {string} fileName - Path to local file 192 | * @return {object} Contents of the local file parsed as an object 193 | * @alias module:file 194 | */ 195 | function readJSON( fileName ) 196 | { 197 | var fileContent = readFile( fileName ) 198 | if ( !fileContent ) return null 199 | return applib.parseJSON( fileContent ) 200 | } 201 | 202 | /** 203 | * Writes a serializable object as JSON to a local file. 204 | * @param {string} fileName - Path to local file 205 | * @param {object} fileContent - Content to write as JSON 206 | * @alias module:file 207 | */ 208 | function writeJSON( fileName, fileContent ) 209 | { 210 | writeFile( fileName, applib.stringifyJSON( fileContent ) ) 211 | } 212 | 213 | /** 214 | * Given an object, reads a local JSON file and merges the object with file contents, writing back the merged object as JSON. 215 | * @param {string} fileName - Path to local file 216 | * @param {object} updateObject - A serializable object to be merged with the JSON file 217 | * @alias module:file 218 | */ 219 | function updateJSON( fileName, updateObject ) 220 | { 221 | const jCurrent = readJSON( fileName ) 222 | const jUpdate = applib.mergeObjects( jCurrent, updateObject ) 223 | writeFile( fileName, applib.stringifyJSON( jUpdate ) ) 224 | } 225 | 226 | /** 227 | * Checks whether the command exists, i.e. can be run with an exec() statement. 228 | * @param {string} commandString - A shell comment to be tested 229 | * @return {boolean} True if the command exists 230 | * @alias module:file 231 | */ 232 | function checkCommand( commandString ) 233 | { 234 | var exists 235 | try { 236 | execSync( `which ${commandString}`, { stdio: 'ignore' } ) 237 | exists = true 238 | } 239 | catch (error) { 240 | exists = false 241 | } 242 | return exists 243 | } 244 | 245 | module.exports = { 246 | readFile: readFile, 247 | writeFile: writeFile, 248 | deleteFile: deleteFile, 249 | isFile: isFile, 250 | isFolder: isFolder, 251 | makeFolder: makeFolder, 252 | listFolders: listFolders, 253 | listFiles: listFiles, 254 | deleteFolder: deleteFolder, 255 | readJSON: readJSON, 256 | writeJSON: writeJSON, 257 | updateJSON: updateJSON, 258 | checkCommand: checkCommand 259 | } 260 | -------------------------------------------------------------------------------- /lib/firestore.js: -------------------------------------------------------------------------------- 1 | /* firestore.js 2 | * Copyright (c) 2019-2020 by David Asher, https://github.com/david-asher 3 | */ 4 | 'use strict'; 5 | 6 | /** 7 | * Interface to Google Cloud Firestore Database using high-level interface objects. 8 | * All Firestore document I/O is performed in the security context of the 9 | * logged-in user and the specific app you have built. 10 | * 11 | * It is important to understand the structure of a Firestore because it is not a file tree. A single 12 | * document may contain a set of properties, but not another document. A document may also contain 13 | * collections. A collection is a set of documents, but not properties. Therefore a document is always 14 | * a member of a collection, and a collection is a member of a document. You can describe a specific 15 | * path to a document, and it must always be an even number of path components since the document 16 | * parent will be a collection, except for the root document of the Firestore. If you follow only this 17 | * interface for access to the Firestore, you will not have direct access to the root document. 18 | * @see {@link https://firebase.google.com/docs/firestore/manage-data/structure-data} 19 | * @see {@link https://firebase.google.com/docs/firestore/data-model} 20 | * 21 | * Once a firestore object is defined, all document I/O is performed relative to (constrained to) 22 | * this top-level document, so your code can't wander astray into other parts of the Firestore 23 | * where you don't belong. Each API starts with a docPath parameter, and if null will refer to the 24 | * top-level doc, into which you can read and write fields. If you want to create or work with documents, 25 | * the docPath parameter must have an even number of path segments, e.g. "/maps/chicago" in which case 26 | * the collection "maps" will be automatically created if it doesn't exist. 27 | * 28 | * After initialization three objects are available from this module: 29 | * * .doc - A Firestore subtree (/users//) for the signed-in user's documents in Firestore 30 | * * .app - A Firestore subtree (/apps//) for the app being used, accessible to all users 31 | * * .public - A Firestore subtree (/apps/public/) that any user or app and read or write to 32 | * @module firestore 33 | */ 34 | 35 | const authn = require( './authentication' ) 36 | 37 | // firestoreDoc root collections, for object persistence and internal reference 38 | var fireSet = {} 39 | 40 | var fromServer = { source: "default" } 41 | 42 | class firestoreDoc 43 | { 44 | /** 45 | * Create a top-level Firestore db/collection/doc/, into which you can segment your Firestore. 46 | * @param {string} rootCollectionName - Top level segmentation of your Firestore, e.g. "users" 47 | * @param {string} topLevelDocument - A specific name (i.e. constraint) for this document tree, e.g. userId 48 | * @returns {null} 49 | */ 50 | constructor( rootCollection, topLevelDocument, scopeName ) 51 | { 52 | this.name = scopeName, 53 | this.rootName = rootCollection 54 | this.topDocName = topLevelDocument 55 | this.root = authn.firestore().collection( this.rootName ) 56 | this.topdoc = this.root.doc( this.topDocName ) 57 | } 58 | 59 | _ref( docPath ) 60 | { 61 | var docRef = null 62 | try { 63 | // docPath is assumed to be relative to this.topdoc. If it is blank or null it refers to the 64 | // top-level document. If it has an odd number of path segments, _ref() creates a new document 65 | // with an automatically-generated unique ID. 66 | const cleanPath = ( docPath || "" ).replace( "//", "/" ).replace( "\\", "/" ) 67 | const parts = ( cleanPath ).split( '/' ) 68 | if ( parts.length === 0 ) { 69 | return this.topdoc 70 | } 71 | // check for an even number of path segments, we have a named document 72 | const nParts = parts.length 73 | const docName = parts.pop() 74 | var collectionName = parts.join( '/' ) 75 | if ( !collectionName || collectionName.length == 0 ) return docRef 76 | if ( 0 !== ( nParts % 2 ) ) return docRef 77 | // get the collection, then the doc if there's an even number of path parts 78 | const collectionRef = this.topdoc.collection( collectionName ) 79 | docRef = collectionRef.doc( docName ) 80 | } 81 | catch (error) { 82 | console.error( "firestore.js _ref: ", docPath, error ) 83 | } 84 | return docRef 85 | } 86 | 87 | /** 88 | * Gets a DocumentSnapshot for the Firestore document which contains meta information and functions 89 | * to get data, test existence, etc. 90 | * @param {string} docPath - Relative path to a Firebase document within the root collection 91 | * @returns {Promise} An object which can be used to get further information and data 92 | * about the document: .exists, .id, .metadata, .get(), .data(), .isEqual() 93 | * @see {@link https://firebase.google.com/docs/reference/node/firebase.firestore.DocumentSnapshot|DocumentSnapshot} 94 | */ 95 | async about( docPath ) 96 | { 97 | // returns a promise containing a DocumentSnapshot, 98 | // which contains properties: exists, id, metadata; and methods get(), data(), isEqual() 99 | return await this._ref( docPath ).get( fromServer ) 100 | } 101 | 102 | /** 103 | * Reads the Firestore document at the requested path and returns an object representing the content. 104 | * @param {string} docPath - Path to a Firebase document 105 | * @returns {Promise} The contents of the requested document 106 | */ 107 | async read( docPath ) 108 | { 109 | var result = null 110 | try { 111 | const readResult = await this.about( docPath ) 112 | if ( readResult && readResult.exists ) result = await readResult.data() 113 | } 114 | catch (error) { 115 | throw( error ) 116 | // let it go 117 | } 118 | return result 119 | } 120 | 121 | /** 122 | * Creates a new document in the Firestore at the requested path, else updates an existing document 123 | * if it already exists, overwriting all fields. 124 | * @param {string} docPath - Path to a Firebase document 125 | * @param {object} [contents] - Content to write into new document, or merge into existing document 126 | * @returns {Promise} DocumentReference for the docPath 127 | * @see {@link https://firebase.google.com/docs/reference/node/firebase.firestore.DocumentSnapshot} 128 | */ 129 | async write( docPath, contents = {} ) 130 | { 131 | await this._ref( docPath ).set( contents, { merge: false } ) 132 | return await this.about( docPath ) 133 | } 134 | 135 | /** 136 | * Creates a new document in the Firestore at the requested path, else updates an existing document 137 | * if it already exists, merging all fields. 138 | * @param {string} docPath - Path to a Firebase document 139 | * @param {object} [contents] - Content to write into new document, or merge into existing document 140 | * @returns {Promise} DocumentReference for the docPath 141 | * @see {@link https://firebase.google.com/docs/reference/node/firebase.firestore.DocumentSnapshot} 142 | */ 143 | async merge( docPath, contents = {} ) 144 | { 145 | await this._ref( docPath ).set( contents, { merge: true } ) 146 | return await this.about( docPath ) 147 | } 148 | 149 | /** 150 | * Updates an existing document in the Firestore at the requested path with the given contents. Like 151 | * merge() except it will fail if the document does not exist. 152 | * @param {string} docPath - Path to a Firebase document 153 | * @param {object} [contents] - Content to write into new document, or merge into existing document 154 | * @returns {Promise} DocumentReference for the docPath 155 | * @see {@link https://firebase.google.com/docs/reference/node/firebase.firestore.DocumentSnapshot} 156 | */ 157 | async update( docPath, contents = {} ) 158 | { 159 | /* 160 | * NOTE: DocumentReference has an update() method that fails if the document doesn't exist, 161 | * however in the failure case it simply prints an error message and neither throws nor returns 162 | * an error, so it's undetectable. So instead we'll check for document existence, and if the doc 163 | * doesn't exist return Promise else Promise 164 | */ 165 | var result = null 166 | result = await this.about( docPath ) 167 | if ( result && result.exists ) await this.merge( docPath, contents ) 168 | return result 169 | } 170 | 171 | /** 172 | * Deletes the Firestore document at the given path. 173 | * @param {string} docPath - Path to a Firebase document 174 | * @returns {Promise} Returns a promise that resolves once the document is deleted 175 | * @see {@link https://firebase.google.com/docs/reference/node/firebase.firestore.DocumentReference#delete|DocumentReference delete()} 176 | */ 177 | async delete( docPath ) 178 | { 179 | return await this._ref( docPath ).delete() 180 | } 181 | 182 | /** 183 | * Queries a collection to find a match for a specific field name with optional matching operator. 184 | * @param {string} collectionPath - The path to a collection, cannot be blank 185 | * @param {string} fieldName - The name of a document field to search against all of the collection documents 186 | * @param {string} fieldMatch - The value of the fieldName to match against 187 | * @param {string} [matchOperator] - Optional comparison operator, defaults to "==" 188 | * @returns {Promise} 189 | * @see {@link https://firebase.google.com/docs/reference/node/firebase.firestore.Query#where|Query where()} 190 | * @see {@link https://firebase.google.com/docs/reference/node/firebase.firestore.QuerySnapshot|QuerySnapshot} 191 | */ 192 | async query( collectionPath, fieldName, fieldMatch, matchOperator = "==" ) 193 | { 194 | const collectionRef = this.topdoc.collection( collectionPath ) 195 | const resultQuery = await collectionRef.where( fieldName, matchOperator, fieldMatch ) 196 | // note, the .get() call can return almost immediately or take up to 7-8 seconds. Yuk. 197 | // There seems to be no good explanation on Stackoverflow or elsewhere. 198 | return await resultQuery.get( { source: "default" } ) 199 | } 200 | 201 | /** 202 | * Gets the value of a specified field within a Firestore document. 203 | * @param {string} docPath - Path to a Firebase document 204 | * @param {string} fieldName - The name of a top-level field within the Firebase document 205 | * @returns {Promise} The data at the specified field location or undefined if no such field exists in the document 206 | * @see {@link https://firebase.google.com/docs/reference/node/firebase.firestore.DocumentSnapshot#get|DocumentSnapshot get()} 207 | */ 208 | async field( docPath, fieldName ) 209 | { 210 | const docRef = this._ref( docPath ) 211 | const snap = await docRef.get( fromServer ) 212 | return await snap.get( fieldName ) 213 | } 214 | 215 | /** 216 | * This function will insert a new value, or multiple values, onto an array field of the 217 | * Firestore document. Each specified element that doesn't already exist in the array will 218 | * be added to the end. If the field being modified is not already an array it will be 219 | * overwritten with an array containing exactly the specified elements. 220 | * @param {string} docPath - Path to a Firebase document 221 | * @param {string} arrayName - The name of the array field to be updated 222 | * @param {*} newValue - The new value to push on the array field 223 | * @returns {Promise} - The array after the new value is inserted 224 | * @see {@link https://firebase.google.com/docs/reference/node/firebase.firestore.FieldValue#static-arrayunion|FieldValue union} 225 | */ 226 | async union( docPath, arrayName, newValue ) 227 | { 228 | const updateElement = {} 229 | updateElement[ arrayName ] = authn.FieldValue.arrayUnion( newValue ) 230 | await this._ref( docPath ).update( updateElement ) 231 | return await this.field( docPath, arrayName ) 232 | } 233 | 234 | /** 235 | * This function will remove a value, or multiple values, from an array field of the 236 | * Firestore document. 237 | * @param {string} docPath - Path to a Firebase document 238 | * @param {string} arrayName - The name of the array field to be updated 239 | * @param {*} newValue - The new value to push on the array field 240 | * @returns {Promise} - The array after the value is removed 241 | * @see {@link https://firebase.google.com/docs/reference/node/firebase.firestore.FieldValue#static-arrayremove|FieldValue remove} 242 | */ 243 | async splice( docPath, arrayName, oldValue ) 244 | { 245 | const updateElement = {} 246 | updateElement[ arrayName ] = authn.FieldValue.arrayRemove( oldValue ) 247 | await this._ref( docPath ).update( updateElement ) 248 | return await this.field( docPath, arrayName ) 249 | } 250 | 251 | /** 252 | * This function will push a new value onto the end of an array field of the Firestore document. 253 | * @param {string} docPath - Path to a Firebase document 254 | * @param {string} arrayName - The name of the array field to be updated 255 | * @param {*} newValue - The new value to push on the array field 256 | * @returns {Promise} The updated array field 257 | */ 258 | async push( docPath, arrayName, newValue ) 259 | { 260 | const docRef = this._ref( docPath ) 261 | const snap = await docRef.get( fromServer ) 262 | var baseDoc = snap.data() 263 | var arrayRef = baseDoc[ arrayName ] 264 | if ( !arrayRef ) arrayRef = baseDoc[ arrayName ] = [] 265 | arrayRef.push( newValue ) 266 | await docRef.set( baseDoc, { merge: true } ) 267 | return arrayRef 268 | } 269 | 270 | /** 271 | * This function will pop a value from the end of an array field of the Firestore document. 272 | * @param {string} docPath - Path to a Firebase document 273 | * @param {string} arrayName - The name of the array field to be updated 274 | * @returns {Promise} The popped value 275 | */ 276 | async pop( docPath, arrayName ) 277 | { 278 | var popped = null 279 | const docRef = this._ref( docPath ) 280 | const snap = await docRef.get( fromServer ) 281 | var baseDoc = snap.data() 282 | var arrayRef = baseDoc[ arrayName ] 283 | if ( arrayRef ) popped = arrayRef.pop() 284 | await docRef.set( baseDoc, { merge: true } ) 285 | return popped 286 | } 287 | } 288 | 289 | /** 290 | * Firestore interfaces are defined when your app starts: 291 | * * .doc - A Firestore subtree (/users/userid/) for the signed-in user's documents in Firestore 292 | * * .app - A Firestore subtree (/apps/projectId/) for the app being used, accessible to all users 293 | * * .public - A Firestore subtree (/apps/public/) that any user or app and read or write to 294 | * 295 | * @param {string} userid - The Firebase assigned userId from authentication process 296 | * @param {string} projectId - Unique string for this application, typically the Firebase projectId 297 | * @alias module:firestore 298 | */ 299 | function initialize( userid, projectId ) 300 | { 301 | fireSet.doc = new firestoreDoc( "users", userid, "doc" ) 302 | fireSet.app = new firestoreDoc( "apps", projectId, "app" ) 303 | fireSet.public = new firestoreDoc( "apps", "public", "public" ) 304 | Object.assign( module.exports, fireSet ) 305 | } 306 | 307 | module.exports = { 308 | initialize: initialize 309 | } 310 | -------------------------------------------------------------------------------- /lib/localstorage.js: -------------------------------------------------------------------------------- 1 | /* localstorage.js 2 | * Copyright (c) 2019 by David Asher, https://github.com/david-asher 3 | */ 4 | 'use strict'; 5 | 6 | /** 7 | * Functions that use the localStorage capability in a BrowserWindow to store persistent information. These APIs 8 | * run in the main node.js process and use IPC to request and transfer information from the browser. This 9 | * feature is used in conjunction with the weblocal.js file if referenced by a BrowserWindow. weblocal.js should 10 | * not be loaded into more than one BrowserWindow. This API is intended to mimic the localStorage API available in 11 | * every Browser, except the getItem() call must be asynchronous and replies with either a callback or a promise. 12 | * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage|localStorage} 13 | * @module local 14 | */ 15 | 16 | /* 17 | * Pub-style messaging from the main process to the browser window. 18 | * @param {string} topic - A topic that you expect the browser to be subscribed to 19 | * @param {string} key - A storage or command key 20 | * @param {string|object} value - The content of the message 21 | */ 22 | function _send( topic, key, value ) 23 | { 24 | try { 25 | // accessing the .id property is a way to check if the window exists so we don't hang or crash 26 | const bad = global.mainWindow.id 27 | global.mainWindow.webContents.send( "localStorage", topic, key, value ) 28 | return { topic: topic, key: key, value: value } 29 | } 30 | catch (error) { 31 | console.error( "webContents.send error on " + key + ", " + value ) 32 | return { error: error } 33 | } 34 | } 35 | 36 | /* 37 | * Sub-style messaging from the browser window to the main process. 38 | * @param {string} topic - A topic to receive a message from 39 | */ 40 | async function _receive( topic ) 41 | { 42 | return new Promise( (resolve,reject) => { 43 | if ( !global.mainIPC ) return reject(0) 44 | global.mainIPC.once( topic, ( event, value ) => { 45 | try { 46 | value = JSON.parse( value ) 47 | } 48 | catch (error) { 49 | // leave the original value 50 | } 51 | resolve( value ) 52 | }) 53 | }) 54 | } 55 | 56 | /** 57 | * When passed a key name and value, will add that key to the Storage object, or update that 58 | * key's value if it already exists. This function will not confirm that the key and value 59 | * were written to the BrowserWindow localStorage. 60 | * @param {*} key - The name of the key you want to create/update 61 | * @param {*} value - The value you want to give the key you are creating/updating 62 | * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem|localStorage setItem()} 63 | * @alias module:local 64 | */ 65 | function setItem( key, value ) 66 | { 67 | if ( value && typeof value == 'object' ) { 68 | try { 69 | value = JSON.stringify( value ) 70 | } 71 | catch (error) { 72 | console.error( "setItem: ", key, value, error ) 73 | } 74 | } 75 | _send( "setItem", key, value ) 76 | } 77 | 78 | /** 79 | * When passed a key name, will remove that key from the Storage object if it exists. If there 80 | * is no item associated with the given key, this function will do nothing. 81 | * @param {string} key - The name of the key you want to remove 82 | * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Storage/removeItem|localStorage removeItem()} 83 | * @alias module:local 84 | */ 85 | function removeItem( key ) 86 | { 87 | _send( "removeItem", key, null ) 88 | } 89 | 90 | /** 91 | * When passed a key name, will return that key's value, or null if the key does not exist. 92 | * @param {string} key - The name of the key you want to retrieve the value of 93 | * @param {*} [optionalCallback] - Optional callback function to retreive the 94 | * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Storage/getItem|localStorage getItem()} 95 | * @returns {Promise} A promise which resolves to containing the value of the key. If the key does not exist, null is returned. 96 | * @alias module:local 97 | */ 98 | function getItem( key, optionalCallback ) 99 | { 100 | var failTimer 101 | return new Promise( (resolve,reject) => { 102 | // we can't get from localStorage if there is no IPC 103 | if ( !global.mainIPC ) { 104 | if ( optionalCallback ) optionalCallback( null ) 105 | resolve( null ) 106 | return 107 | 108 | } 109 | _receive( "localStorage:" + key ) 110 | .then( (value) => { 111 | if ( failTimer ) clearTimeout( failTimer ) 112 | failTimer = null 113 | if ( optionalCallback ) optionalCallback( value ) 114 | resolve( value ) 115 | }) 116 | .catch( (error) => { 117 | if ( optionalCallback ) optionalCallback( null ) 118 | reject( error ) 119 | }) 120 | failTimer = setTimeout( () => { 121 | failTimer = null 122 | console.error( "Timeout on getItem for " + key ) 123 | resolve( null ) 124 | }, 4000 ) 125 | _send( "getItem", key, null ) 126 | }) 127 | } 128 | 129 | module.exports = { 130 | setItem: setItem, 131 | removeItem: removeItem, 132 | getItem: getItem 133 | } 134 | -------------------------------------------------------------------------------- /lib/webserver.js: -------------------------------------------------------------------------------- 1 | /* webserver.js 2 | * Copyright (c) 2019-2021 by David Asher, https://github.com/david-asher 3 | */ 4 | 'use strict'; 5 | 6 | /** 7 | * This module sets up a local webserver which is primarily used for secure communication with 8 | * a BrowserWindow. Although it is possible to use IPC for this purpose, that would require enabling 9 | * the nodeIntegration option for the window, which would expose the app to all manner of mischief. 10 | * The webserver is an instance of express, configured for HTTPS with a self-signed cert. 11 | * @module server 12 | */ 13 | 14 | const https = require('https') 15 | const express = require('express') 16 | const helmet = require('helmet') 17 | 18 | var appserver 19 | var webtls 20 | var certFingerprint 21 | 22 | /* 23 | * This is a hook into an HTTP received request. Since we are using localhost and a self-signed 24 | * certificate, we want to reject the error that this would normally generate, but only on the 25 | * condition that it happens with our self-signed cert. We do this check by examining the 26 | * cert fingerprint. 27 | * @see {@link https://electronjs.org/docs/api/app#event-certificate-error|app event: certificate-error} 28 | */ 29 | function _checkCertificate( event, webContents, url, error, certificate, callback ) 30 | { 31 | // disable the certificate signing error only if this is our certificate 32 | var isOurCert = false 33 | if ( certificate.fingerprint == certFingerprint ) { 34 | event.preventDefault() 35 | isOurCert = true 36 | } 37 | callback( isOurCert ) 38 | } 39 | 40 | /* 41 | * Another security check. This function is in the express routing chain for /api/... calls. 42 | * It will check that /api calls cannot come from an external computer. 43 | */ 44 | function _checkOurApp( req, res, next ) 45 | { 46 | const referer = ( req.headers || {} ).host || "" 47 | if ( !req.secure || !referer.match( global.appConfig.webapp.hostPort ) ) { 48 | res.status( 401 ).send( "Unauthorized" ) 49 | return 50 | } 51 | next() 52 | } 53 | 54 | /* 55 | * The WebOptions are required to specify the certificate for TLS. 56 | * A new self-signed certificate is generated every time the app starts. 57 | * The cert is used to apply TLS to the webserver that runs in the main app, 58 | * and enables a secure connection from a browser window without having to 59 | * enable IPC for the browser. 60 | */ 61 | function _getWebOptions() 62 | { 63 | const selfsigned = require('selfsigned') 64 | const forge = require('node-forge') 65 | 66 | const attrs = [ 67 | { name: "commonName", value: global.appContext.name }, 68 | { name: "countryName", value: global.appContext.countryCode }, 69 | { name: "organizationName", value: "Self-Signed" } 70 | ] 71 | const pems = selfsigned.generate(attrs, { days: 365 }) 72 | const originalCert = forge.pki.certificateFromPem(pems.cert) 73 | const asn1Cert = forge.pki.certificateToAsn1(originalCert) 74 | const asn1Encoded = forge.asn1.toDer(asn1Cert).getBytes() 75 | const fingerprintDigest = forge.md.sha256.create().update(asn1Encoded).digest() 76 | certFingerprint = "sha256/" + forge.util.encode64(fingerprintDigest.getBytes()) 77 | 78 | return { 79 | key: pems.private, 80 | cert: pems.cert, 81 | requestCert: false, 82 | rejectUnauthorized: false 83 | } 84 | } 85 | 86 | /* 87 | * This function will start the HTTPS local webserver and configure static document serving. 88 | * @param {app} mainApp - The Electron main app 89 | * @param {array} staticFolders - A list of folder names to be configured for static document serving 90 | * @returns {Promise} Returns a reference to the express middleware that can be used to create API routes 91 | * @see {@link https://electronjs.org/docs/api/app#app|Electron app} 92 | * @see {@link https://expressjs.com/|expressjs} 93 | * @alias module:server 94 | */ 95 | function logRequest( req, options, callback ) 96 | { 97 | var reqReturn 98 | var responseHandler 99 | function logResponse( response ) 100 | { 101 | console.log( `RESPONSE (${Date.now()}): `, response.statusCode, response.statusMessage, response.headers ) 102 | if ( responseHandler ) responseHandler.apply( this, arguments ) 103 | } 104 | console.log( `REQUEST (${Date.now()}): `, req ) 105 | // call the original 'request' function 106 | if ( typeof options == 'function' ) { 107 | responseHandler = options 108 | reqReturn = logRequest.originalHttpsRequest( req, logResponse ) 109 | } 110 | else { 111 | responseHandler = callback 112 | reqReturn = logRequest.originalHttpsRequest( req, options, logResponse ) 113 | } 114 | return reqReturn 115 | } 116 | 117 | /** 118 | * Start the HTTPS server for the Main node.js process. 119 | * @param {object} mainApp - Reference to the Electron app 120 | * @param {array} staticFolders - List of folders that will be exposed from the webserver as static content 121 | * @alias module:server 122 | */ 123 | function start( mainApp, staticFolders ) 124 | { 125 | // axios doesn't have a debug facility the way that request has, so put in 126 | // request and response hooks so we can log the network traffic 127 | if ( global.appConfig.debugMode ) { 128 | logRequest.originalHttpsRequest = https.request 129 | https.request = logRequest 130 | } 131 | 132 | return new Promise( ( resolve, reject ) => 133 | { 134 | try { 135 | appserver = express() 136 | 137 | // support json encoded bodies 138 | appserver.use(express.json()); 139 | appserver.use(express.urlencoded({ extended: true })); 140 | 141 | // In order to use a self signed certificate without throwing an error but still have security, 142 | // we check to make sure that only our certificate can be used; any other self-signed cert 143 | // shouldn't happen in our app, and will throw an error 144 | mainApp.on( 'certificate-error', _checkCertificate ) 145 | 146 | // security checks 147 | appserver.use( '/api', _checkOurApp ) 148 | appserver.use( helmet() ) 149 | 150 | // set ContentSecurityPolicy header for all local web pages 151 | appserver.use((req, res, next) => { 152 | res.set( 'Content-Security-Policy', global.ContentSecurityString ) 153 | next(); 154 | }); 155 | 156 | // set up static web content folders 157 | const folderOptions = { 158 | index: false, 159 | maxAge: '1d', 160 | redirect: false, 161 | } 162 | if ( 'string' == typeof (staticFolders) ) staticFolders = staticFolders.split( /,|;/ ) 163 | staticFolders.forEach( (folder) => { 164 | const slashFolder = '/' + folder.replace(/^\/+/, '') 165 | appserver.use( slashFolder, express.static( process.env.INIT_CWD + slashFolder, folderOptions ) ) 166 | }) 167 | 168 | // start the secure web server 169 | webtls = https.createServer( _getWebOptions(), appserver ) 170 | webtls.listen( global.appConfig.webapp.port, () => { 171 | // console.log( "TLS server on port " + webtls.address().port ) 172 | resolve( appserver ) 173 | }) 174 | } 175 | catch (error) { 176 | console.error( "webserver.start: ", error ) 177 | } 178 | }) 179 | } 180 | 181 | module.exports = { 182 | start: start 183 | } 184 | 185 | -------------------------------------------------------------------------------- /lib/windows.js: -------------------------------------------------------------------------------- 1 | /* windows.js 2 | * Copyright (c) 2019-2021 by David Asher, https://github.com/david-asher 3 | */ 4 | 'use strict'; 5 | 6 | /** 7 | * This module will open and manage Electron BrowserWindow instances, and make sure that they 8 | * all close when the app closes. 9 | * @module fbwindow 10 | */ 11 | 12 | const url = require('url') 13 | const { ipcMain, BrowserWindow } = require('electron') 14 | const applib = require( './applibrary' ) 15 | const local = require( './localstorage' ) 16 | 17 | /** 18 | * Opens a BrowserWindow. 19 | * @see {@link https://electronjs.org/docs/api/browser-window|Electron BrowserWindow} 20 | * @alias module:fbwindow 21 | */ 22 | class open extends BrowserWindow 23 | { 24 | /** 25 | * Create a window.open object. The window will automatically track window changes in size and 26 | * position and keep the bounds changes in localStorage. 27 | * @param {string} urlToOpen - Opens the window and loads this page 28 | * @param {options} [setOptions] - A set of options for changing the window properties 29 | * @returns {Promise} An WindowObject inhereted from BrowserWindow 30 | * @see {@link https://electronjs.org/docs/api/browser-window|BrowserWindow options} 31 | */ 32 | constructor( urlToOpen, setOptions, urlOptions ) 33 | { 34 | var urlParts 35 | try { 36 | urlParts = new URL( urlToOpen ) 37 | } 38 | catch (error) { 39 | console.error( "ERROR on open: ", urlToOpen, error ) 40 | return null 41 | } 42 | 43 | // nodeIntegration: open a safe window that can't connect to the Main process if URL is not localhost 44 | const isLocalHost = ( "localhost" == urlParts.hostname ) 45 | const baseOptions = { 46 | show: false, 47 | resizable: true, 48 | movable: true, 49 | webPreferences: { 50 | nodeIntegration: isLocalHost, 51 | enableRemoteModule: isLocalHost, 52 | contextIsolation: !isLocalHost 53 | } 54 | } 55 | const openOptions = applib.mergeObjects( baseOptions, setOptions || {} ); 56 | 57 | // open the BrowserWindow 58 | super( openOptions ) 59 | 60 | // setup the chrome debug tools as requested in the app config 61 | if ( global.appConfig.debugMode ) super.webContents.openDevTools() 62 | 63 | // automatic persistence of window move and resize events 64 | this.boundsCheckerEnabled = openOptions.resizable || openOptions.movable 65 | if ( this.boundsCheckerEnabled ) this._setupBoundsChecker( openOptions.title ) 66 | 67 | if ( !urlOptions ) urlOptions = {} 68 | if ( global.userAgent && !urlOptions.userAgent ) urlOptions.userAgent = global.userAgent 69 | 70 | // now that the window is configured, open it with the URL 71 | super.loadURL( urlToOpen, urlOptions ) 72 | } 73 | 74 | /** 75 | * Why is this function here? If you create a new window.open object and pass that 76 | * to dialog.showMessageBox() for a modal doalog, it won't render the dialog content 77 | * (i.e. it's a blank dialog). Even when you capture the constructor super(), the call 78 | * to showMessageBox() still comes up blank. This method returns an actual 79 | * BrowserWindow object that is satisfactory for building a modal dialog. 80 | */ 81 | window() 82 | { 83 | return BrowserWindow.fromId( super.id ) 84 | } 85 | 86 | /** 87 | * If you open the window with option show:false then call window.show(), use this function 88 | * to get a Promise that returns after the window is visible. 89 | * @returns {Promise} 90 | */ 91 | waitForShow() 92 | { 93 | return new Promise( ( resolve, reject ) => 94 | { 95 | // super.id is a test to make sure the window still exists 96 | try { 97 | const bad = super.id 98 | } 99 | catch (error) { 100 | return resolve( true ) 101 | } 102 | if ( super.isVisible() ) { 103 | return resolve( true ) 104 | } 105 | super.once( 'ready-to-show', () => { 106 | return resolve( true ) 107 | }) 108 | }) 109 | } 110 | 111 | /** 112 | * If you close the window with window.close(), use this function 113 | * to get a Promise that returns after the window is destroyed. 114 | * @returns {Promise} 115 | */ 116 | waitForClose() 117 | { 118 | return new Promise( ( resolve, reject ) => 119 | { 120 | // super.id is a test to make sure the window still exists 121 | try { 122 | const bad = super.id 123 | } 124 | catch (error) { 125 | return resolve( true ) 126 | } 127 | if ( super.isDestroyed() ) { 128 | return resolve( true ) 129 | } 130 | super.once( 'closed', () => { 131 | return resolve( true ) 132 | }) 133 | }) 134 | } 135 | 136 | _updateBounds() 137 | { 138 | var current = Date.now() 139 | if ( this.boundsTimer ) clearTimeout( this.boundsTimer ) 140 | if ( this.lastMove == null || current - this.lastMove < 500 ) { 141 | this.boundsTimer = setTimeout( this._updateBounds, 500 ) 142 | this.lastMove = current 143 | return 144 | } 145 | var bounds = super.getBounds() 146 | local.setItem( this.boundsKey, bounds ) 147 | this.lastMove = null 148 | } 149 | 150 | /** 151 | * Recalls the last saved position and shape of the window, particularly useful for the first showing of the window. 152 | * @returns {object} Returns the previous bounds object that the window will now be set to 153 | */ 154 | resize( callback ) 155 | { 156 | var updatedBounds = null 157 | if ( !this.boundsCheckerEnabled ) { 158 | if ( callback ) callback( null ) 159 | return 160 | } 161 | local.getItem( this.boundsKey ) 162 | .then( ( bounds ) => { 163 | updatedBounds = bounds 164 | if ( bounds ) { 165 | super.setBounds( updatedBounds ) 166 | } 167 | if ( callback ) callback( updatedBounds ) 168 | }) 169 | .catch( (error) => { 170 | if ( callback ) callback( null ) 171 | }) 172 | } 173 | 174 | _setupBoundsChecker( title ) 175 | { 176 | this.boundsTimer = null 177 | this.lastMove = null 178 | this._updateBounds = this._updateBounds.bind( this ) 179 | this.boundsKey = title ? "bounds:" + title.replace( /\W/g, "" ) : null 180 | super.on( 'resize', this._updateBounds ) 181 | super.on( 'move', this._updateBounds ) 182 | this.boundsCheckerEnabled = true 183 | } 184 | 185 | /** 186 | * Sends a message - a payload on a specific channel - to the BrowserWindow 187 | * @param {string} channel - A topic which the BrowserWindow should be expecting 188 | * @param {string|number|object|array} payload - The message content to send on the topic 189 | * @see {@link https://electronjs.org/docs/api/web-contents#contentssendchannel-arg1-arg2-|BrowserWindow.webContents.send()} 190 | */ 191 | send( channel, payload ) 192 | { 193 | if ( !super.webContents ) return 194 | super.webContents.send( channel, payload ) 195 | } 196 | 197 | /** 198 | * Receives a message event from the BrowserWindow, with optional callback or Promise interface. The callback 199 | * or Promise will fire whenever a message event is received on the channel. 200 | * @param {string} channel - A topic which the BrowserWindow should be expecting 201 | * @param {function} [callback] - Optional callback function to receive the message event 202 | * @returns {Promise} If no callback is supplied then a Promise is returned 203 | * @see {@link https://electronjs.org/docs/api/web-contents#contentssendchannel-arg1-arg2-|BrowserWindow.webContents.send()} 204 | */ 205 | receive( channel, callback ) 206 | { 207 | if ( callback ) { 208 | ipcMain.on( channel, (event,...args) => { 209 | callback.apply( this, args ) 210 | }) 211 | return 212 | } 213 | return new Promise( (resolve,reject) => { 214 | ipcMain.on( channel, (event,...args) => { 215 | resolve.apply( this, args ) 216 | }) 217 | }) 218 | } 219 | 220 | /** 221 | * Close the window. 222 | */ 223 | close() 224 | { 225 | if ( this.boundsTimer ) clearTimeout( this.boundsTimer ) 226 | super.off( 'resize', this._updateBounds ) 227 | super.off( 'move', this._updateBounds ) 228 | super.close() 229 | } 230 | } 231 | 232 | /** 233 | * Similar to window.open() except a modal window is created as a child to the parentWindow. 234 | * @param {string} urlToOpen - Opens the window and loads this page 235 | * @param {BrowserWindow} parentWindow - A BrowserWindow which will contain the model window 236 | * @param {options} setOptions - A set of options for changing the window properties 237 | * @returns {Promise} An WindowObject inhereted from BrowserWindow 238 | * @see {@link https://electronjs.org/docs/api/browser-window|BrowserWindow options} 239 | */ 240 | function openModal( urlToOpen, parentWindow, setOptions, urlOptions ) 241 | { 242 | const baseOptions = { 243 | parent: parentWindow, 244 | modal: true, 245 | show: true, 246 | alwaysOnTop: true, 247 | frame: false, 248 | } 249 | const openOptions = applib.mergeObjects( baseOptions, setOptions || {} ); 250 | 251 | // this may look strange to call a new window "open", but the method is 252 | // usually called from the outside using this library, e.g. require('windows.js').open 253 | return new open( urlToOpen, openOptions, urlOptions ) 254 | } 255 | 256 | module.exports = { 257 | open: open, 258 | openModal: openModal 259 | } 260 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /* main.js 2 | * electron-firebase 3 | * This is a quickstart template for building Firebase authentication workflow into an electron app 4 | * Copyright (c) 2019-2020 by David Asher, https://github.com/david-asher 5 | * 6 | * Read about Electron security: 7 | * https://www.electronjs.org/docs/tutorial/security 8 | */ 9 | 'use strict'; 10 | 11 | /* 12 | * Why is this function here? Our sample app source code lives in the same folder as the 13 | * electron-firebase module source code, so pulling in a module would look like 14 | * require('./electron-firebase') but when the sample app is in your application, the 15 | * electron-firebase code is in the usual and loadable ./node_modules location. So calling 16 | * loadModule() instead of require() would work in either configuration, but for your app, 17 | * you can just delete this function and use require() like the rest of the world. 18 | */ 19 | global.loadModule = function ( moduleName ) 20 | { 21 | var newModule 22 | try { 23 | newModule = require( moduleName ) 24 | } 25 | catch( error ) 26 | { 27 | newModule = require( './' + moduleName ) 28 | } 29 | return newModule 30 | } 31 | 32 | // Load modules. answerBrowser.js and setupApp.js are two modules in our sample app. mainapp 33 | // isn't an app, but a helper library for the main electron-firebase app. 34 | const { app } = require('electron') 35 | const { mainapp } = loadModule( 'electron-firebase' ) 36 | const { infoRequest, showFile } = loadModule('answerBrowser') 37 | const { updateUserDocs } = loadModule('setupApp') 38 | 39 | // Some startup code 40 | 41 | !function() 42 | { 43 | // call this instead of console.log, so output will be suppressed if debugMode isn't set 44 | global.logwrite = function( ...stuff ) {} 45 | 46 | // one call to setup the electron-firebase framework 47 | mainapp.setupAppConfig() 48 | 49 | if ( !global.appConfig.debugMode ) return 50 | 51 | // show all warnings, comment this line of it's too much for you 52 | process.on('warning', e => console.warn(e.stack)); 53 | 54 | global.logwrite = function( ...stuff ) 55 | { 56 | console.log.apply( null, stuff ) 57 | } 58 | }() 59 | 60 | // electron-firebase framework event handling 61 | 62 | mainapp.event.once( "user-login", (user) => 63 | { 64 | // this event will trigger on sign-in, not every time the app runs with cached credentials 65 | logwrite( "EVENT user-login: ", user.displayName ) 66 | }) 67 | 68 | mainapp.event.once( "user-ready", async ( user ) => 69 | { 70 | logwrite( "EVENT user-ready: ", user.displayName ) 71 | await updateUserDocs( user, global.appContext, global.appConfig ) 72 | mainapp.sendToBrowser( 'app-ready' ) 73 | }) 74 | 75 | mainapp.event.once( "window-open", (window) => 76 | { 77 | // first event will be the main window 78 | logwrite( "EVENT window-open: ", window.getTitle() ) 79 | }) 80 | 81 | mainapp.event.once( "main-window-ready", (window) => 82 | { 83 | logwrite( "EVENT main-window-ready: ", window.getTitle() ) 84 | 85 | // shut down the app and clean up when the main window closes 86 | window.on( 'close', (event) => { 87 | console.log( "CLOSE main-window-ready ", event.sender.getTitle() ) 88 | mainapp.closeApplication(window) 89 | }) 90 | 91 | // signout button was pressed 92 | mainapp.getFromBrowser( "user-signout", mainapp.signoutUser ) 93 | 94 | // one of the information request buttons was clicked 95 | mainapp.getFromBrowser( 'info-request', infoRequest ) 96 | 97 | // action request from browser 98 | mainapp.getFromBrowser( 'show-file', showFile ) 99 | }) 100 | 101 | // This function will be called when Electron has finished initialization and is ready to create 102 | // browser windows. Some APIs can only be used after this event occurs. launchInfo is macOS specific. 103 | // see: https://www.electronjs.org/docs/api/app#event-ready 104 | app.on( 'ready', async (launchInfo) => 105 | { 106 | logwrite( "EVENT app ready" ) 107 | global.launchInfo = launchInfo | {} 108 | try { 109 | await mainapp.startMainApp({ 110 | title: "Main Window: " + global.fbConfig.projectId, 111 | open_html: global.appConfig.webapp.mainPage, 112 | show:true 113 | }) 114 | // now do some other synchronous startup thing if you want to 115 | // otherwise wait for the "user-ready" event 116 | } 117 | catch (error) { 118 | console.error( error ) 119 | } 120 | }) 121 | 122 | // see: https://electronjs.org/docs/api/app#event-activate-macos 123 | // macOS specific - Emitted when the application is activated. Various actions can trigger this 124 | // event, such as launching the application for the first time, attempting to re-launch the 125 | // application when it's already running, or clicking on the application's dock or taskbar icon. 126 | app.on( 'activate', (appEvent,hasVisibleWindows) => 127 | { 128 | logwrite( "EVENT app activate " ) 129 | // do whatever 130 | }) 131 | 132 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-firebase", 3 | "productName": "Electron-Firebase Quickstart Framework", 4 | "version": "1.2.0", 5 | "description": "framework for building firebase cloud authentication and database in an electron app", 6 | "main": "electron-firebase.js", 7 | "config": { 8 | "loglevel": "error" 9 | }, 10 | "scripts": { 11 | "preinstall": "node ./install_app/preinstall.js", 12 | "postinstall": "node ./install_app/postinstall.js", 13 | "start": "electron ./main.js", 14 | "test": "electron ./tests/main_test_all.js", 15 | "rebuild": "./node_modules/.bin/electron-rebuild", 16 | "gendocs": "node ./developer/gendocs.js", 17 | "deploy": "node ./config/firebase-deploy.js" 18 | }, 19 | "dependencies": { 20 | "@google-cloud/storage": "^5.1.2", 21 | "axios": "^0.21.1", 22 | "electron": "^12.0.4", 23 | "electron-rebuild": "^2.3.5", 24 | "express": "^4.17.1", 25 | "fast-crc32c": "^2.0.0", 26 | "firebase": "^8.4.1", 27 | "firebaseui": "^4.8.0", 28 | "grpc": "^1.24.3", 29 | "helmet": "^4.5.0", 30 | "selfsigned": "^1.10.8", 31 | "url": "^0.11.0" 32 | }, 33 | "devDependencies": { 34 | "jsdoc-to-markdown": "^7.0.0" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/david-asher/electron-firebase.git" 39 | }, 40 | "keywords": [ 41 | "electron", 42 | "firebase", 43 | "authentication", 44 | "oauth" 45 | ], 46 | "author": "David Asher", 47 | "license": "MIT", 48 | "files": [ 49 | "functions", 50 | "install_app", 51 | "lib", 52 | "pages", 53 | "scripts", 54 | "config", 55 | ".firebaserc", 56 | "firebase.json", 57 | "answerBrowser.js", 58 | "electron-firebase.js", 59 | "main.js", 60 | "setupApp.js" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /pages/electron-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/david-asher/electron-firebase/2ae1dd54de59014fd18939e11cd3c5f2a7911d7a/pages/electron-logo.png -------------------------------------------------------------------------------- /pages/firebase-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/david-asher/electron-firebase/2ae1dd54de59014fd18939e11cd3c5f2a7911d7a/pages/firebase-logo.png -------------------------------------------------------------------------------- /pages/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Electron-Firebase Quickstart Cloud Framework 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 30 |
31 |
32 |

User and App Information (firebase.database)

33 | 34 | 35 | 36 |
37 |
38 |

Cloud Objects (firebase.firestore) 39 |
40 | 41 | 42 | 43 |
44 |

45 |
46 |
47 | 52 |
53 |
54 | 72 |
73 |
74 |
75 |
76 | 80 |
81 |
82 |
83 | Loading... 84 |
85 |

Starting the application...

86 |
87 |
88 | 92 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /pages/logincomplete.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Login Complete 6 | 7 | 8 | 9 | 10 | 11 |

Login Complete

12 | This window can be closed. 13 | 14 | -------------------------------------------------------------------------------- /pages/loginstart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Electron-Firebase Login 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 21 |

Electron-Firebase Quickstart Cloud Framework

22 |
23 |
Loading Firebase Login...
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /pages/splashpage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Splash Page for Electron-Firebase Quickstart 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 |

15 |
16 | 17 | -------------------------------------------------------------------------------- /release-notes.md: -------------------------------------------------------------------------------- 1 | 2 | # Release 1.1.0 (April 2021) 3 | 4 | ## Content Security Policy 5 | * CSP is now applied to all HTML pages 6 | * The policies are configured in the config/content-security-profile.json file 7 | 8 | ## updates 9 | * package.json dependencies, as of April 2021 10 | 11 | ## module changes 12 | 13 | ### app-config.json 14 | * added hostPort parameter 15 | 16 | ### webserver.js 17 | * removed body-parser as independent npm module 18 | * app security check fix for referrer in header 19 | * Content-Security-Policy header 20 | 21 | ### windows.js 22 | * webPreferences: now specifying contextIsolation and enableRemoteModule 23 | -------------------------------------------------------------------------------- /scripts/indexpage_script.js: -------------------------------------------------------------------------------- 1 | // indexpage_script.js 2 | // functions for interacting with main app, note that ipc is defined in weblocal.js 3 | 4 | function signout() 5 | { 6 | // send the signout signal back to Main 7 | ipc.send( 'user-signout', Date.now().toString() ) 8 | } 9 | 10 | // custom elements 11 | class InfoButton extends HTMLButtonElement { 12 | constructor() { 13 | super() 14 | this.setAttribute('class', 'btn btn-info') 15 | this.setAttribute('type', 'button') 16 | this.setAttribute('data-toggle', 'modal') 17 | this.setAttribute('data-target', '#ModalDialog') 18 | } 19 | } 20 | customElements.define('info-button', InfoButton, {extends: 'button'}); 21 | 22 | function showSpinner( parent, bShowState ) 23 | { 24 | const spinner = parent.find('.spinner-border') 25 | switch( bShowState ) { 26 | case true: spinner.removeClass("invisible") 27 | break; 28 | case false: spinner.addClass("invisible") 29 | break; 30 | case "show": spinner.removeClass("d-none") 31 | break; 32 | case "hide": spinner.addClass("d-none") 33 | break; 34 | } 35 | } 36 | 37 | function insertSpinner( parent, bShowState ) 38 | { 39 | const spinner = parent.find('.spinner-border') 40 | if ( bShowState ) spinner.removeClass("d-none") 41 | else spinner.addClass("d-none") 42 | } 43 | 44 | function createNavLink( navColumn, link ) 45 | { 46 | const anchor = $('') // text-info 47 | anchor.attr( "data-toggle", "tab" ) 48 | anchor.attr( "role", "pill" ) 49 | anchor.attr( "aria-controls", "v-pills-profile" ) 50 | anchor.attr( 'href', '#folder-list-anchor' ) 51 | anchor.attr( "data-link", link ) 52 | anchor.text( link ) 53 | anchor.prepend( $('') ) 54 | navColumn.append( anchor ) 55 | } 56 | 57 | async function setFolderList() 58 | { 59 | // ask the main app for the folder list 60 | // put each folder into a nav column of links 61 | const domain = $("#file-domain").find("button.active").val() 62 | const navColumn = $("#nav-folder-links") 63 | navColumn.find(".nav-link").remove() 64 | insertSpinner( navColumn, true ) 65 | 66 | // request back to main for the folder list 67 | const response = await askMain( "info-request", "folder-list", domain ) 68 | insertSpinner( navColumn, false ) 69 | await response.forEach( (element) => { 70 | createNavLink( navColumn, element ) 71 | }) 72 | return response 73 | } 74 | 75 | function displayDate( fileISOString ) 76 | { 77 | const fileDate = new Date( fileISOString ) 78 | var showDate = fileDate.toLocaleDateString( undefined, displayDate.doptions ) 79 | if ( showDate == (new Date()).toLocaleDateString( undefined, displayDate.doptions ) ) { 80 | showDate = fileDate.toLocaleTimeString() 81 | } 82 | return showDate 83 | } 84 | 85 | displayDate.doptions = { 86 | day: "2-digit", 87 | month: "short", 88 | year: "numeric" 89 | } 90 | 91 | function imageLink( iconClass, url, mime ) 92 | { 93 | const link = `` 94 | return link 95 | } 96 | 97 | function setFileList( fileList ) 98 | { 99 | var tableBody = setFileList.fileTable.find('.tableBody') 100 | tableBody.empty() 101 | fileList.forEach( (element) => { 102 | createTableRow( tableBody, 103 | element.name, 104 | displayDate(element.updated), 105 | element.size, 106 | imageLink( "file-text", element.path ), 107 | imageLink( "cloud-download", element.downloadUrl, element.contentType ) ) 108 | }) 109 | } 110 | 111 | document.onreadystatechange = function () 112 | { 113 | if ( document.readyState !== 'complete' ) return 114 | 115 | $('#file-domain button').on('click', function() { 116 | var thisBtn = $(this); 117 | thisBtn.addClass('active').siblings().removeClass('active'); 118 | setFolderList() 119 | }); 120 | 121 | $('#nav-folder-links').click( (event) => 122 | { 123 | setFileList.filesDiv = $("#nav-folder-files") 124 | setFileList.fileTable = setFileList.filesDiv.find( ".table" ) 125 | setFileList.fileTable.addClass("invisible") 126 | showSpinner( setFileList.filesDiv, "show" ) 127 | const anchor = $(event.target) 128 | const panel = $(this) 129 | 130 | // request back to main for the file list 131 | askMain( "info-request", "file-list", anchor.data('link'), $("#file-domain").find("button.active").val() ) 132 | .then( setFileList ) 133 | .finally( () => { 134 | setFileList.fileTable.removeClass("invisible") 135 | showSpinner( setFileList.filesDiv, "hide" ) 136 | }) 137 | }) 138 | 139 | // trigger any class=btn-user-action button basedon its value parameter 140 | $('.btn-user-action').on('click', function() 141 | { 142 | switch( $(this).val() ) { 143 | case 'signout': 144 | signout() 145 | break 146 | } 147 | }); 148 | 149 | $(document).on( "click", "a.fa-file-text", async (event) => 150 | { 151 | const target = $(event.target) 152 | const url = target.data("url") 153 | const modal = $("#ModalDialog") 154 | const card = $('
') 155 | modal.modal('show') 156 | modal.find('.modal-title').text( url ) 157 | 158 | // request back to main for the file content 159 | const response = await askMain( "show-file", "path", url ) 160 | card.append( makeJsonElement(response) ) 161 | modal.find('.modal-body').append(card) 162 | showSpinner( modal, false ) 163 | }) 164 | 165 | $(document).on( "click", "a.fa-cloud-download", (event) => 166 | { 167 | const anchor = $(event.target) 168 | const url = anchor.data("url") 169 | const mime = anchor.data("content") 170 | 171 | // request back to main for the file content 172 | askMain( "show-file", "url", url, mime ) 173 | }) 174 | 175 | $('#ModalDialog').on('show.bs.modal', async (event) => 176 | { 177 | // The modal dialog is about to open, but it's generic. 178 | // So we need to set its title, and build a table with the right content. 179 | const button = $(event.relatedTarget) 180 | const modal = $("#ModalDialog") 181 | modal.find('.modal-title').text( button.text() ) 182 | var table = $('') 183 | modal.find('.modal-body').append(table) 184 | await setModalContent( table, button.data('set'), button.data('ask') ) 185 | showSpinner( modal, false ) 186 | }) 187 | 188 | $('#ModalDialog').on('hidden.bs.modal', (event) => 189 | { 190 | const modal = $("#ModalDialog") 191 | modal.find('.table').remove() 192 | modal.find('.card-body').remove() 193 | showSpinner( modal, true ) 194 | }) 195 | 196 | $('#ModalDialogAccept').on('click', function (event) 197 | { 198 | var button = $(event.relatedTarget) // Button that triggered the modal 199 | // do whatever you want after the user accepts OK 200 | }) 201 | 202 | ipc.on( 'app-ready', () => 203 | { 204 | // hide the opening spinner and show the contents 205 | $("#main-window-loading").addClass("d-none") 206 | $("#main-window-content").removeClass("d-none") 207 | 208 | // populate the folder list 209 | setFolderList() 210 | }) 211 | } 212 | -------------------------------------------------------------------------------- /scripts/indexpage_style.css: -------------------------------------------------------------------------------- 1 | a.nav-link:hover:not(.active) { 2 | color: #F00F00 !important; 3 | cursor: pointer; 4 | } 5 | .tab-content { 6 | overflow-y: scroll; 7 | } 8 | -------------------------------------------------------------------------------- /scripts/logincomplete.js: -------------------------------------------------------------------------------- 1 | // logincomplete.js 2 | // after login, redirect to this page, and we probably want to close it. 3 | 4 | window.close() 5 | -------------------------------------------------------------------------------- /scripts/loginstart.js: -------------------------------------------------------------------------------- 1 | /** 2 | * loginstart.js 3 | * this is not a module, but is included in other HTML pages 4 | */ 5 | 6 | /* For definition of firebaseUI, see: https://github.com/firebase/firebaseui-web 7 | * 8 | * This page does not use ipc to contact the main process because it will call 9 | * external authentication pages, so for security this page should run without 10 | * nodeIntegration enabled. Therefore communication between this Renderer 11 | * process and the Main process happens through API calls over HTTPS. 12 | */ 13 | 14 | const idpConfig = { 15 | 'google.com': { 16 | provider: firebase.auth.GoogleAuthProvider.PROVIDER_ID, 17 | scopes: [ 18 | 'profile', 19 | 'email', 20 | 'openid', 21 | 'https://www.googleapis.com/auth/devstorage.full_control' 22 | ], 23 | customParameters: { prompt: 'select_account' } 24 | }, 25 | 'facebook.com': { 26 | provider: firebase.auth.FacebookAuthProvider.PROVIDER_ID, 27 | scopes: [ 28 | 'public_profile', 29 | 'email' 30 | ], 31 | customParameters: { auth_type: 'reauthenticate' } 32 | }, 33 | 'twitter.com': { 34 | provider: firebase.auth.TwitterAuthProvider.PROVIDER_ID 35 | }, 36 | 'github.com': { 37 | provider: firebase.auth.GithubAuthProvider.PROVIDER_ID 38 | }, 39 | 'password': { 40 | provider: firebase.auth.EmailAuthProvider.PROVIDER_ID 41 | }, 42 | 'phone': { 43 | provider: firebase.auth.PhoneAuthProvider.PROVIDER_ID 44 | } 45 | } 46 | 47 | async function startApplication( fbConfig ) 48 | { 49 | try { 50 | // initializeApp must be called before any other Firebase APIs 51 | firebase.initializeApp( fbConfig ) 52 | 53 | // a signout request may be passed in the browser querystring 54 | await checkForSignout() 55 | 56 | // whether or not the user is forced to log in every time 57 | await setUserPersistence( getTrueQueryParam( "persistentUser" ) ) 58 | 59 | // we must make a decision: if there is a persistent user then do not start the login workflow 60 | const foundUser = await checkForPersistentUser() 61 | if ( !foundUser ) { 62 | throw( "NO USER" ) 63 | } 64 | 65 | // we found a user defined in the IndexedDB object store, so wait for the login 66 | firebase.auth().onAuthStateChanged( postAuthResult ) 67 | } 68 | catch (error) { 69 | // no persisted user or some error, so start the login UI workflow from firebaseUI 70 | const response = await showLoginWindow() 71 | firebaseAuthUIStart( fbConfig, idpConfig ) 72 | } 73 | } 74 | 75 | // when the document is loaded, call Main process to get the app config parameters 76 | // and then we can start firebaseui 77 | document.onreadystatechange = function () 78 | { 79 | if ( document.readyState !== 'complete' ) return 80 | 81 | // ask the Main process to get the configuration parameters for firebase authentication 82 | api( 'GET', getQueryParam( "firebaseconfig" ), null ) 83 | .then( startApplication ) 84 | .catch( (error) => { 85 | alert( "ERROR getting app configuration, please restart application" ) 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /scripts/setmodule_off.js: -------------------------------------------------------------------------------- 1 | if (typeof module === 'object') {window.module = module; module = undefined;} -------------------------------------------------------------------------------- /scripts/setmodule_on.js: -------------------------------------------------------------------------------- 1 | if (window.module) module = window.module; -------------------------------------------------------------------------------- /scripts/splashpage_style.css: -------------------------------------------------------------------------------- 1 | html, body {height:100%;} 2 | html {display:table; width:100%;} 3 | body {display:table-cell; text-align:center; vertical-align:middle;} 4 | .logobox { 5 | width: 50%; 6 | } 7 | .loader { 8 | border: 16px solid #f3f3f3; 9 | border-top: 16px solid #3498db; 10 | border-radius: 50%; 11 | width: 80px; 12 | height: 80px; 13 | animation: spin 2s linear infinite; 14 | margin-right: auto; 15 | margin-left: auto; 16 | } 17 | @keyframes spin { 18 | 0% { transform: rotate(0deg); } 19 | 100% { transform: rotate(360deg); } 20 | } -------------------------------------------------------------------------------- /scripts/weblocal.js: -------------------------------------------------------------------------------- 1 | // weblocal.js 2 | // this is not a module, but is included in other HTML pages 3 | // provides an interface to web localStorage 4 | 5 | // communication between Main and Renderer processes 6 | var ipc = require('electron').ipcRenderer 7 | 8 | // implements the browser interface to localstorage.js 9 | ipc.on( 'localStorage', ( event, command, key, value ) => 10 | { 11 | switch( command ) { 12 | case "setItem": 13 | localStorage.setItem( key, value ) 14 | break 15 | case "removeItem": 16 | localStorage.removeItem( key ) 17 | break 18 | case "getItem": 19 | ipc.send( 'localStorage:' + key, localStorage.getItem( key ) ) 20 | break 21 | } 22 | }) 23 | 24 | function askMain( topic, request, ...parameters ) 25 | { 26 | return new Promise( (resolve, reject) => { 27 | ipc.once( topic, ( event, response ) => { 28 | resolve( response ) 29 | }) 30 | ipc.send( topic, request, parameters[0], parameters[1], parameters[2] ) 31 | }) 32 | } 33 | 34 | ipc.send( 'about-browser', { 35 | language: navigator.language, 36 | userAgent: navigator.userAgent, 37 | platform: navigator.platform, 38 | dateFormat: Intl.DateTimeFormat().resolvedOptions(), 39 | numberFormat: Intl.NumberFormat().resolvedOptions(), 40 | startUTC: (new Date()).toUTCString(), 41 | started: Date.now().toString(), 42 | online: navigator.onLine 43 | }) 44 | -------------------------------------------------------------------------------- /scripts/weblogin.js: -------------------------------------------------------------------------------- 1 | // weblogin.js 2 | // this is not a module, but is included in the loginstart.html page 3 | 4 | // These specify the local storage location for persistent user credentials, 5 | // which we can use to determine if there is a persistent credential that is 6 | // used for auto-login. 7 | const fbDatabaseName = "firebaseLocalStorageDb" 8 | const fbStorageName = "firebaseLocalStorage" 9 | const fbStoredUserKey = "firebase:authUser" 10 | 11 | const queryParams = new URLSearchParams( location.search ) 12 | 13 | function getQueryParam( paramName ) 14 | { 15 | return queryParams.get( paramName ) 16 | } 17 | 18 | function getTrueQueryParam( paramName ) 19 | { 20 | const value = queryParams.get( paramName ) || "" 21 | return ( value === true ) || ( value.toLowerCase() === 'true' ) 22 | } 23 | 24 | function openObjectStore( dataStoreName, dataObjectName, openMode ) 25 | { 26 | var dbReq, fbdb, transaction 27 | return new Promise( (resolve, reject) => 28 | { 29 | dbReq = indexedDB.open( dataStoreName, 1 ) 30 | dbReq.onerror = (error) => { 31 | reject( "openObjectStore ERROR opening database, ", error ) 32 | } 33 | dbReq.onsuccess = (event) => { 34 | fbdb = event.target.result 35 | transaction = fbdb.transaction( dataObjectName, openMode || "readonly" ) 36 | transaction.onerror = (error) => { 37 | reject( "openObjectStore transaction ERROR, ", error ) 38 | } 39 | resolve( transaction.objectStore( dataObjectName ) ) 40 | } 41 | }) 42 | } 43 | 44 | function findKeyInStore( dbStore, dbKeyName ) 45 | { 46 | var keyReq, keyList, foundKey 47 | return new Promise( (resolve, reject) => 48 | { 49 | keyReq = dbStore.getAllKeys() 50 | keyReq.onerror = (error) => { 51 | reject( "findKeyInStore ERROR getting all keys, ", error ) 52 | } 53 | keyReq.onsuccess = (event) => { 54 | keyList = event.target.result 55 | foundKey = keyList.find( (element, index) => { 56 | return ( element.indexOf( dbKeyName ) == 0 ) 57 | }) 58 | if ( !foundKey ) { 59 | return reject( "findKeyInStore: NO KEY" ) 60 | } 61 | resolve( foundKey ) 62 | } 63 | }) 64 | } 65 | 66 | function getObjectFromStore( dbStore, dbKeyName ) 67 | { 68 | var keyValueReq 69 | return new Promise( (resolve, reject) => 70 | { 71 | findKeyInStore( dbStore, dbKeyName ) 72 | .then( (foundKey) => { 73 | keyValueReq = dbStore.get( foundKey ) 74 | keyValueReq.onerror = (error) => { 75 | reject( "getObjectFromStore ERROR getting value, ", error ) 76 | } 77 | keyValueReq.onsuccess = (event) => { 78 | resolve( event.target.result ) 79 | } 80 | }) 81 | .catch( (error) => { 82 | reject( "getObjectFromStore ERROR finding key, ", error ) 83 | }) 84 | }) 85 | } 86 | 87 | function deleteObjectFromStore( dbStore, dbKeyName ) 88 | { 89 | var keyValueReq 90 | return new Promise( (resolve, reject) => 91 | { 92 | findKeyInStore( dbStore, dbKeyName ) 93 | .then( (foundKey) => { 94 | keyValueReq = dbStore.delete( foundKey ) 95 | keyValueReq.onerror = (error) => { 96 | reject( "deleteObjectFromStore ERROR deleting key, ", error ) 97 | } 98 | keyValueReq.onsuccess = (event) => { 99 | resolve( "user deleted" ) 100 | } 101 | }) 102 | .catch( (error) => { 103 | reject( "deleteObjectFromStore ERROR finding key, ", error ) 104 | }) 105 | }) 106 | } 107 | 108 | function postAuthResult( authResult ) 109 | { 110 | // authResult is https://firebase.google.com/docs/reference/js/firebase.auth#.UserCredential 111 | // the user's credentials get sent back to Main process via API 112 | const appUpdate = { 113 | user: firebase.auth().currentUser.toJSON(), 114 | operationType: authResult.operationType || null, 115 | additionalUserInfo: authResult.additionalUserInfo, 116 | refreshToken: authResult.refreshToken || authResult.user.refreshToken, 117 | credential: authResult.credential || null 118 | } 119 | return api( 'POST', getQueryParam( "logintoken" ), appUpdate ) 120 | .then( (status) => { 121 | // redirect to the login complete page which should close this window 122 | window.location.assign( getQueryParam( "loginRedirect" ) ) 123 | }) 124 | .catch( (error) => { 125 | alert( "ERROR in sign-in process, please restart this application: ", stringifyJSON(error) ) 126 | window.close() 127 | }) 128 | } 129 | 130 | async function showLoginWindow() 131 | { 132 | var responseCode = null 133 | 134 | await api( 'GET', getQueryParam( "loginready" ), null ) 135 | .then( (response) => { 136 | responseCode = response.status 137 | }) 138 | .catch( (error ) => { 139 | console.error( "showLoginWindow ERROR: ", error ) 140 | }) 141 | return responseCode 142 | } 143 | 144 | function firebaseAuthUIStart( fbConfig, idpConfig ) 145 | { 146 | var uiConfig = { 147 | callbacks: { 148 | signInSuccessWithAuthResult: ( authResult, redirectUrl ) => { 149 | // send the authentication event and result back to the main app 150 | postAuthResult( authResult ) 151 | // return false to prevent redirection 152 | return false 153 | }, 154 | signInFailure: ( error, credential ) => { 155 | alert( "Sign in failure: " + error + ". Please restart the app" ) 156 | window.close() 157 | }, 158 | uiShown: () => { 159 | // The widget is rendered. Hide the loader. 160 | document.getElementById('loader').style.display = 'none'; 161 | } 162 | }, 163 | signInOptions: [ 164 | // fill this in dynamically based on configuration 165 | ], 166 | // see: https://github.com/firebase/firebaseui-web#credential-helper 167 | credentialHelper: firebaseui.auth.CredentialHelper.ACCOUNT_CHOOSER_COM, 168 | // tosUrl and privacyPolicyUrl accept either url string or a callback function. 169 | tosUrl: fbConfig.tosUrl, 170 | privacyPolicyUrl: fbConfig.privacyUrl 171 | } 172 | 173 | // Initialize the FirebaseUI Widget 174 | // set up the configuration for each identity provider that was specified in uiConfig 175 | fbConfig.providers.forEach( ( provider ) => { 176 | uiConfig.signInOptions.push( idpConfig[ provider ] ) 177 | }) 178 | var ui = new firebaseui.auth.AuthUI( firebase.auth() ) 179 | ui.start('#firebaseui-auth-container', uiConfig) 180 | } 181 | 182 | async function checkForPersistentUser() 183 | { 184 | try { 185 | const dbStore = await openObjectStore( fbDatabaseName, fbStorageName ) 186 | return await getObjectFromStore( dbStore, fbStoredUserKey ) 187 | } 188 | catch (error) { 189 | throw( "checkForPersistentUser ERROR, ", error ) 190 | } 191 | } 192 | 193 | async function setUserPersistence( allowPersistence ) 194 | { 195 | var persistence = firebase.auth.Auth.Persistence.NONE 196 | 197 | switch ( allowPersistence ) { 198 | case false: 199 | case 'false': 200 | case 'SESSION': 201 | case 'session': 202 | persistence = firebase.auth.Auth.Persistence.SESSION 203 | break; 204 | case true: 205 | case 'true': 206 | case 'LOCAL': 207 | case 'local': 208 | persistence = firebase.auth.Auth.Persistence.LOCAL 209 | break; 210 | } 211 | await firebase.auth().setPersistence( persistence ) 212 | } 213 | 214 | async function checkForSignout() 215 | { 216 | try { 217 | // normal case is no signout, just leave 218 | if ( !getTrueQueryParam( "signoutuser" ) ) return true 219 | // signout, clear persistence and signout from firebase 220 | setUserPersistence( 'none' ) 221 | return firebase.auth().signOut() 222 | } 223 | catch (error) { 224 | alert( "Error in signout process: ", error ) 225 | throw( error ) 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /scripts/webmodal.js: -------------------------------------------------------------------------------- 1 | // webmodal.js 2 | // this is not a module, but is included in the index.html page 3 | // functions for formatting data returned from info-buttons requests 4 | 5 | function isImagePath(url) 6 | { 7 | if ( typeof url !== 'string' ) return false 8 | return null != url.match( /\.(jpeg|jpg|gif|png)$/, 'i' ) 9 | } 10 | 11 | function isURL( whatever ) 12 | { 13 | if ( typeof whatever !== 'string' ) return false 14 | return null != whatever.match( /^(http|https):\/\//, 'i' ) 15 | } 16 | 17 | function isTag( whatever ) 18 | { 19 | if ( typeof whatever !== 'string' ) return false 20 | return ( whatever.charAt(0) == '<' && whatever.charAt(whatever.length-1) == '>' ) 21 | } 22 | 23 | function makeImageElement( url ) 24 | { 25 | return $(`${url}`) 26 | } 27 | 28 | function makeJsonElement( arg ) 29 | { 30 | var pretext = $('
')
31 |     pretext.text( JSON.stringify( arg, null, 4 ) )
32 |     return pretext
33 | }
34 | 
35 | function makeBasicElement( arg )
36 | {
37 |     return String( arg )
38 | }
39 | 
40 | function createTableRow( table, ...args )
41 | {
42 |     var row = $('
') 43 | table.append(row) 44 | for (let arg of args) { 45 | var cell = $('
') 46 | row.append(cell) 47 | // make a formatting decision based on the content of this arg 48 | if ( isTag(arg) ) cell.append( $(arg) ) 49 | else if ( $.isPlainObject(arg) ) cell.append( makeJsonElement(arg) ) 50 | else if ( isURL(arg) ) cell.append( makeImageElement(arg) ) 51 | else cell.text( makeBasicElement( arg ) ) 52 | } 53 | } 54 | 55 | async function setModalContent( table, request, parameter ) 56 | { 57 | // send the info-request back to the main app, and 58 | // stuff each response into a table row 59 | const response = await askMain( 'info-request', request, parameter ) 60 | Object.entries(response).forEach( ([key,value]) => { 61 | createTableRow( table, key, value ) 62 | }) 63 | return response 64 | } 65 | -------------------------------------------------------------------------------- /scripts/webutils.js: -------------------------------------------------------------------------------- 1 | // webutils.js 2 | // this is not a module, but is included in other HTML pages 3 | 4 | function isJSON( s ) 5 | { 6 | return ( typeof s == 'string' ) && ( s.charAt(0) == '{' || s.charAt(0) == '[' ); 7 | } 8 | 9 | function isObject( obj ) 10 | { 11 | return obj && typeof obj == 'object' 12 | } 13 | 14 | function parseJSON( jString ) 15 | { 16 | if( jString == null ) return null 17 | if ( !isJSON( jString ) ) { 18 | return jString 19 | } 20 | try { 21 | return JSON.parse( jString ) 22 | } 23 | catch (err) { 24 | console.error( "ERROR parsing JSON: ", err, jString ) 25 | return null 26 | } 27 | } 28 | 29 | function stringifyJSON( thisObject ) 30 | { 31 | if ( thisObject == null ) return null 32 | if ( typeof thisObject !== 'object' && !Array.isArray( thisObject ) ) { 33 | return thisObject 34 | } 35 | try { 36 | return JSON.stringify( thisObject ); 37 | } 38 | catch (err) { 39 | console.error( "ERROR stringifying JSON: ", err ) 40 | return null 41 | } 42 | } 43 | 44 | function api( method, url, payload ) 45 | { 46 | return new Promise((resolve, reject) => { 47 | var xhr = new XMLHttpRequest(); 48 | xhr.open( method , url ); 49 | if ( isJSON( payload ) || isObject( payload ) ) { 50 | xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); 51 | } 52 | xhr.onreadystatechange = function() { 53 | // readyState 4 is DONE 54 | if ( xhr.readyState !== 4 ) return 55 | if ( xhr.status >= 300 ) { 56 | console.error( "ERROR in api: ", method, url, xhr.status, xhr.response ) 57 | reject( {error: "API ERROR", url: url, payload: payload, status: xhr.status } ) 58 | return 59 | } 60 | resolve( parseJSON( xhr.response ) ) 61 | } 62 | xhr.send( stringifyJSON( payload ) ); 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /setupApp.js: -------------------------------------------------------------------------------- 1 | /* setupapp.js 2 | * Copyright (c) 2019-2021 by David Asher, https://github.com/david-asher 3 | * 4 | * This is a quickstart template for building Firebase authentication workflow into an electron app 5 | * This module contains functions that help to initialize or update the application 6 | * @module setupapp 7 | */ 8 | 'use strict'; 9 | 10 | const { firestore, fbstorage } = loadModule( 'electron-firebase' ) 11 | 12 | const docAboutmeFolder = "aboutme/" 13 | 14 | function makeUserDocuments( user, appContext, appConfig ) 15 | { 16 | if ( !user || !user.uid || !user.displayName ) { 17 | return null 18 | } 19 | 20 | const isNow = ( new Date() ).toISOString() 21 | 22 | // fixups 23 | const profile = { ... user.profile } 24 | if ( !profile.email ) profile.email = user.email || null 25 | if ( !profile.picture ) profile.picture = user.photoURL || null 26 | 27 | const provider = { ... user.providerData[0] } 28 | if ( !provider.displayName ) provider.displayName = user.displayName || null 29 | if ( !provider.email ) provider.displayName = user.email || null 30 | if ( !provider.phoneNumber ) provider.phoneNumber = user.phoneNumber || null 31 | if ( !provider.photoURL ) provider.photoURL = user.photoURL || null 32 | 33 | const account = { 34 | uid: user.uid, 35 | name: user.displayName, 36 | photo: user.photoURL || null, 37 | email: user.email || null, 38 | created: user.metadata.creationTime || null, 39 | accessed: isNow 40 | } 41 | 42 | const session = { 43 | uid: user.uid, 44 | apiKey: global.fbConfig.apiKey || null, 45 | project: global.fbConfig.projectId || null, 46 | domain: global.fbConfig.authDomain || null, 47 | authenticated: user.metadata.lastSignInTime || null, 48 | start: isNow 49 | } 50 | 51 | return { 52 | profile: profile, 53 | provider: provider, 54 | account: account, 55 | session: session, 56 | application: appContext, 57 | configuration: appConfig 58 | } 59 | } 60 | 61 | async function updateUserDocs( user, appContext, appConfig ) 62 | { 63 | try { 64 | const userDocs = makeUserDocuments( user, appContext, appConfig ) 65 | 66 | const aboutMe = await firestore.doc.about( docAboutmeFolder + "profile" ) 67 | 68 | if ( !aboutMe || !aboutMe.exists ) { 69 | await firestore.doc.write( docAboutmeFolder + "profile", userDocs.profile ) 70 | await firestore.doc.write( docAboutmeFolder + "provider", userDocs.provider ) 71 | await firestore.doc.write( docAboutmeFolder + "account", userDocs.account ) 72 | await firestore.doc.write( docAboutmeFolder + "session", userDocs.session ) 73 | } 74 | 75 | const myProfile = await fbstorage.file.about( "aboutme/MyProfile" ) 76 | 77 | if ( !myProfile || !myProfile.exists ) { 78 | await fbstorage.file.upload( "aboutme/MyProfile", userDocs.profile ) 79 | 80 | await fbstorage.file.upload( "aboutme/MyAccount", userDocs.account ) 81 | await fbstorage.file.upload( "/info/first/second/MyProfile", userDocs.profile ) 82 | await fbstorage.file.upload( "/info/MyProvider", userDocs.provider ) 83 | await fbstorage.file.upload( "/account/third/my-account", userDocs.account ) 84 | await fbstorage.file.upload( "/account/my-session", userDocs.session ) 85 | 86 | await fbstorage.app.upload( "account/my-session", userDocs.session ) 87 | await fbstorage.app.upload( "info/MyProvider", userDocs.provider ) 88 | 89 | await fbstorage.public.upload( "account/my-session", userDocs.session ) 90 | await fbstorage.public.upload( "info/MyProvider", userDocs.provider ) 91 | 92 | } 93 | } 94 | catch (error) { 95 | console.error( error ) 96 | } 97 | } 98 | 99 | module.exports = { 100 | updateUserDocs: updateUserDocs 101 | } 102 | 103 | -------------------------------------------------------------------------------- /tests/generated.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "_id": "5c749a8f42f6afd2f30e23a6", 4 | "index": 0, 5 | "guid": "fb89a89d-0256-48d0-ab8c-a89cce710d34", 6 | "isActive": true, 7 | "balance": "$2,068.74", 8 | "picture": "http://placehold.it/32x32", 9 | "age": 28, 10 | "eyeColor": "blue", 11 | "name": "Lou Mckee", 12 | "gender": "female", 13 | "company": "SOFTMICRO", 14 | "email": "loumckee@softmicro.com", 15 | "phone": "+1 (961) 417-3509", 16 | "address": "939 Clinton Avenue, Chalfant, New York, 8598", 17 | "about": "Ad quis officia do cupidatat dolor non elit fugiat magna sint. Labore consectetur minim duis dolore proident minim nisi pariatur deserunt veniam incididunt commodo minim labore. Quis consequat amet culpa cillum eu fugiat fugiat dolor aliqua sint consequat ut. Aute commodo culpa est pariatur anim sint aliqua.\r\n", 18 | "registered": "2016-09-06T04:41:03 +04:00", 19 | "latitude": 42.489112, 20 | "longitude": -176.878198, 21 | "tags": [ 22 | "consectetur", 23 | "eu", 24 | "occaecat", 25 | "est", 26 | "amet", 27 | "non", 28 | "exercitation" 29 | ], 30 | "friends": [ 31 | { 32 | "id": 0, 33 | "name": "Elinor Park" 34 | }, 35 | { 36 | "id": 1, 37 | "name": "Iris Nunez" 38 | }, 39 | { 40 | "id": 2, 41 | "name": "Salinas Sims" 42 | } 43 | ], 44 | "greeting": "Hello, Lou Mckee! You have 3 unread messages.", 45 | "favoriteFruit": "apple" 46 | }, 47 | { 48 | "_id": "5c749a8fc99924096feaea4a", 49 | "index": 1, 50 | "guid": "6cb0fc4e-cfe0-4bf5-b6b3-dc635ad15e8d", 51 | "isActive": true, 52 | "balance": "$1,111.32", 53 | "picture": "http://placehold.it/32x32", 54 | "age": 24, 55 | "eyeColor": "brown", 56 | "name": "Elizabeth Robinson", 57 | "gender": "female", 58 | "company": "XELEGYL", 59 | "email": "elizabethrobinson@xelegyl.com", 60 | "phone": "+1 (988) 506-2008", 61 | "address": "468 Cove Lane, Islandia, Texas, 4733", 62 | "about": "Non dolor consequat commodo amet pariatur mollit anim sint eu. Excepteur sunt exercitation minim commodo labore duis id sint adipisicing adipisicing magna ut. Exercitation tempor ullamco irure velit ut occaecat non nulla. Enim consequat quis adipisicing excepteur velit proident id irure eiusmod. Deserunt esse Lorem ex aute veniam laboris id excepteur enim et consequat fugiat. Dolor aliquip exercitation nulla commodo elit sit laborum deserunt reprehenderit ullamco ipsum ut adipisicing.\r\n", 63 | "registered": "2017-03-04T05:41:59 +05:00", 64 | "latitude": -14.38438, 65 | "longitude": 179.438195, 66 | "tags": [ 67 | "dolore", 68 | "dolor", 69 | "nulla", 70 | "cupidatat", 71 | "proident", 72 | "non", 73 | "tempor" 74 | ], 75 | "friends": [ 76 | { 77 | "id": 0, 78 | "name": "Cook Watson" 79 | }, 80 | { 81 | "id": 1, 82 | "name": "Gutierrez Herman" 83 | }, 84 | { 85 | "id": 2, 86 | "name": "Katelyn Rodgers" 87 | } 88 | ], 89 | "greeting": "Hello, Elizabeth Robinson! You have 5 unread messages.", 90 | "favoriteFruit": "strawberry" 91 | }, 92 | { 93 | "_id": "5c749a8fd27511e0e4caf518", 94 | "index": 2, 95 | "guid": "6054d644-d267-469f-b9e5-1d2081682751", 96 | "isActive": true, 97 | "balance": "$3,147.53", 98 | "picture": "http://placehold.it/32x32", 99 | "age": 34, 100 | "eyeColor": "blue", 101 | "name": "Orr Short", 102 | "gender": "male", 103 | "company": "EPLODE", 104 | "email": "orrshort@eplode.com", 105 | "phone": "+1 (974) 599-3402", 106 | "address": "224 Monaco Place, Welch, Wisconsin, 2501", 107 | "about": "Sint aliqua cillum eiusmod exercitation voluptate duis Lorem pariatur ipsum anim proident enim pariatur. Ad labore quis minim occaecat deserunt sunt do laborum laboris amet cillum. In eiusmod id veniam pariatur voluptate consectetur fugiat laboris occaecat et commodo amet. Tempor quis nisi velit consequat aliquip pariatur officia. Id nulla amet exercitation ut tempor id dolore esse ipsum esse. Aliqua reprehenderit laboris commodo culpa est ipsum officia elit laboris voluptate excepteur. Elit amet veniam exercitation velit eu elit dolore fugiat.\r\n", 108 | "registered": "2018-12-19T04:07:50 +05:00", 109 | "latitude": -40.242281, 110 | "longitude": 13.339475, 111 | "tags": [ 112 | "labore", 113 | "reprehenderit", 114 | "magna", 115 | "eiusmod", 116 | "sint", 117 | "dolor", 118 | "minim" 119 | ], 120 | "friends": [ 121 | { 122 | "id": 0, 123 | "name": "Turner Daniel" 124 | }, 125 | { 126 | "id": 1, 127 | "name": "Snyder Espinoza" 128 | }, 129 | { 130 | "id": 2, 131 | "name": "Neal Key" 132 | } 133 | ], 134 | "greeting": "Hello, Orr Short! You have 10 unread messages.", 135 | "favoriteFruit": "strawberry" 136 | }, 137 | { 138 | "_id": "5c749a8f80f7dd9e8fd41679", 139 | "index": 3, 140 | "guid": "d96617a8-d7ca-445b-95e0-5a823b19983b", 141 | "isActive": false, 142 | "balance": "$1,492.03", 143 | "picture": "http://placehold.it/32x32", 144 | "age": 34, 145 | "eyeColor": "blue", 146 | "name": "Rojas Morales", 147 | "gender": "male", 148 | "company": "SONGLINES", 149 | "email": "rojasmorales@songlines.com", 150 | "phone": "+1 (883) 431-3642", 151 | "address": "214 Nichols Avenue, Bainbridge, Virgin Islands, 323", 152 | "about": "Nostrud laboris dolor sit elit aliqua ad. Anim aliquip cillum eu nostrud sit. Enim qui labore proident Lorem aliqua proident cupidatat sint nulla labore elit adipisicing non velit. Ex consequat excepteur ipsum cupidatat amet in magna veniam qui nisi magna duis.\r\n", 153 | "registered": "2016-05-03T11:40:36 +04:00", 154 | "latitude": -85.328954, 155 | "longitude": 110.667992, 156 | "tags": [ 157 | "culpa", 158 | "incididunt", 159 | "excepteur", 160 | "ipsum", 161 | "dolor", 162 | "qui", 163 | "qui" 164 | ], 165 | "friends": [ 166 | { 167 | "id": 0, 168 | "name": "Teri Hatfield" 169 | }, 170 | { 171 | "id": 1, 172 | "name": "Mcmahon Ward" 173 | }, 174 | { 175 | "id": 2, 176 | "name": "Pate Garrett" 177 | } 178 | ], 179 | "greeting": "Hello, Rojas Morales! You have 3 unread messages.", 180 | "favoriteFruit": "strawberry" 181 | }, 182 | { 183 | "_id": "5c749a8f17cf878b8d979244", 184 | "index": 4, 185 | "guid": "7998ca64-fde5-42d8-ac7b-e56e7ed61309", 186 | "isActive": true, 187 | "balance": "$3,671.65", 188 | "picture": "http://placehold.it/32x32", 189 | "age": 20, 190 | "eyeColor": "blue", 191 | "name": "Roth Benson", 192 | "gender": "male", 193 | "company": "EURON", 194 | "email": "rothbenson@euron.com", 195 | "phone": "+1 (971) 531-3567", 196 | "address": "508 Dean Street, Dawn, Kentucky, 5276", 197 | "about": "Dolor aute excepteur cupidatat magna velit proident voluptate Lorem nulla sit ad reprehenderit amet commodo. Dolore nostrud id sint aute labore. Est deserunt minim aliquip exercitation aliquip do aute nisi aliquip ex amet duis. Aliqua voluptate dolore minim ullamco ut magna voluptate. Enim consequat incididunt magna sunt tempor proident proident elit commodo commodo cillum eu qui tempor.\r\n", 198 | "registered": "2017-08-18T06:31:37 +04:00", 199 | "latitude": 24.692351, 200 | "longitude": 32.425209, 201 | "tags": [ 202 | "velit", 203 | "id", 204 | "velit", 205 | "quis", 206 | "id", 207 | "laborum", 208 | "aute" 209 | ], 210 | "friends": [ 211 | { 212 | "id": 0, 213 | "name": "Gibbs Rosario" 214 | }, 215 | { 216 | "id": 1, 217 | "name": "Stein Hodges" 218 | }, 219 | { 220 | "id": 2, 221 | "name": "Letha Manning" 222 | } 223 | ], 224 | "greeting": "Hello, Roth Benson! You have 6 unread messages.", 225 | "favoriteFruit": "banana" 226 | } 227 | ] -------------------------------------------------------------------------------- /tests/main_test_all.js: -------------------------------------------------------------------------------- 1 | /* main.js 2 | * electron-firebase 3 | * This is a quickstart template for building Firebase authentication workflow into an electron app 4 | * Copyright (c) 2019 by David Asher, https://github.com/david-asher 5 | * 6 | * Log output will show DeprecationWarning: grpc.load blah blah 7 | * which is a know bug: https://github.com/googleapis/nodejs-vision/issues/120 8 | * and here: https://github.com/firebase/firebase-js-sdk/issues/1112 9 | */ 10 | 'use strict'; 11 | 12 | /** 13 | * Testing for all of the electron-firebase modules. 14 | */ 15 | 16 | process.on('warning', e => console.warn(e.stack)); 17 | 18 | const { app } = require('electron') 19 | const { mainapp } = require( '../electron-firebase' ) 20 | 21 | // console logging is not strictly synchronous, so for testing we force log and error to block 22 | 23 | const nodeFS = require('fs') 24 | const nodeUtil = require('util') 25 | 26 | var lastTime = Date.now() 27 | 28 | console.log = (...args) => { 29 | const isNow = Date.now() 30 | const delta = ( " " + ( isNow - lastTime ) ).slice(-4) 31 | nodeFS.writeSync( process.stdout.fd, delta + " -- " + nodeUtil.format.apply(null,args) + "\n" ) 32 | lastTime = isNow 33 | } 34 | 35 | console.error = (...args) => { 36 | const isNow = Date.now() 37 | const delta = ( " " + ( isNow - lastTime ) ).slice(-4) 38 | nodeFS.writeSync( process.stderr.fd, delta + " xx " + nodeUtil.format.apply(null,args) + "\n" ) 39 | lastTime = isNow 40 | } 41 | 42 | global.__TESTMODE__ = true 43 | global.testDocPath = "./tests/generated.json" 44 | 45 | // catch everything just in case 46 | 47 | process.on( 'unhandledRejection', (reason, p) => { 48 | console.log('Unhandled Rejection at: Promise', p, 'reason:', reason) 49 | // application specific logging, throwing an error, or other logic here 50 | }) 51 | 52 | // run the tests after both the user is ready and main window is open 53 | var bIsWindowOpen = false 54 | var bIsUserReady = false 55 | 56 | // one call to setup the electron-firebase framework 57 | mainapp.setupAppConfig() 58 | 59 | // inject the tests folder into the webpage static content set 60 | global.appConfig.webFolders.push( "tests" ) 61 | 62 | function logwrite( ...stuff ) 63 | { 64 | // if ( !global.appConfig.debugMode ) return 65 | console.log.apply( null, stuff ) 66 | } 67 | 68 | // readFile and readJSON are defined here, even though there is a fileutils module, 69 | // to eliminate that dependency 70 | 71 | global.readFile = function( sourceFilename ) 72 | { 73 | try { 74 | return nodeFS.readFileSync( sourceFilename ).toString() 75 | } 76 | catch (error) { 77 | return null 78 | } 79 | } 80 | 81 | global.readJSON = function( sourceFilename ) 82 | { 83 | try { 84 | return JSON.parse( global.readFile( sourceFilename ) ) 85 | } 86 | catch (error) { 87 | return null 88 | } 89 | } 90 | 91 | async function testModule( moduleName, withOption = "" ) 92 | { 93 | // the process for running one module through a test 94 | console.log( `++ ++ ++ ++ ++ ++ ++ ${moduleName}.${withOption}` ) 95 | const testModule = require( `./test_${moduleName}` ) 96 | await testModule.testall( withOption ) 97 | console.log( "-- -- -- -- -- -- -- -- -- -- -- -- ") 98 | } 99 | 100 | async function runTests() 101 | { 102 | // spin through all of the modules 103 | await testModule( "applibrary" ) 104 | await testModule( "fileutils" ) 105 | await testModule( "localstorage" ) 106 | await testModule( "firestore", "doc" ) 107 | await testModule( "firestore", "app" ) 108 | await testModule( "firestore", "public" ) 109 | await testModule( "fbstorage", "file" ) 110 | await testModule( "fbstorage", "app" ) 111 | await testModule( "fbstorage", "public" ) 112 | 113 | // done! 114 | app.exit(0) 115 | } 116 | 117 | 118 | // electron-firebase framework event handling 119 | 120 | mainapp.event.once( "user-login", (user) => 121 | { 122 | // this event will trigger on sign-in, not every time the app runs with cached credentials 123 | logwrite( "EVENT user-login: ", user.displayName ) 124 | }) 125 | 126 | mainapp.event.once( "user-ready", async ( user ) => 127 | { 128 | logwrite( "EVENT user-ready: ", user.displayName ) 129 | mainapp.sendToBrowser( 'app-ready' ) 130 | if ( bIsWindowOpen ) runTests() 131 | bIsUserReady = true 132 | }) 133 | 134 | mainapp.event.once( "window-open", (window) => 135 | { 136 | // first event will be the main window 137 | logwrite( "EVENT window-open: ", window.getTitle() ) 138 | if ( bIsUserReady ) runTests() 139 | bIsWindowOpen = true 140 | }) 141 | 142 | mainapp.event.once( "main-window-ready", (window) => 143 | { 144 | logwrite( "EVENT main-window-ready: ", window.getTitle() ) 145 | 146 | // mainapp.getFromBrowser( "user-signout", mainapp.signoutUser ) 147 | 148 | }) 149 | 150 | mainapp.event.once( "main-window-close", (window) => 151 | { 152 | // use this to clean up things 153 | }) 154 | 155 | // electron app event handling 156 | 157 | // Quit when all windows are closed. 158 | // see: https://www.electronjs.org/docs/api/app#event-window-all-closed 159 | app.on( 'window-all-closed', () => 160 | { 161 | logwrite( "EVENT app window-all-closed" ) 162 | mainapp.closeApplication() 163 | }) 164 | 165 | // This function will be called when Electron has finished initialization and is ready to create 166 | // browser windows. Some APIs can only be used after this event occurs. launchInfo is macOS specific. 167 | // see: https://www.electronjs.org/docs/api/app#event-ready 168 | app.on( 'ready', async (launchInfo) => 169 | { 170 | logwrite( "EVENT app ready" ) 171 | global.launchInfo = launchInfo | {} 172 | try { 173 | await mainapp.startMainApp({ 174 | title: "TEST Window: " + global.fbConfig.projectId, 175 | open_html: "tests/testpage_local.html", 176 | show: true, ///////////////////////////////////////// false, 177 | movable: false, 178 | resizable: false 179 | }) 180 | } 181 | catch (error) { 182 | console.error( error ) 183 | } 184 | }) 185 | 186 | // see: https://electronjs.org/docs/api/app#event-activate-macos 187 | // macOS specific - Emitted when the application is activated. Various actions can trigger this 188 | // event, such as launching the application for the first time, attempting to re-launch the 189 | // application when it's already running, or clicking on the application's dock or taskbar icon. 190 | app.on( 'activate', (appEvent,hasVisibleWindows) => 191 | { 192 | logwrite( "EVENT app activate " ) 193 | // do whatever 194 | }) 195 | 196 | -------------------------------------------------------------------------------- /tests/test_all_modules.js: -------------------------------------------------------------------------------- 1 | /* 2 | * test_apputils.js 3 | * Unit-test automation for electron-firebase 4 | * Copyright (c) 2019 by David Asher, https://github.com/david-asher 5 | */ 6 | 'use strict'; 7 | 8 | var moduleList = [ 9 | // 'test_applibrary', 10 | // 'test_fileutils', 11 | // 'test_firestore', 12 | // 'test_fbstorage', 13 | // 'test_windows', 14 | // 'test_localstorage', 15 | 'test_webserver' 16 | ] 17 | 18 | /* 19 | * global.__TESTMODE__ is used to configure each JS module to export modulename() 20 | * and probe() functions. Otherwise in normal use these won't be exported. 21 | * 22 | * global.testDocPath is a file with a bunch of random JSON stuff. 23 | * thanks to https://www.json-generator.com/ 24 | * 25 | * The readFile() and readJSON() files are included here as very simple test 26 | * functions - if they throw an error it's okay, it's better to be sure that 27 | * the test are working. 28 | */ 29 | 30 | global.__TESTMODE__ = true 31 | global.testDocPath = "./tests/generated.json" 32 | 33 | const fs = require('fs') 34 | 35 | /* if you want to catch unhandled rejections: 36 | process.on( 'unhandledRejection', (reason, p) => { 37 | console.error('Unhandled Rejection at: Promise', p, 'reason:', reason) 38 | // application specific logging, throwing an error, or other logic here 39 | }) 40 | */ 41 | 42 | global.readFile = function( sourceFilename ) 43 | { 44 | try { 45 | return fs.readFileSync( sourceFilename ).toString() 46 | } 47 | catch (error) { 48 | return null 49 | } 50 | } 51 | 52 | global.readJSON = function( sourceFilename ) 53 | { 54 | try { 55 | return JSON.parse( global.readFile( sourceFilename ) ) 56 | } 57 | catch (error) { 58 | return null 59 | } 60 | } 61 | 62 | // const { auth, data } = require('../lib/electron-firebase') 63 | const efb = require('../electron-firebase') 64 | 65 | function catchError( error ) 66 | { 67 | throw( error ) 68 | } 69 | 70 | async function setupUser( user ) 71 | { 72 | global.user = user 73 | return await efb.data.setup( user ) 74 | .then( async (rootDoc) => { 75 | global.rootDoc = rootDoc 76 | return await Promise.resolve( rootDoc ) 77 | }) 78 | .catch( catchError ) 79 | } 80 | 81 | async function loginAndSetup() 82 | { 83 | efb.auth.initializeFirebase() 84 | 85 | return await efb.auth.startNewSignIn() // signInSavedUser() 86 | .then( async (user) => { 87 | return await setupUser( user ) 88 | }) 89 | .catch( catchError ) 90 | } 91 | 92 | async function testModule( moduleName, index ) 93 | { 94 | const module = require( `./${moduleName}` ) 95 | console.log( `${index}: ${module.target()}` ) 96 | await module.testall().catch( catchError ) 97 | console.log( "_ _ _ _ _ _ _ _") 98 | } 99 | 100 | async function testList() 101 | { 102 | for ( var moduleIndex in moduleList ) { 103 | await testModule( moduleList[moduleIndex], moduleIndex ) 104 | } 105 | } 106 | 107 | async function runAllTests() 108 | { 109 | console.log( "* * * Testing all modules * * *" ) 110 | 111 | // the app config must be loaded since it's used by a lot of functions 112 | global.appConfig = global.readJSON( 'app-config-test.json' ) 113 | if ( !global.appConfig ) global.appConfig = global.readJSON( 'config/app-config.json' ) 114 | global.fbConfig = global.readJSON( 'firebase-config-test.json' ) 115 | if ( !global.fbConfig ) global.fbConfig = global.readJSON( 'config/firebase-config.json' ) 116 | 117 | // console.log( "global.appConfig = ", global.appConfig ) 118 | // console.log( "global.fbConfig = ", global.fbConfig ) 119 | 120 | // database and storage unit tests require a logged in user 121 | await loginAndSetup() 122 | .then( testList ) 123 | .catch( catchError ) 124 | 125 | console.log( "ALL TESTS COMPLETED SUCCESSFULLY") 126 | process.exit(0) 127 | } 128 | 129 | runAllTests() 130 | -------------------------------------------------------------------------------- /tests/test_applibrary.js: -------------------------------------------------------------------------------- 1 | // test_applibrary.js 2 | 'use strict'; 3 | 4 | const assert = require('assert').strict 5 | 6 | // module under test: 7 | const { applib } = require('../electron-firebase'); 8 | 9 | // specimens 10 | const jsonDoc = global.readFile( global.testDocPath ) 11 | const testDoc = JSON.parse( jsonDoc ) 12 | const testObj = testDoc[0] 13 | 14 | var errorCount = 0 15 | 16 | var baseOptions = { 17 | https: { 18 | rejectUnauthorized: false, 19 | }, 20 | headers: { 21 | referer: global.appConfig.webapp.hostUrl 22 | } 23 | } 24 | 25 | const newDocOptions = { ...baseOptions, ...{ 26 | method: 'POST', 27 | url: `${global.appConfig.webapp.hostUrl}/api/test/newdoc`, 28 | data: testObj 29 | } } 30 | 31 | const oldDocOptions = { ...baseOptions, ...{ 32 | method: 'GET', 33 | url: `${global.appConfig.webapp.hostUrl}/api/test/olddoc` 34 | } } 35 | 36 | async function testallFunctions() 37 | { 38 | var newDoc 39 | 40 | // isJSON( s ) 41 | console.log( ">> isJSON" ) 42 | assert( applib.isJSON( jsonDoc ) ) // valid JSON 43 | assert( !applib.isJSON( testObj ) ) // object 44 | assert( !applib.isJSON( testObj._id ) ) // string 45 | assert( !applib.isJSON( testObj.latitude ) ) // number 46 | assert( !applib.isJSON( testObj.tags ) ) // array 47 | 48 | // isObject( obj ) 49 | console.log( ">> isObject" ) 50 | assert( applib.isObject( testObj ) ) // object 51 | assert( applib.isObject( testObj.tags ) ) // array 52 | assert( !applib.isObject( testObj._id ) ) // string 53 | assert( !applib.isObject( testObj.latitude ) ) // number 54 | 55 | // parseJSON( inputSerialized, optionalErrorCode ) 56 | // note: correct testing of optionalErrorCode yields a process exit 57 | console.log( ">> parseJSON" ) 58 | assert.deepEqual( testDoc, applib.parseJSON( jsonDoc ) ) 59 | 60 | // compactJSON( inputObject, optionalErrorCode ) 61 | console.log( ">> compactJSON" ) 62 | assert.deepEqual( testDoc, applib.parseJSON( applib.compactJSON( testDoc ) ) ) 63 | 64 | // stringifyJSON( inputObject, optionalErrorCode ) 65 | console.log( ">> stringifyJSON" ) 66 | assert.deepEqual( testDoc, applib.parseJSON( applib.stringifyJSON( testDoc ) ) ) 67 | 68 | // mergeObjects( ...objects ) 69 | console.log( ">> mergeObjects" ) 70 | const merged = applib.mergeObjects( testDoc[0], testDoc[1], testDoc[2] ) 71 | assert.deepEqual( merged.tags, [].concat( testDoc[0].tags, testDoc[1].tags, testDoc[2].tags ) ) 72 | assert.deepEqual( merged.friends, [].concat( testDoc[0].friends, testDoc[1].friends, testDoc[2].friends ) ) 73 | 74 | // set up a route for GET and a route for POST, then test if a GET retreives 75 | // the same content that was sent with a POST 76 | global.apiRouter.post( "/api/test/newdoc", (req, res, next ) => 77 | { 78 | newDoc = req.body 79 | res.status( 200 ).send() 80 | }) 81 | global.apiRouter.get( "/api/test/olddoc", (req, res, next ) => 82 | { 83 | res.json( newDoc ) 84 | }) 85 | 86 | console.log( ">> request post" ) 87 | const postResponse = await applib.request( newDocOptions ) 88 | assert.equal( postResponse.status, 200 ) 89 | 90 | console.log( ">> request get" ) 91 | const getResponse = await applib.request( oldDocOptions ) 92 | assert.equal( getResponse.status, 200 ) 93 | assert.deepEqual( getResponse.data, testObj ) 94 | 95 | return true 96 | } 97 | 98 | async function testall() 99 | { 100 | try { 101 | await testallFunctions() 102 | } 103 | catch (error) { 104 | errorCount++ 105 | console.error( error ) 106 | } 107 | return errorCount 108 | } 109 | 110 | module.exports = { 111 | testall: testall 112 | } 113 | -------------------------------------------------------------------------------- /tests/test_fbstorage.js: -------------------------------------------------------------------------------- 1 | // test_fbstorage.js 2 | 'use strict'; 3 | 4 | const assert = require('assert').strict 5 | 6 | // module under test: 7 | const { fbstorage } = require('../electron-firebase') 8 | 9 | // specimens 10 | const jsonDoc = global.readFile( "./tests/generated.json" ) 11 | const testDoc = JSON.parse( jsonDoc ) 12 | const testObj = testDoc[0] 13 | 14 | var errorCount = 0 15 | 16 | const folderPath = "testFolder" 17 | const filePath = `${folderPath}/path_to_file.whatever` 18 | 19 | const makeFileSet = [ 20 | `${folderPath}/junk-one/temp-two/test-file-A.tmp`, 21 | `${folderPath}/junk-one/temp-two/test-file-B.tmp`, 22 | `${folderPath}/junk-one/temp-four/test-file-C.tmp`, 23 | `${folderPath}/junk-one/temp-six/test-file-D.tmp`, 24 | `${folderPath}/junk-two/temp-four/test-file-E.tmp`, 25 | `${folderPath}/junk-two/temp-five/test-file-F.tmp`, 26 | `${folderPath}/junk-two/temp-five/test-file-G.tmp`, 27 | `${folderPath}/junk-three/temp-six/test-file-H.tmp`, 28 | `${folderPath}/junk-three/temp-six/test-file-J.tmp` 29 | ] 30 | 31 | const checkFileList = [ 32 | [`${folderPath}/junk-one/temp-two`,2], 33 | [`${folderPath}/junk-one/temp-four`,1], 34 | [`${folderPath}/junk-one/temp-six`,1], 35 | [`${folderPath}/junk-two/temp-four`,1], 36 | [`${folderPath}/junk-two/temp-five`,2], 37 | [`${folderPath}/junk-three/temp-six`,2] 38 | ] 39 | 40 | async function testallFunctions( store ) 41 | { 42 | console.log( ">> upload" ) 43 | var uploadResult = await store.upload( filePath, testObj ) 44 | assert.equal( filePath, uploadResult.path ) 45 | 46 | console.log( ">> about" ) 47 | var aboutResult = await store.about( filePath ) 48 | assert.equal( uploadResult.docid, aboutResult.docid ) 49 | assert.equal( uploadResult.fullPath, aboutResult.fullPath ) 50 | assert.equal( uploadResult.size, aboutResult.size ) 51 | assert.equal( uploadResult.updated, aboutResult.updated ) 52 | 53 | console.log( ">> download" ) 54 | var fileContent = await store.download( filePath ) 55 | assert.deepEqual( testObj, fileContent ) 56 | 57 | console.log( ">> find" ) 58 | var foundFile = await store.find( filePath ) 59 | assert.equal( filePath, foundFile.path ) 60 | assert.equal( uploadResult.docid, foundFile.docid ) 61 | 62 | console.log( ">> update" ) 63 | var fileMeta = await store.update( filePath, { 64 | contentEncoding: 'gzip', 65 | contentType: 'text/html' 66 | } ) 67 | assert.equal( fileMeta.contentEncoding, 'gzip' ) 68 | assert.equal( fileMeta.contentType, 'text/html' ) 69 | 70 | 71 | console.log( ">> folders" ) 72 | for ( var k in makeFileSet ) { 73 | uploadResult = await store.upload( makeFileSet[k], testObj ) 74 | } 75 | var folderList = await store.folders( folderPath ) 76 | assert.equal( 7, folderList.length ) 77 | 78 | console.log( ">> list" ) 79 | for ( var j in checkFileList ) { 80 | var fileList = await store.list( checkFileList[j][0] ) 81 | assert.equal( fileList.length, checkFileList[j][1] ) 82 | } 83 | 84 | console.log( ">> delete" ) 85 | for ( var m in makeFileSet ) { 86 | await store.delete( makeFileSet[m] ) 87 | } 88 | folderList = await store.folders( folderPath ) 89 | assert.equal( 0, folderList.length ) 90 | var deleteResult = await store.delete( filePath ) 91 | assert( !deleteResult ) 92 | aboutResult = await store.about( filePath ) 93 | assert.ok( !aboutResult.exists ) 94 | 95 | return true 96 | } 97 | 98 | async function testall( domain ) 99 | { 100 | try { 101 | await testallFunctions( fbstorage[domain] ) 102 | } 103 | catch (error) { 104 | errorCount++ 105 | console.error( error ) 106 | } 107 | return errorCount 108 | } 109 | 110 | module.exports = { 111 | testall: testall 112 | } 113 | -------------------------------------------------------------------------------- /tests/test_fileutils.js: -------------------------------------------------------------------------------- 1 | // test_fileutils.js 2 | 'use strict'; 3 | 4 | const assert = require('assert').strict 5 | 6 | // module under test: 7 | const { file } = require('../electron-firebase') 8 | 9 | const writeDocPath = "./tests/temp-write-doc.json" 10 | 11 | // specimens 12 | const jsonDoc = global.readFile( global.testDocPath ) 13 | const testDoc = JSON.parse( jsonDoc ) 14 | const testObj = testDoc[0] 15 | 16 | var errorCount = 0 17 | 18 | async function testallFunctions() 19 | { 20 | // readFile( fileName ) 21 | console.log( ">> readFile" ) 22 | assert.equal( jsonDoc, file.readFile( global.testDocPath ) ) 23 | 24 | // writeFile( fileName, fileContent ) 25 | console.log( ">> writeFile" ) 26 | file.writeFile( writeDocPath, jsonDoc ) 27 | assert.equal( jsonDoc, file.readFile( writeDocPath ) ) 28 | 29 | // isFile( fileName ) 30 | console.log( ">> isFile" ) 31 | assert( file.isFile( global.testDocPath ) ) 32 | assert( !file.isFile( global.testDocPath + "BAD_STUFF" ) ) 33 | 34 | // isFolder( folderName ) 35 | console.log( ">> isFolder" ) 36 | const testFolder = global.testDocPath.split("/").slice(0,-1).join("/") 37 | const newFolder = testFolder + "/newFolder" 38 | assert( file.isFolder( testFolder ) ) 39 | assert( !file.isFolder( newFolder ) ) 40 | 41 | // makeFolder( folderName ) 42 | console.log( ">> makeFolder" ) 43 | assert( file.makeFolder( newFolder ) ) 44 | assert( file.isFolder( newFolder ) ) 45 | 46 | // deleteFolder( folderName ) 47 | console.log( ">> deleteFolder" ) 48 | assert( file.deleteFolder( newFolder ) ) 49 | assert( !file.isFolder( newFolder ) ) 50 | 51 | // listFolders( folderName ) 52 | const showFolder = "." 53 | console.log( ">> listFolders" ) 54 | const folderList = file.listFolders( showFolder ) 55 | assert( 0 <= folderList.indexOf( 'lib' ) ) 56 | assert( 0 <= folderList.indexOf( 'pages' ) ) 57 | assert( 0 <= folderList.indexOf( 'node_modules' ) ) 58 | assert( 0 <= folderList.indexOf( 'tests' ) ) 59 | assert( -1 == folderList.indexOf( 'is-not-there' ) ) 60 | 61 | // listFiles( folderName ) 62 | console.log( ">> listFiles" ) 63 | const fileList = file.listFiles( showFolder ) 64 | assert( 0 <= fileList.indexOf( 'LICENSE' ) ) 65 | assert( 0 <= fileList.indexOf( 'electron-firebase.js' ) ) 66 | assert( 0 <= fileList.indexOf( 'package.json' ) ) 67 | assert( 0 <= fileList.indexOf( 'README.md' ) ) 68 | assert( -1 == fileList.indexOf( 'is-not-there' ) ) 69 | 70 | // deleteFile( fileName ) 71 | console.log( ">> deleteFile" ) 72 | assert( file.isFile( writeDocPath ) ) 73 | file.deleteFile( writeDocPath ) 74 | assert( !file.isFile( writeDocPath ) ) 75 | 76 | // readJSON( fileName ) 77 | console.log( ">> readJSON" ) 78 | assert.deepEqual( testDoc, file.readJSON( global.testDocPath ) ) 79 | 80 | // writeJSON( fileName, fileContent ) 81 | console.log( ">> writeJSON" ) 82 | file.writeJSON( writeDocPath, testObj ) 83 | assert.deepEqual( testObj, file.readJSON( writeDocPath ) ) 84 | 85 | // updateJSON( fileName, updateObject ) 86 | console.log( ">> updateJSON" ) 87 | file.updateJSON( writeDocPath, { age: 59, company: "fictionco" } ) 88 | testObj.age = 59 89 | testObj.company = "fictionco" 90 | assert.deepEqual( testObj, file.readJSON( writeDocPath ) ) 91 | 92 | // checkCommand( commandString ) 93 | console.log( ">> checkCommand" ) 94 | 95 | assert( file.checkCommand( "mkdir" ) ) 96 | assert( !file.checkCommand( "doesNotExist" ) ) 97 | 98 | // cleanup 99 | file.deleteFile( writeDocPath ) 100 | assert( !file.isFile( writeDocPath ) ) 101 | 102 | return true 103 | } 104 | 105 | async function testall() 106 | { 107 | try { 108 | await testallFunctions() 109 | } 110 | catch (error) { 111 | errorCount++ 112 | console.error( error ) 113 | } 114 | return errorCount 115 | } 116 | 117 | module.exports = { 118 | testall: testall 119 | } 120 | 121 | -------------------------------------------------------------------------------- /tests/test_firestore.js: -------------------------------------------------------------------------------- 1 | // test_firestore.js 2 | 'use strict'; 3 | 4 | const assert = require('assert').strict 5 | 6 | // module under test: 7 | const { firestore } = require('../electron-firebase') 8 | 9 | const testMergeName = "height" 10 | const testMergeValue = "medium" 11 | const testUpdateBogusDoc = "testAlpha/does_not_exist" 12 | 13 | // specimens 14 | const jsonDoc = global.readFile( global.testDocPath ) 15 | const testDoc = JSON.parse( jsonDoc ) 16 | const testObj = testDoc[0] 17 | const mergeObj = {} 18 | mergeObj[testMergeName] = testMergeValue 19 | 20 | const testPath = "testAlpha/docOne.json" 21 | const testCollection = "testAlpha" 22 | const testDocSet = "testAlpha/docOne-" 23 | 24 | const insertTag = "insert-this-tag" 25 | const checkTag = "occaecat" 26 | const tagName = "tags" 27 | 28 | var errorCount = 0 29 | 30 | async function testallFunctions( data ) 31 | { 32 | console.log( ">> write" ) 33 | await data.write( testPath, testObj ) 34 | 35 | console.log( ">> about" ) 36 | var aboutResult = await data.about( testPath ) 37 | assert.ok( aboutResult.exists ) 38 | assert.equal( aboutResult.id, testPath.split("/").pop() ) 39 | assert.equal( aboutResult.get("guid"), testObj.guid ) 40 | 41 | console.log( ">> read" ) 42 | var readResult = await data.read( testPath ) 43 | assert.deepEqual( testObj, readResult ) 44 | 45 | console.log( ">> merge" ) 46 | assert.ok( !readResult[testMergeName] ) 47 | await data.merge( testPath, mergeObj ) 48 | readResult = await data.read( testPath ) 49 | assert.ok( aboutResult.exists ) 50 | assert.equal( readResult[testMergeName], testMergeValue ) 51 | assert.equal( aboutResult.get("guid"), testObj.guid ) 52 | 53 | console.log( ">> update" ) 54 | await data.write( testPath, testObj ) 55 | readResult = await data.read( testPath ) 56 | assert.ok( !readResult[testMergeName] ) 57 | await data.update( testPath, mergeObj ) 58 | readResult = await data.read( testPath ) 59 | assert.equal( readResult[testMergeName], testMergeValue ) 60 | assert.equal( aboutResult.get("guid"), testObj.guid ) 61 | await data.update( testUpdateBogusDoc, mergeObj ) 62 | aboutResult = await data.about( testUpdateBogusDoc ) 63 | assert.ok( !aboutResult.exists ) 64 | 65 | console.log( ">> field" ) 66 | var getField = await data.field( testPath, "guid" ) 67 | assert.equal( getField, testObj.guid ) 68 | getField = await data.field( testPath, "friends" ) 69 | assert.deepEqual( getField, testObj.friends ) 70 | getField = await data.field( testPath, "there-is-no-field" ) 71 | assert.ok( getField == undefined ) 72 | 73 | console.log( ">> union" ) 74 | assert.equal( 7, ( await data.field( testPath, tagName ) ).length ) 75 | await data.union( testPath, tagName, checkTag ) 76 | await data.union( testPath, tagName, checkTag ) 77 | await data.union( testPath, tagName, checkTag ) 78 | assert.equal( 7, ( await data.field( testPath, tagName ) ).length ) 79 | await data.union( testPath, tagName, insertTag ) 80 | await data.union( testPath, tagName, insertTag ) 81 | await data.union( testPath, tagName, insertTag ) 82 | assert.equal( 8, ( await data.field( testPath, tagName ) ).length ) 83 | 84 | console.log( ">> splice" ) 85 | await data.splice( testPath, tagName, "eu" ) 86 | assert.equal( 7, ( await data.field( testPath, tagName ) ).length ) 87 | 88 | console.log( ">> push" ) 89 | const endTag = "at-the-end" 90 | await data.push( testPath, tagName, endTag ) 91 | await data.push( testPath, tagName, endTag ) 92 | await data.push( testPath, tagName, endTag ) 93 | await data.push( testPath, tagName, endTag ) 94 | assert.equal( 11, ( await data.field( testPath, tagName ) ).length ) 95 | 96 | console.log( ">> pop" ) 97 | await data.pop( testPath, tagName ) 98 | await data.pop( testPath, tagName ) 99 | await data.pop( testPath, tagName ) 100 | var popped = await data.pop( testPath, tagName ) 101 | assert.equal( popped, endTag ) 102 | assert.equal( 7, ( await data.field( testPath, tagName ) ).length ) 103 | 104 | console.log( ">> delete" ) 105 | aboutResult = await data.about( testPath ) 106 | assert.ok( aboutResult.exists ) 107 | await data.delete( testPath ) 108 | aboutResult = await data.about( testPath ) 109 | assert.ok( !aboutResult.exists ) 110 | 111 | console.log( ">> query" ) 112 | const docSet = [] 113 | testDoc.forEach( async (doc,index) => { 114 | docSet[index] = testDocSet + index 115 | await data.write( docSet[index], doc ) 116 | }) 117 | var queryResult = await data.query( testCollection, "eyeColor", "blue" ) 118 | assert.equal( queryResult.size, 4 ) 119 | queryResult = await data.query( testCollection, "eyeColor", "red" ) 120 | assert.equal( queryResult.size, 0 ) 121 | queryResult = await data.query( testCollection, "age", 28, ">" ) 122 | assert.equal( queryResult.size, 2 ) 123 | docSet.forEach( async (docPath) => { 124 | await( data.delete( docPath ) ) 125 | }) 126 | 127 | return true 128 | } 129 | 130 | async function testall( domain ) 131 | { 132 | try { 133 | await testallFunctions( firestore[domain] ) 134 | } 135 | catch (error) { 136 | errorCount++ 137 | console.error( error ) 138 | } 139 | return errorCount 140 | } 141 | 142 | module.exports = { 143 | testall: testall 144 | } 145 | 146 | 147 | -------------------------------------------------------------------------------- /tests/test_localstorage.js: -------------------------------------------------------------------------------- 1 | // test_localstorage.js 2 | 'use strict'; 3 | 4 | const assert = require('assert').strict 5 | 6 | // module under test: 7 | const { local } = require('../electron-firebase') 8 | 9 | // specimens 10 | const testKey = "testKey" 11 | const testValue = { 12 | one: "first item", 13 | two: "second item" 14 | } 15 | 16 | var errorCount = 0 17 | 18 | async function testallFunctions() 19 | { 20 | // setItem( key, value ) 21 | console.log( ">> setItem" ) 22 | local.setItem( testKey, testValue ) 23 | 24 | console.log( ">> getItem" ) 25 | const getValue = await local.getItem( testKey ) 26 | assert.deepEqual( getValue, testValue ) 27 | 28 | // removeItem( key ) 29 | console.log( ">> removeItem" ) 30 | local.removeItem( testKey ) 31 | const removeValue = await local.getItem( testKey ) 32 | assert.equal( removeValue, null ) 33 | } 34 | 35 | async function testall() 36 | { 37 | try { 38 | await testallFunctions() 39 | } 40 | catch (error) { 41 | errorCount++ 42 | console.error( error ) 43 | } 44 | return errorCount 45 | } 46 | 47 | module.exports = { 48 | testall: testall 49 | } 50 | 51 | 52 | -------------------------------------------------------------------------------- /tests/testpage_local.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Test Webpage - LocalStorage 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | header goes here... 15 |
16 | 17 |
18 |
19 |

TEST PAGE

20 |

for electron-firebase

21 |
22 |
23 | 24 | 27 | 28 | 29 | 30 | --------------------------------------------------------------------------------