app/index.js

Summary

Maintainability
A
0 mins
Test Coverage
'use strict'
/**
 * index.js
 * Entry point for app execution
 ******************************/

const { app, dialog, BrowserWindow } = require('electron'),
  { openNewGitHubIssue, debugInfo } = require('electron-util'),
  debug = require('electron-debug'),
  unhandled = require('electron-unhandled')

unhandled({
  reportButton: error => {
    openNewGitHubIssue({
      user: 'HR',
      repo: 'Crypter',
      body: `\`\`\`\n${error.stack}\n\`\`\`\n\n---\n\n${debugInfo()}`
    })
  }
})
debug()
app.setAppUserModelId('com.github.hr.crypter')

// MasterPass credentials global
global.creds = {}
// User settings global
global.settings = {}
// Paths global (only resolved at runtime)
global.paths = {
  mdb: `${app.getPath('userData')}/mdb`,
  userData: app.getPath('userData'),
  home: app.getPath('home'),
  documents: app.getPath('documents')
}

let fileToCrypt
let settingsWindowNotOpen = true

const logger = require('electron-log')
const { existsSync } = require('fs-extra')
const { checkUpdate } = require('./utils/update')
// Core
const Db = require('./core/Db')
const MasterPass = require('./core/MasterPass')
const MasterPassKey = require('./core/MasterPassKey')
// Windows
const crypter = require('./src/crypter')
const masterPassPrompt = require('./src/masterPassPrompt')
const setup = require('./src/setup')
const settings = require('./src/settings')
const { ERRORS } = require('./config')

// Debug info
logger.info(`Crypter v${app.getVersion()}`)
logger.info(`Process args: ${process.argv}`)
logger.info(`AppPath: ${app.getAppPath()}`)
logger.info(`UseData Path: ${app.getPath('userData')}`)
// Change exec path
process.chdir(app.getAppPath())
logger.info(`Changed cwd to: ${process.cwd()}`)
logger.info(`Electron v${process.versions.electron}`)
logger.info(`Electron node v${process.versions.node}`)
// Prevent second instance
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
  app.quit()
}

/**
 * Promisification of initialisation
 **/

const init = function () {
  return new Promise(function (resolve, reject) {
    // initialise mdb
    global.mdb = new Db(global.paths.mdb, function (mdb) {
      // Get the credentials serialized object from mdb
      resolve(mdb.get('creds'))
    })
  })
}

const initMain = function () {
  logger.verbose(`initialising Main...`)
  return global.mdb
    .restoreGlobalObj('creds')
    .then(() => MasterPass.init())
    .then(mpk => mpk && (global.MasterPassKey = new MasterPassKey(mpk.key)))
}

/**
 * Event handlers
 **/

// Main event handler
app.on('ready', function () {
  // Check for updates, silently
  checkUpdate().catch(err => {
    logger.warn(err)
  })
  // Check synchronously whether paths exist
  init()
    .then(mainRun => {
      logger.info(`Init done.`)
      // If the credentials not find in mdb, run setup
      // otherwise run main
      if (mainRun) {
        // Run main
        logger.info(`Main run. Creating CrypterWindow...`)

        // Initialise (open mdb and get creds)
        initMain()
          .then(mpLoaded => {
            logger.verbose(
              'INIT: MasterPass',
              mpLoaded ? 'loaded' : 'not saved'
            )
            // Obtain MasterPass, derive MasterPassKey and set globally
            return mpLoaded || createWindow(masterPassPrompt, false)
          })
          .then(() => {
            // Create the Crypter window and open it
            return createWindow(crypter, fileToCrypt)
          })
          .then(() => {
            // Quit app after crypterWindow is closed
            app.quit()
          })
          .catch(function (error) {
            if (error) {
              // Catch any fatal errors and exit
              logger.error(`PROMISE ERR: ${error.stack}`)
              dialog.showErrorBox(ERRORS.PROMISE, error.message)
            }
            app.quit()
          })
      } else {
        // Run Setup
        logger.info('Setup run. Creating Setup wizard...')

        createWindow(setup)
          .then(() => {
            logger.info('MAIN Setup successfully completed. quitting...')
            // setup successfully completed
            app.quit()
          })
          .catch(function (error) {
            logger.error(`PROMISE ERR: ${error.stack}`)
            // Display error to user
            dialog.showErrorBox(ERRORS.PROMISE, error.message)
            app.quit()
          })
      }
    })
    .catch(function (error) {
      logger.error(`PROMISE ERR: ${error.stack}`)
      // Display error to user
      dialog.showErrorBox(ERRORS.PROMISE, error.message)
      app.quit()
    })
})

/**
 * Electron events
 **/
app.on('will-finish-launching', () => {
  // Check if launched with a file (opened with app in macOS)
  app.on('open-file', (event, file) => {
    if (app.isReady() === false) {
      // Opening when not launched yet
      logger.info('Launching with open-file ' + file)
      fileToCrypt = file
    }
    event.preventDefault()
  })

  // Check if launched with a file (opened with app in Windows)
  if (
    process.argv[1] &&
    process.argv[1].length > 1 &&
    existsSync(process.argv[1])
  ) {
    fileToCrypt = process.argv[1]
  }
})

app.on('window-all-closed', () => {
  logger.verbose('APP: window-all-closed event emitted')
  // On macOS it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
})

app.on('quit', () => {
  logger.info('APP: quit event emitted')
  global.mdb.close().catch(err => {
    console.error(err)
    throw err
  })
})

app.on('will-quit', event => {
  // will exit program once exit procedures have been run
  logger.info(`APP.ON('will-quit'): will-quit event emitted`)
  global.mdb.close().catch(err => {
    console.error(err)
    throw err
  })
})

/**
 * Custom events
 **/

app.on('app:quit', () => {
  logger.verbose('APP: app:quit event emitted')
  app.quit()
})

app.on('app:open-settings', () => {
  logger.verbose('APP: app:open-settings event emitted')
  createWindow(settings)
})

app.on('app:check-update', () => {
  logger.verbose('APP: app:check-updates event emitted')
  // Check for updates
  checkUpdate()
    .then(updateAvailable => {
      if (!updateAvailable) {
        dialog.showMessageBox({
          type: 'info',
          message: 'No update available.',
          detail: `You have the latest version Crypter ${app.getVersion()} :)`
        })
      }
    })
    .catch(err => {
      logger.warn(err)
      dialog.showErrorBox(
        'Failed to check for update',
        `An error occured while checking for update:\n ${err.message}`
      )
    })
})

app.on('app:relaunch', () => {
  logger.verbose('APP: app:relaunch event emitted')
  // Relaunch Crypter
  app.relaunch()
  // Exit successfully
  app.quit(0)
  // app.exit(0)
})

app.on('app:reset-masterpass', () => {
  logger.verbose('APP: app:reset-masterpass event emitted')
  createWindow(masterPassPrompt)
})

/**
 * Promisification of windows
 **/

function createWindow (window, ...args) {
  const winInst = BrowserWindow.getAllWindows().find(
    win => win.getTitle() === window.title
  )
  // Focus on existing instance
  if (winInst) return winInst.focus()
  return new Promise((resolve, reject) =>
    window.window(global, ...args, err => {
      if (err) reject(err)
      resolve()
    })
  )
}