BookBrainz/bookbrainz-site

View on GitHub
src/server/app.js

Summary

Maintainability
B
6 hrs
Test Coverage
/*
 * Copyright (C) 2014-2015  Ben Ockmore
 *               2015-2017  Sean Burke
 *               2015       Leo Verto
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */
/* eslint-disable node/no-process-env */

import * as auth from './helpers/auth';
import * as error from '../common/helpers/error';
import * as search from '../common/helpers/search';
import * as serverErrorHelper from './helpers/error';

import {repositoryUrl, siteRevision, userAgent} from './info';

import BookBrainzData from 'bookbrainz-data';
import Debug from 'debug';
import appCleanup from '../common/helpers/appCleanup';
import compression from 'compression';
import config from '../common/helpers/config';
import express from 'express';
import favicon from 'serve-favicon';
import initInflux from './influx';
import logNode from 'log-node';
import logger from 'morgan';
import path from 'path';
import routes from './routes';
import serveStatic from 'serve-static';
import session from '../common/helpers/session';


// Initialize log-to-stdout  writer
logNode({
    env: {
        LOG_LEVEL: config.site.log
    }
});
const debug = Debug('bbsite');

// Initialize application
const app = express();
app.locals.orm = BookBrainzData(config.database);

const rootDir = path.join(__dirname, '../../');

app.set('trust proxy', config.site.proxyTrust);

app.use(favicon(path.join(rootDir, 'static/images/icons/favicon.ico')));

if (app.get('env') !== 'testing') {
    app.use(logger('dev'));
}

app.use(express.json({limit: '10mb'}));
app.use(express.urlencoded({
    extended: false
}));
app.use(compression());

// Set up serving of static assets
if (process.env.NODE_ENV === 'development') {
    /* eslint-disable node/global-require, node/no-unpublished-require, @typescript-eslint/no-var-requires */
    const webpack = require('webpack');
    const webpackDevMiddleware = require('webpack-dev-middleware');
    const webpackHotMiddleware = require('webpack-hot-middleware');

    // eslint-disable-next-line import/no-dynamic-require
    const webpackConfig = require(path.resolve(rootDir, './webpack.client'));
    /* eslint-enable node/global-require, node/no-unpublished-require, @typescript-eslint/no-var-requires */
    const compiler = webpack(webpackConfig);

    app.use(webpackDevMiddleware(compiler));
    app.use(webpackHotMiddleware(compiler));
}
else {
    app.use(serveStatic(path.join(rootDir, 'static/js')));
}
app.use(express.static(path.join(rootDir, 'static')));

app.use(session(process.env.NODE_ENV));


if (config.influx) {
    initInflux(app, config);
}

// Authentication code depends on session, so init session first
const authInitiated = auth.init(app);
let searchInitiated;

// Clone search config to prevent error if starting webserver and api
// https://github.com/elastic/elasticsearch-js/issues/33
(async function initializeSearch() {
    searchInitiated = await search.init(app.locals.orm, Object.assign({}, config.search));
})();

// Set up constants that will remain valid for the life of the app
debug(`Git revision: ${siteRevision}`);
debug('User-Agent:', userAgent);

app.use((req, res, next) => {
    // Set up globally-used properties
    res.locals.siteRevision = siteRevision;
    res.locals.repositoryUrl = repositoryUrl;
    res.locals.alerts = [];
    req.signUpDisabled = false;
    if (process.env.DEPLOY_ENV === 'test' || process.env.DEPLOY_ENV === 'beta') {
        let msg;
        if (process.env.DEPLOY_ENV === 'beta') {
            msg = 'You are on the beta website, which uses the main database but with a newer version of the code to test new features.';
        }
        else {
            msg = 'You are on the test website; all changes made here are not synced with the main database and will be overwritten periodically.';
        }
        res.locals.alerts.push({
            level: 'info',
            message: `${msg}`
        });
    }

    if (!searchInitiated) {
        const msg = 'We could not connect to our search server, all search functionality is unavailable.';
        res.locals.alerts.push({
            level: 'danger',
            message: `${msg}`
        });
    }

    if (!req.session || !authInitiated) {
        res.locals.alerts.push({
            level: 'danger',
            message: `We are currently experiencing technical difficulties;
                signing in and signing up are disabled until this is resolved.`
        });
        req.signUpDisabled = true;
    }

    // Add user data to every rendered route
    res.locals.user = req.user;

    next();
});

// Set up routes
routes(app);

// Catch 404 and forward to error handler
app.use((req, res, next) => {
    next(new error.NotFoundError(null, req));
});

// Error handler; arity MUST be 4 or express doesn't treat it as such
// eslint-disable-next-line @typescript-eslint/no-unused-vars
app.use((err, req, res, next) => {
    serverErrorHelper.renderError(req, res, err);
});


const DEFAULT_PORT = 9099;
app.set('port', process.env.PORT || DEFAULT_PORT); // eslint-disable-line no-process-env

const server = app.listen(app.get('port'), () => {
    debug(`Express server listening on port ${server.address().port}`);
});

function cleanupFunction() {
    return new Promise((resolve, reject) => {
        debug('Cleaning up before closing');
        server.close((err) => {
            if (err) {
                debug('Error while closing server connections');
                reject(err);
            }
            else {
                debug('Closed all server connections. Bye bye!');
                resolve();
            }
        });
        // force-kill after X milliseconds.
        if (config.site.forceExitAfterMs) {
            setTimeout(() => {
                reject(new Error(`Cleanup function timed out after ${config.site.forceExitAfterMs} ms`));
            }, config.site.forceExitAfterMs);
        }
    });
}

// Run cleanup function
if (process.env.NODE_ENV !== 'test') {
    appCleanup(cleanupFunction);
}

export default server;