diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts index e7497a69287..a6a47235d12 100644 --- a/packages/server/src/app.ts +++ b/packages/server/src/app.ts @@ -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 = { +const defaultEnvVariables: Record = { 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); diff --git a/packages/server/src/config.ts b/packages/server/src/config.ts index 00c295565a9..90a48765aa0 100644 --- a/packages/server/src/config.ts +++ b/packages/server/src/config.ts @@ -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, }; } diff --git a/packages/server/src/env.ts b/packages/server/src/env.ts new file mode 100644 index 00000000000..f8386f6d210 --- /dev/null +++ b/packages/server/src/env.ts @@ -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; +} diff --git a/packages/server/src/utils/testing/testUtils.ts b/packages/server/src/utils/testing/testUtils.ts index 203daeb2b36..94c5ff3a181 100644 --- a/packages/server/src/utils/testing/testUtils.ts +++ b/packages/server/src/utils/testing/testUtils.ts @@ -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, });