dbmedialab/reader-critics

View on GitHub
src/app/config.ts

Summary

Maintainability
A
0 mins
Test Coverage
//
// LESERKRITIKK v2 (aka Reader Critics)
// Copyright (C) 2017 DB Medialab/Aller Media AS, Oslo, Norway
// https://github.com/dbmedialab/reader-critics/
//
// 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 3 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, see <http://www.gnu.org/licenses/>.
//

// tslint:disable:max-line-length max-file-line-count

import * as cluster from 'cluster';
import * as fs from 'fs';
import * as path from 'path';
import * as convict from 'convict';

import printEnvironment from 'print-env';

import {
    dbMessageQueue,
    dbSessionCache,
} from 'app/db/Redis';

import { rootPath } from 'app/util/applib';

import * as app from 'app/util/applib';

const log = app.createLog('config');
const isDumpEnabled = cluster.isMaster && app.isDevelop;

const isHexSecret = (val : any) => /^[a-fA-F0-9]{64}$/.test(val);

if (isDumpEnabled) {
    printEnvironment(log);
}

convict.addFormats({
    'hex-secret': {
        validate: (value) => {
            if (!isHexSecret(value)) {
                throw new Error('Must be a 64 character hex key');
            }
        },
    },
    'string-or-empty': {
        validate: (value) => {
            if (!(
                ((typeof value === 'string'))
                || value === null
                || value === undefined
            )) {
                throw new Error('Must be a string value or empty (null/undefined)');
            }
        },
        coerce: (value) => ((typeof value === 'string') && value.length > 0)
            ? value
            : undefined,
    },
});

const config = convict({
    analytics: {
        google: {
            trackingID: {
                default: undefined,
                format: 'string-or-empty',
                doc: 'Set this to your GA tracking ID to activate it in all page templates',
                env: 'ANALYTICS_GOOGLE_TRACKING_ID',
            },
        },
    },
    auth: {
        bcrypt: {
            rounds: {
                doc: 'Number of salt rounds when hashing passwords with BCrypt',
                // BCrypt documentation recommends chosing the number of salt rounds so
                // that the salting process takes about one second on the target machine.
                // The default of 14 rounds takes ~900ms on a Skylake E5v3 Xeon (tested)
                default: 14,
                env: 'AUTH_BCRYPT_ROUNDS',
            },
        },
        jwt: {
            secret: {
                default: null,
                doc: '64 character hexadecimal random sequence for signing JSON web tokens',
                format: 'hex-secret',
                env: 'AUTH_JWT_SECRET',
            },
            session: {
                default: true,
                format: Boolean,
                env: 'AUTH_JWT_SESSION',
            },
        },
        session: {
            secret: {
                default: null,
                doc: '64 character hexadecimal random sequence for signing session tokens',
                format: 'hex-secret',
                env: 'AUTH_SESSION_SECRET',
            },
            ttl: {
                default: 30,
                doc: 'Lifetime of a session in minutes',
                format: 'nat',
                env: 'AUTH_SESSION_TTL',
            },
        },
    },
    db: {
        mongo: {
            url: {
                doc: 'MongoDB connection URL for the main backend database',
                format: String,
                default: 'mongodb://localhost:27017/readercritics',
                env: 'MONGODB_URL',
            },
            reconnectionLimit: {
                default: 5,
                format: Number,
                doc: 'Amount of retries for connecting to MongoDB',
                env: 'MONGODB_RECONNECTION_LIMIT',
            },
        },
        redis: {
            url: {
                // See app/db/createRedisConnection for details about the constants
                [dbMessageQueue]: {
                    doc: 'Redis URL for the database that holds the message queue',
                    format: String,
                    default: null,
                    env: 'REDIS_URL_MESSAGE_QUEUE',
                },
                [dbSessionCache]: {
                    doc: 'Redis URL for the database that holds the session cache',
                    format: String,
                    default: null,
                    env: 'REDIS_URL_SESSION_CACHE',
                },
            },
        },
    },
    escalateThreshold: {
        defaults: {
            toEditor: {
                doc: 'Default number of feedbacks that an article can receive before being escalated to the editor',
                format: Number,
                default: 5,
                env: 'ESCALATE_THRESHOLD_DEFAULT_TO_EDITOR',
            },
        },
    },
    http: {
        port: {
            doc: 'Network port where the HTTP server is going to listen',
            format: 'port',
            default: 4000,
            env: 'HTTP_PORT',
        },
    },
    localization: {
        systemLocale: {
            doc: 'Default system locale that will be used when websites do not override',
            format: String,
            default: 'en',
            env: 'I18N_SYS_LOCALE',
        },
    },
    mail: {
        sender: {
            domain: {
                doc: 'Default domain to use for sending out e-mails',
                default: 'readercritics.com',
                format: String,
                env: 'MAIL_SENDER_DOMAIN',
            },
        },
        sendgrid: {
            api_key: {
                default: undefined,
                format: 'string-or-empty',
                doc: 'API key for SendGrid mail service',
                env: 'SENDGRID_API_KEY',
            },
        },
        bccRecipient: {
            default: undefined,
            format: 'string-or-empty',
            doc: 'Set this to a valid e-mail address to BCC all outgoing mail to it.',
            env: 'MAIL_BCC_RECIPIENT',
        },
        testOverride: {
            default: undefined,
            format: 'string-or-empty',
            doc: 'Set this to a valid e-mail address to direct ALL outgoing mail to it. Automatically disabled in production mode.',
            env: 'MAIL_TEST_OVERRIDE',
        },
    },
    recaptcha: {
        key: {
            secret: {
                default: undefined,
                doc: 'Secret Google Recaptcha key',
                format: 'string-or-empty',
                env: 'RECAPTCHA_SECRET_KEY',
            },
            public: {
                default: undefined,
                doc: 'Public Google Recaptcha key',
                format: 'string-or-empty',
                env: 'RECAPTCHA_PUBLIC_KEY',
            },
        },
    },
    slack: {
        channel: {
            default: undefined,
            format: 'string-or-empty',
            doc: 'Channel name for the Slack integration to use for notifications. Overrides the Webhook configuration on the receiver.',
            env: 'SLACK_CHANNEL',
        },
        botname: {
            default: 'Reader Critics',
            format: 'string-or-empty',
            doc: 'Bot display name for the Slack integration.',
            env: 'SLACK_BOTNAME',
        },
        webhook: {
            default: undefined,
            format: 'string-or-empty',
            doc: 'If set to a Slack webhook URL, warnings and errors will be posted to this integration',
            env: 'SLACK_WEBHOOK',
        },
    },
});

{
    const configFile = path.join(rootPath, 'config.json5');

    if (fs.existsSync(configFile)) {
        try {
            config.loadFile(configFile);
        }
        catch (error) {
            log('Error while trying to load %s', configFile);
            log(error);
        }
    }
}

try {
    config.validate();

    if (config.get('auth.jwt.secret') === config.get('auth.session.secret')) {
        throw new Error('JWT and session secret are identical');
    }

    if (isDumpEnabled) {
        log(app.inspect(config.getProperties()));
    }
}
catch (error) {
    log('Configuration error:', error.message);
    process.exit(128);
}

export default config;