libflitter/express/ExpressUnit.js

/**
 * @module libflitter/express/ExpressUnit
 */

const uuid = require('uuid/v4')
const Unit = require('../Unit')
const session = require('express-session')
const MongoDBStore = require('connect-mongodb-session')(session)
const path = require('path')
const fs = require('fs')
const http = require('http')
const https = require('https')

/**
 * The Express unit is responsible for injecting the 3rd-party tools
 * that Flitter makes available into the underlying Express framework
 * so they can be used in lower contexts. Currently, that includes the
 * body parser and session store.
 * @extends module:libflitter/Unit~Unit
 */
class ExpressUnit extends Unit {
    /**
     * Defines the services required by this unit.
     * @returns {Array<string>}
     */
    static get services() {
        return [...super.services, 'configs', 'output', 'database']
    }

    /**
     * Gets the name of the service provided by this unit: 'express'
     * @returns {string} - 'express'
     */
    static get name() {
        return 'express'
    }

    /**
     * If true, the application has been provided an SSL certificate and key.
     * @returns {boolean}
     */
    use_ssl() {
        return !!this.configs.get('server.ssl.enable')
    }

    /**
     * Gets the contents of the configured SSL certificate.
     * @returns {Promise<string|undefined>}
     */
    async ssl_certificate() {
        if ( this.configs.get('server.ssl.enable') )
            return fs.promises.readFile(this.configs.get('server.ssl.cert_file'), 'utf8')
    }

    /**
     * Gets the contents of the configured SSL key.
     * @returns {Promise<string|undefined>}
     */
    async ssl_key() {
        if ( this.configs.get('server.ssl.enable') )
            return fs.promises.readFile(this.configs.get('server.ssl.key_file'), 'utf8')
    }

    /**
     * Loads the unit. Registers the 'busboy-body-parser' and 'express-session' packages with the underlying Express app.
     * @param {module:libflitter/app/FlitterApp~FlitterApp} app - the Flitter app
     * @returns {Promise<void>}
     */
    async go(app){

        /*
         * Use a custom HTTP/S server, instead of the built-in
         * Express server. This allows for application-provided
         * SSL, as well as advanced customizations.
         */
        let server
        if ( this.configs.get('server.ssl.enable') ){
            this.output.info(`Will attempt to use SSL server.`)
            server = https.createServer({
                cert: fs.readFileSync(this.configs.get('server.ssl.cert_file'), 'utf8'),
                key: fs.readFileSync(this.configs.get('server.ssl.key_file'), 'utf8'),
            }, app.express)
        }
        else server = http.createServer(app.express)

        this.server = server

        const allow_uploads = this.configs.get('server.uploads.enable')
        const allowed_path = this.configs.get('server.uploads.allowed_path')
        const destination = this.configs.get('server.uploads.destination')

        /*
         * Load the body parser into the underlying Express app.
         * This is done here do that the body is parsed before any
         * of the middleware/controllers/routing/etc.
         */
        require('express-busboy').extend(app.express, {
            upload: !!allow_uploads,
            allowedPath: (allowed_path ? allowed_path : /./),
            path: destination,
        })

        let store
        if ( this.database && this.database.status() === Unit.STATUS_RUNNING ) {

            /*
             * Set up the session store if we have the DB.
             * Defaults to memory if not.
             */
            store = new MongoDBStore({
                uri: this.database.connect_string,
                collection: 'flitter_sessions'
            })
        } else {
            this.output.warn(`No database found. Using in-memory session driver. This can cause memory issues, and sessions will be lost on restart.`)
        }

        const session_max_age = this.configs.get('server.session.max_age')

        app.express.use(session({
            genid: () => {
                return uuid() // use UUIDs for session IDs
            },
            secret: this.configs.get('server.session.secret'),
            resave: true,
            saveUninitialized: true,
            store: (store ? store : null),
            cookie: {
                ...(session_max_age ? { maxAge: session_max_age } : {})
            }
        }))

        this.session_store = store
    }

    /**
     * Closes the session store's DB connection.
     * @param {module:libflitter/app/FlitterApp~FlitterApp} app
     * @returns {Promise<void>}
     */
    async cleanup(app){
        if ( this.session_store && this.session_store.client && this.session_store.client.close ) {
            this.output.info(`Closing session storage client.`)
            await this.session_store.client.close()
        }
    }

    /**
     * Get the fully-qualified path to the migrations provided by this unit.
     * @returns {string}
     */
    migrations(){
        return path.resolve(__dirname, 'migrations')
    }

}

module.exports = exports = ExpressUnit