import chalk from 'chalk' // Load environment variables import dotenv from 'dotenv' import { asJson } from './utils/index.mjs' import { measurements } from '../../../config/measurements.mjs' dotenv.config() // Allow these 2 to be imported export const port = process.env.BACKEND_PORT || 3000 export const api = process.env.BACKEND_URL || `http://localhost:${port}` // All environment variables are strings // This is a helper method to turn them into a boolean const envToBool = (input = 'no') => { if (['yes', '1', 'true'].includes(input.toLowerCase())) return true return false } // Construct config object const config = { api, port, website: { domain: process.env.BACKEND_WEBSITE_DOMAIN || 'freesewing.org', scheme: process.env.BACKEND_WEBSITE_SCHEME || 'https', }, db: { url: process.env.BACKEND_DB_URL, }, tests: { allow: envToBool(process.env.BACKEND_TEST_ALLOW), domain: process.env.BACKEND_TEST_DOMAIN || 'freesewing.dev', sendEmail: envToBool(process.env.BACKEND_TEST_SEND_EMAIL), includeSanity: envToBool(process.env.BACKEND_TEST_SANITY), }, encryption: { key: process.env.BACKEND_ENC_KEY, }, jwt: { secretOrKey: process.env.BACKEND_ENC_KEY, issuer: process.env.BACKEND_JWT_ISSUER || 'freesewing.org', audience: process.env.BACKEND_JWT_ISSUER || 'freesewing.org', expiresIn: process.env.BACKEND_JWT_EXPIRY || '7d', }, apikeys: { levels: [0, 1, 2, 3, 4, 5, 6, 7, 8], expiryMaxSeconds: 365 * 24 * 3600, }, roles: { levels: { user: 4, bughunter: 5, support: 7, admin: 8, }, base: 'user', }, languages: ['en', 'de', 'es', 'fr', 'nl'], measies: measurements, aws: { ses: { region: process.env.BACKEND_AWS_SES_REGION || 'us-east-1', from: process.env.BACKEND_AWS_SES_FROM || 'FreeSewing ', replyTo: process.env.BACKEND_AWS_SES_REPLY_TO ? JSON.parse(process.env.BACKEND_AWS_SES_REPLY_TO) : ['FreeSewing '], feedback: process.env.BACKEND_AWS_SES_FEEDBACK, cc: process.env.BACKEND_AWS_SES_CC ? JSON.parse(process.env.BACKEND_AWS_SES_CC) : [], bcc: process.env.BACKEND_AWS_SES_BCC ? JSON.parse(process.env.BACKEND_AWS_SES_BCC) : ['FreeSewing records '], }, }, sanity: { use: process.env.BACKEND_USE_SANITY || false, project: process.env.SANITY_PROJECT, dataset: process.env.SANITY_DATASET || 'production', token: process.env.SANITY_TOKEN, version: process.env.SANITY_VERSION || 'v2022-10-31', api: `https://${process.env.SANITY_PROJECT || 'missing-project-id'}.api.sanity.io/${ process.env.SANITY_VERSION || 'v2022-10-31' }`, }, oauth: { github: { clientId: process.env.BACKEND_OAUTH_GITHUB_CLIENT_ID, clientSecret: process.env.BACKEND_OAUTH_GITHUB_CLIENT_SECRET, tokenUri: 'https://github.com/login/oauth/access_token', dataUri: 'https://api.github.com/user', emailUri: 'https://api.github.com/user/emails', }, google: { clientId: process.env.BACKEND_OAUTH_GOOGLE_CLIENT_ID, clientSecret: process.env.BACKEND_OAUTH_GOOGLE_CLIENT_SECRET, tokenUri: 'https://oauth2.googleapis.com/token', dataUri: 'https://people.googleapis.com/v1/people/me?personFields=emailAddresses,names,photos', }, }, github: { token: process.env.BACKEND_GITHUB_TOKEN, api: 'https://api.github.com', bot: { user: process.env.BACKEND_GITHUB_USER || 'freesewing-robot', name: process.env.BACKEND_GITHUB_USER_NAME || 'Freesewing bot', email: process.env.BACKEND_GITHUB_USER_EMAIL || 'bot@freesewing.org', }, notify: { specific: { albert: ['woutervdub'], bee: ['bobgeorgethe3rd'], benjamin: ['woutervdub'], cornelius: ['woutervdub'], diana: ['alfalyr'], holmes: ['alfalyr'], hortensia: ['woutervdub'], lunetius: ['starfetch'], penelope: ['woutervdub'], tiberius: ['starfetch'], sandy: ['alfalyr'], ursula: ['nataliasayang'], yuri: ['biou', 'hellgy'], walburga: ['starfetch'], waralee: ['woutervdub'], }, dflt: [process.env.BACKEND_GITHUB_NOTIFY_DEFAULT_USER || 'joostdecock'], }, }, } // Stand-alone config export const sanity = config.sanity export const website = config.website const vars = { BACKEND_DB_URL: 'required', BACKEND_PORT: 'optional', BACKEND_WEBSITE_DOMAIN: 'optional', BACKEND_WEBSITE_SCHEME: 'optional', BACKEND_ENC_KEY: 'requiredSecret', BACKEND_JWT_ISSUER: 'optional', BACKEND_JWT_EXPIRY: 'optional', // Feature flags BACKEND_ENABLE_AWS_SES: 'optional', BACKEND_ENABLE_SANITY: 'optional', BACKEND_ENABLE_GITHUB: 'optional', BACKEND_ENABLE_OAUTH_GITHUB: 'optional', BACKEND_ENABLE_OAUTH_GOOGLE: 'optional', BACKEND_ENABLE_TESTS: 'optional', BACKEND_ENABLE_DUMP_CONFIG_AT_STARTUP: 'optional', } // Vars for AWS SES integration if (envToBool(process.env.BACKEND_ENABLE_AWS_SES)) { vars.AWS_ACCESS_KEY_ID = 'required' vars.AWS_SECRET_ACCESS_KEY = 'requiredSecret' vars.BACKEND_AWS_SES_REGION = 'optional' vars.BACKEND_AWS_SES_FROM = 'optional' vars.BACKEND_AWS_SES_REPLY_TO = 'optional' vars.BACKEND_AWS_SES_FEEDBACK = 'optional' vars.BACKEND_AWS_SES_CC = 'optional' vars.BACKEND_AWS_SES_BCC = 'optional' } // Vars for Sanity integration if (envToBool(process.env.BACKEND_USE_SANITY)) { vars.SANITY_PROJECT = 'required' vars.SANITY_TOKEN = 'requiredSecret' vars.SANITY_VERSION = 'optional' vars.BACKEND_TEST_SANITY = 'optional' } // Vars for Github integration if (envToBool(process.env.BACKEND_ENABLE_GITHUB)) { vars.BACKEND_GITHUB_TOKEN = 'requiredSecret' vars.BACKEND_GITHUB_USER = 'required' vars.BACKEND_GITHUB_USER_NAME = 'required' vars.BACKEND_GITHUB_USER_EMAIL = 'required' vars.BACKEND_GITHUB_NOTIFY_DEFAULT_USER = 'required' } // Vars for Oauth via Github integration if (envToBool(process.env.BACKEND_ENABLE_OAUTH_GITHUB)) { vars.BACKEND_OAUTH_GITHUB_CLIENT_ID = 'required' vars.BACKEND_OAUTH_GITHUB_CLIENT_SECRET = 'requiredSecret' } // Vars for Oauth via Google integration if (envToBool(process.env.BACKEND_ENABLE_OAUTH_GOOGLE)) { vars.BACKEND_OAUTH_GOOGLE_CLIENT_ID = 'required' vars.BACKEND_OAUTH_GOOGLE_CLIENT_SECRET = 'requiredSecret' } // Vars for unit tests if (envToBool(process.env.BACKEND_ENABLE_TESTS)) { vars.BACKEND_TEST_DOMAIN = 'optional' vars.BACKEND_ENABLE_TESTS_EMAIL = 'optional' } /* * This method is how you load the config. * * It will verify whether whether everyting is setup correctly * which is not a given since there's a number of environment * variables that need to be set for this backend to function. */ export function verifyConfig(silent = false) { const emptyString = (input) => { if (typeof input === 'string' && input.length > 0) return false return true } const warnings = [] const errors = [] const ok = [] for (const [key, type] of Object.entries(vars)) { if (['required', 'requiredSecret'].includes(type)) { if (typeof process.env[key] === 'undefined' || emptyString(process.env[key])) errors.push(key) if (type === 'requiredSecret') ok.push(`🔒 ${chalk.yellow(key)}: ` + chalk.grey('***redacted***')) else ok.push(`✅ ${chalk.green(key)}: ${chalk.grey(process.env[key])}`) } else { if (typeof process.env[key] !== 'undefined' && !emptyString(process.env[key])) { ok.push(`✅ ${chalk.green(key)}: ${chalk.grey(process.env[key])}`) } } } if (!silent) { for (const o of ok) console.log(o) for (const e of errors) { console.log( chalk.redBright('Error:'), 'Required environment variable', chalk.redBright(e), "is missing. The backend won't start without it.", '\n', chalk.yellow('See: '), chalk.yellow.bold('https://freesewing.dev/reference/backend'), '\n' ) } } if (errors.length > 0) { console.log(chalk.redBright('Invalid configuration. Stopping here...')) return process.exit(1) } if (envToBool(process.env.BACKEND_ENABLE_DUMP_CONFIG_AT_STARTUP)) { console.log( chalk.cyan.bold('Dumping configuration:\n'), asJson( { ...config, encryption: { ...config.encryption, key: config.encryption.key.slice(0, 4) + '**redacted**' + config.encryption.key.slice(-4), }, jwt: { secretOrKey: config.jwt.secretOrKey.slice(0, 4) + '**redacted**' + config.jwt.secretOrKey.slice(-4), }, sanity: { ...config.sanity, token: config.sanity.token.slice(0, 4) + '**redacted**' + config.sanity.token.slice(-4), }, }, null, 2 ) ) } return config }