Skip to content

Commit

Permalink
Server: Improved env variable handling to make it self documenting an…
Browse files Browse the repository at this point in the history
…d enforce type checking
  • Loading branch information
laurent22 committed Nov 2, 2021
1 parent 3704413 commit b5d792c
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 116 deletions.
11 changes: 5 additions & 6 deletions packages/server/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ require('source-map-support').install();
import * as Koa from 'koa';
import * as fs from 'fs-extra';
import Logger, { LoggerWrapper, TargetType } from '@joplin/lib/Logger';
import config, { initConfig, runningInDocker, EnvVariables } from './config';
import config, { initConfig, runningInDocker } from './config';
import { migrateLatest, waitForConnection, sqliteDefaultDir } from './db';
import { AppContext, Env, KoaNext } from './utils/types';
import FsDriverNode from '@joplin/lib/fs-driver-node';
Expand All @@ -20,6 +20,7 @@ import clickJackingHandler from './middleware/clickJackingHandler';
import newModelFactory from './models/factory';
import setupCommands from './utils/setupCommands';
import { RouteResponseFormat, routeResponseFormat } from './utils/routeUtils';
import { parseEnv } from './env';

interface Argv {
env?: Env;
Expand All @@ -33,7 +34,7 @@ const nodeEnvFile = require('node-env-file');
const { shimInit } = require('@joplin/lib/shim-init-node.js');
shimInit({ nodeSqlite });

const defaultEnvVariables: Record<Env, EnvVariables> = {
const defaultEnvVariables: Record<Env, any> = {
dev: {
// To test with the Postgres database, uncomment DB_CLIENT below and
// comment out SQLITE_DATABASE. Then start the Postgres server using
Expand Down Expand Up @@ -95,10 +96,7 @@ async function main() {

if (!defaultEnvVariables[env]) throw new Error(`Invalid env: ${env}`);

const envVariables: EnvVariables = {
...defaultEnvVariables[env],
...process.env,
};
const envVariables = parseEnv(process.env, defaultEnvVariables[env]);

const app = new Koa();

Expand Down Expand Up @@ -254,6 +252,7 @@ async function main() {
appLogger().info('User content base URL:', config().userContentBaseUrl);
appLogger().info('Log dir:', config().logDir);
appLogger().info('DB Config:', markPasswords(config().database));
appLogger().info('Mailer Config:', markPasswords(config().mailer));

appLogger().info('Trying to connect to database...');
const connectionCheck = await waitForConnection(config().database);
Expand Down
137 changes: 28 additions & 109 deletions packages/server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,101 +2,20 @@ import { rtrimSlashes } from '@joplin/lib/path-utils';
import { Config, DatabaseConfig, DatabaseConfigClient, Env, MailerConfig, RouteType, StripeConfig } from './utils/types';
import * as pathUtils from 'path';
import { loadStripeConfig, StripePublicConfig } from '@joplin/lib/utils/joplinCloud';
import { EnvVariables } from './env';

interface PackageJson {
version: string;
}

const packageJson: PackageJson = require(`${__dirname}/packageInfo.js`);

export interface EnvVariables {
// ==================================================
// General config
// ==================================================

APP_NAME?: string;
APP_PORT?: string;
SIGNUP_ENABLED?: string;
TERMS_ENABLED?: string;
ACCOUNT_TYPES_ENABLED?: string;
ERROR_STACK_TRACES?: string;
COOKIES_SECURE?: string;
RUNNING_IN_DOCKER?: string;

// ==================================================
// URL config
// ==================================================

APP_BASE_URL?: string;
USER_CONTENT_BASE_URL?: string;
API_BASE_URL?: string;
JOPLINAPP_BASE_URL?: string;

// ==================================================
// Database config
// ==================================================

DB_CLIENT?: string;
DB_SLOW_QUERY_LOG_ENABLED?: string;
DB_SLOW_QUERY_LOG_MIN_DURATION?: string; // ms
DB_AUTO_MIGRATION?: string;

POSTGRES_PASSWORD?: string;
POSTGRES_DATABASE?: string;
POSTGRES_USER?: string;
POSTGRES_HOST?: string;
POSTGRES_PORT?: string;

// This must be the full path to the database file
SQLITE_DATABASE?: string;

// ==================================================
// Mailer config
// ==================================================

MAILER_ENABLED?: string;
MAILER_HOST?: string;
MAILER_PORT?: string;
MAILER_SECURE?: string;
MAILER_AUTH_USER?: string;
MAILER_AUTH_PASSWORD?: string;
MAILER_NOREPLY_NAME?: string;
MAILER_NOREPLY_EMAIL?: string;

SUPPORT_EMAIL?: string;
SUPPORT_NAME?: string;
BUSINESS_EMAIL?: string;

// ==================================================
// Stripe config
// ==================================================

STRIPE_SECRET_KEY?: string;
STRIPE_WEBHOOK_SECRET?: string;
}

let runningInDocker_: boolean = false;

export function runningInDocker(): boolean {
return runningInDocker_;
}

function envReadString(s: string, defaultValue: string = ''): string {
return s === undefined || s === null ? defaultValue : s;
}

function envReadBool(s: string, defaultValue = false): boolean {
if (s === undefined || s === null) return defaultValue;
return s === '1';
}

function envReadInt(s: string, defaultValue: number = null): number {
if (!s) return defaultValue === null ? 0 : defaultValue;
const output = Number(s);
if (isNaN(output)) throw new Error(`Invalid number: ${s}`);
return output;
}

function databaseHostFromEnv(runningInDocker: boolean, env: EnvVariables): string {
if (env.POSTGRES_HOST) {
// When running within Docker, the app localhost is different from the
Expand All @@ -116,19 +35,19 @@ function databaseConfigFromEnv(runningInDocker: boolean, env: EnvVariables): Dat
const baseConfig: DatabaseConfig = {
client: DatabaseConfigClient.Null,
name: '',
slowQueryLogEnabled: envReadBool(env.DB_SLOW_QUERY_LOG_ENABLED),
slowQueryLogMinDuration: envReadInt(env.DB_SLOW_QUERY_LOG_MIN_DURATION, 10000),
autoMigration: envReadBool(env.DB_AUTO_MIGRATION, true),
slowQueryLogEnabled: env.DB_SLOW_QUERY_LOG_ENABLED,
slowQueryLogMinDuration: env.DB_SLOW_QUERY_LOG_MIN_DURATION,
autoMigration: env.DB_AUTO_MIGRATION,
};

if (env.DB_CLIENT === 'pg') {
return {
...baseConfig,
client: DatabaseConfigClient.PostgreSQL,
name: env.POSTGRES_DATABASE || 'joplin',
user: env.POSTGRES_USER || 'joplin',
password: env.POSTGRES_PASSWORD || 'joplin',
port: env.POSTGRES_PORT ? Number(env.POSTGRES_PORT) : 5432,
name: env.POSTGRES_DATABASE,
user: env.POSTGRES_USER,
password: env.POSTGRES_PASSWORD,
port: env.POSTGRES_PORT,
host: databaseHostFromEnv(runningInDocker, env) || 'localhost',
};
}
Expand All @@ -143,27 +62,27 @@ function databaseConfigFromEnv(runningInDocker: boolean, env: EnvVariables): Dat

function mailerConfigFromEnv(env: EnvVariables): MailerConfig {
return {
enabled: env.MAILER_ENABLED !== '0',
host: env.MAILER_HOST || '',
port: Number(env.MAILER_PORT || 587),
secure: !!Number(env.MAILER_SECURE) || true,
authUser: env.MAILER_AUTH_USER || '',
authPassword: env.MAILER_AUTH_PASSWORD || '',
noReplyName: env.MAILER_NOREPLY_NAME || '',
noReplyEmail: env.MAILER_NOREPLY_EMAIL || '',
enabled: env.MAILER_ENABLED,
host: env.MAILER_HOST,
port: env.MAILER_PORT,
secure: env.MAILER_SECURE,
authUser: env.MAILER_AUTH_USER,
authPassword: env.MAILER_AUTH_PASSWORD,
noReplyName: env.MAILER_NOREPLY_NAME,
noReplyEmail: env.MAILER_NOREPLY_EMAIL,
};
}

function stripeConfigFromEnv(publicConfig: StripePublicConfig, env: EnvVariables): StripeConfig {
return {
...publicConfig,
enabled: !!env.STRIPE_SECRET_KEY,
secretKey: env.STRIPE_SECRET_KEY || '',
webhookSecret: env.STRIPE_WEBHOOK_SECRET || '',
secretKey: env.STRIPE_SECRET_KEY,
webhookSecret: env.STRIPE_WEBHOOK_SECRET,
};
}

function baseUrlFromEnv(env: any, appPort: number): string {
function baseUrlFromEnv(env: EnvVariables, appPort: number): string {
if (env.APP_BASE_URL) {
return rtrimSlashes(env.APP_BASE_URL);
} else {
Expand All @@ -178,12 +97,12 @@ export async function initConfig(envType: Env, env: EnvVariables, overrides: any

const rootDir = pathUtils.dirname(__dirname);
const stripePublicConfig = loadStripeConfig(envType === Env.BuildTypes ? Env.Dev : envType, `${rootDir}/stripeConfig.json`);
const appName = env.APP_NAME || 'Joplin Server';
const appName = env.APP_NAME;
const viewDir = `${rootDir}/src/views`;
const appPort = env.APP_PORT ? Number(env.APP_PORT) : 22300;
const appPort = env.APP_PORT;
const baseUrl = baseUrlFromEnv(env, appPort);
const apiBaseUrl = env.API_BASE_URL ? env.API_BASE_URL : baseUrl;
const supportEmail = env.SUPPORT_EMAIL || 'SUPPORT_EMAIL'; // Defaults to "SUPPORT_EMAIL" so that server admin knows they have to set it.
const supportEmail = env.SUPPORT_EMAIL;

config_ = {
appVersion: packageJson.version,
Expand All @@ -200,17 +119,17 @@ export async function initConfig(envType: Env, env: EnvVariables, overrides: any
stripe: stripeConfigFromEnv(stripePublicConfig, env),
port: appPort,
baseUrl,
showErrorStackTraces: (env.ERROR_STACK_TRACES === undefined && envType === Env.Dev) || env.ERROR_STACK_TRACES === '1',
showErrorStackTraces: env.ERROR_STACK_TRACES,
apiBaseUrl,
userContentBaseUrl: env.USER_CONTENT_BASE_URL ? env.USER_CONTENT_BASE_URL : baseUrl,
joplinAppBaseUrl: envReadString(env.JOPLINAPP_BASE_URL, 'https://joplinapp.org'),
signupEnabled: env.SIGNUP_ENABLED === '1',
termsEnabled: env.TERMS_ENABLED === '1',
accountTypesEnabled: env.ACCOUNT_TYPES_ENABLED === '1',
joplinAppBaseUrl: env.JOPLINAPP_BASE_URL,
signupEnabled: env.SIGNUP_ENABLED,
termsEnabled: env.TERMS_ENABLED,
accountTypesEnabled: env.ACCOUNT_TYPES_ENABLED,
supportEmail,
supportName: env.SUPPORT_NAME || appName,
businessEmail: env.BUSINESS_EMAIL || supportEmail,
cookieSecure: env.COOKIES_SECURE === '1',
cookieSecure: env.COOKIES_SECURE,
...overrides,
};
}
Expand Down
138 changes: 138 additions & 0 deletions packages/server/src/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
export interface EnvVariables {
// ==================================================
// General config
// ==================================================

APP_NAME: string;
APP_PORT: number;
SIGNUP_ENABLED: boolean;
TERMS_ENABLED: boolean;
ACCOUNT_TYPES_ENABLED: boolean;
ERROR_STACK_TRACES: boolean;
COOKIES_SECURE: boolean;
RUNNING_IN_DOCKER: boolean;

// ==================================================
// URL config
// ==================================================

APP_BASE_URL: string;
USER_CONTENT_BASE_URL: string;
API_BASE_URL: string;
JOPLINAPP_BASE_URL: string;

// ==================================================
// Database config
// ==================================================

DB_CLIENT: string;
DB_SLOW_QUERY_LOG_ENABLED: boolean;
DB_SLOW_QUERY_LOG_MIN_DURATION: number;
DB_AUTO_MIGRATION: boolean;

POSTGRES_PASSWORD: string;
POSTGRES_DATABASE: string;
POSTGRES_USER: string;
POSTGRES_HOST: string;
POSTGRES_PORT: number;

// This must be the full path to the database file
SQLITE_DATABASE: string;

// ==================================================
// Mailer config
// ==================================================

MAILER_ENABLED: boolean;
MAILER_HOST: string;
MAILER_PORT: number;
MAILER_SECURE: boolean;
MAILER_AUTH_USER: string;
MAILER_AUTH_PASSWORD: string;
MAILER_NOREPLY_NAME: string;
MAILER_NOREPLY_EMAIL: string;

SUPPORT_EMAIL: string;
SUPPORT_NAME: string;
BUSINESS_EMAIL: string;

// ==================================================
// Stripe config
// ==================================================

STRIPE_SECRET_KEY: string;
STRIPE_WEBHOOK_SECRET: string;
}

const defaultEnvValues: EnvVariables = {
APP_NAME: 'Joplin Server',
APP_PORT: 22300,
SIGNUP_ENABLED: false,
TERMS_ENABLED: false,
ACCOUNT_TYPES_ENABLED: false,
ERROR_STACK_TRACES: false,
COOKIES_SECURE: false,
RUNNING_IN_DOCKER: false,

APP_BASE_URL: '',
USER_CONTENT_BASE_URL: '',
API_BASE_URL: '',
JOPLINAPP_BASE_URL: 'https://joplinapp.org',

DB_CLIENT: 'sqlite3',
DB_SLOW_QUERY_LOG_ENABLED: false,
DB_SLOW_QUERY_LOG_MIN_DURATION: 1000,
DB_AUTO_MIGRATION: true,

POSTGRES_PASSWORD: 'joplin',
POSTGRES_DATABASE: 'joplin',
POSTGRES_USER: 'joplin',
POSTGRES_HOST: '',
POSTGRES_PORT: 5432,

SQLITE_DATABASE: '',

MAILER_ENABLED: false,
MAILER_HOST: '',
MAILER_PORT: 587,
MAILER_SECURE: true,
MAILER_AUTH_USER: '',
MAILER_AUTH_PASSWORD: '',
MAILER_NOREPLY_NAME: '',
MAILER_NOREPLY_EMAIL: '',

SUPPORT_EMAIL: 'SUPPORT_EMAIL', // Defaults to "SUPPORT_EMAIL" so that server admin knows they have to set it.
SUPPORT_NAME: '',
BUSINESS_EMAIL: '',

STRIPE_SECRET_KEY: '',
STRIPE_WEBHOOK_SECRET: '',
};

export function parseEnv(rawEnv: any, defaultOverrides: any = null): EnvVariables {
const output: EnvVariables = {
...defaultEnvValues,
...defaultOverrides,
};

for (const [key, value] of Object.entries(defaultEnvValues)) {
const rawEnvValue = rawEnv[key];

if (rawEnvValue === undefined) continue;

if (typeof value === 'number') {
const v = Number(rawEnvValue);
if (isNaN(v)) throw new Error(`Invalid number value for env variable ${key} = ${rawEnvValue}`);
(output as any)[key] = v;
} else if (typeof value === 'boolean') {
if (rawEnvValue !== '0' && rawEnvValue !== '1') throw new Error(`Invalid boolean for for env variable ${key}: ${rawEnvValue}`);
(output as any)[key] = rawEnvValue === '1';
} else if (typeof value === 'string') {
(output as any)[key] = `${rawEnvValue}`;
} else {
throw new Error(`Invalid env default value type: ${typeof value}`);
}
}

return output;
}
Loading

0 comments on commit b5d792c

Please sign in to comment.