1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-27 08:21:03 +02:00

Server: Improved env variable handling to make it self documenting and enforce type checking

This commit is contained in:
Laurent Cozic 2021-11-02 12:51:59 +00:00
parent 370441333f
commit b5d792c606
4 changed files with 172 additions and 116 deletions

View File

@ -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';
@ -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;
@ -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
@ -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();
@ -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);

View File

@ -2,6 +2,7 @@ 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;
@ -9,94 +10,12 @@ interface PackageJson {
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
@ -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',
};
}
@ -143,14 +62,14 @@ 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,
};
}
@ -158,12 +77,12 @@ function stripeConfigFromEnv(publicConfig: StripePublicConfig, env: EnvVariables
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 {
@ -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,
@ -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,
};
}

138
packages/server/src/env.ts Normal file
View File

@ -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;
}

View File

@ -86,7 +86,7 @@ export async function beforeAllDb(unitName: string, createDbOptions: CreateDbOpt
await initConfig(Env.Dev, {
SQLITE_DATABASE: createdDbPath_,
SUPPORT_EMAIL: 'testing@localhost',
}, {
} as any, {
tempDir: tempDir,
});