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:
parent
370441333f
commit
b5d792c606
@ -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);
|
||||
|
@ -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
138
packages/server/src/env.ts
Normal 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;
|
||||
}
|
@ -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,
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user