import { toMerged } from 'es-toolkit/object' import { isPlainObject } from 'es-toolkit/predicate' import chalk from 'chalk' import cfgHelper from '../helpers/config.ts' import fs from 'node:fs/promises' import path from 'node:path' import { load } from 'js-yaml' import { v4 as uuid } from 'uuid' /** * Config is assembled at runtime from config.yml + base.yml + the `settings` DB table, so its shape * is only known dynamically. Kept loose on purpose. */ type ConfigObject = Record export default { /** * Load root config from disk */ async init(silent = false): Promise { const confPaths = { config: path.join(WIKI.ROOTPATH, 'config.yml'), data: path.join(WIKI.SERVERPATH, 'base.yml') } if (process.env.CONFIG_FILE) { confPaths.config = path.resolve(WIKI.ROOTPATH, process.env.CONFIG_FILE) } if (!silent) { process.stdout.write(chalk.blue(`Loading configuration from ${confPaths.config}... `)) } let appconfig: ConfigObject = {} let appdata: ConfigObject = {} try { appconfig = load( cfgHelper.parseConfigValue(await fs.readFile(confPaths.config, 'utf8')) ) as ConfigObject appdata = load(await fs.readFile(confPaths.data, 'utf8')) as ConfigObject if (!silent) { console.info(chalk.green.bold('OK')) } } catch (err: any) { console.error(chalk.red.bold('FAILED')) console.error(err.message) console.error( chalk.red.bold('>>> Unable to read configuration file! Did you create the config.yml file?') ) process.exit(1) } // Merge with defaults appconfig = toMerged(appdata.defaults.config, appconfig) // Override port if (appconfig.port < 1) { appconfig.port = process.env.PORT || 80 } if (process.env.WIKI_PORT) { appconfig.port = process.env.WIKI_PORT || 80 } // Load package info const packageInfo = JSON.parse( await fs.readFile(path.join(WIKI.SERVERPATH, 'package.json'), 'utf-8') ) // Load DB Password from Docker Secret File if (process.env.DB_PASS_FILE) { if (!silent) { console.info(chalk.blue('DB_PASS_FILE is defined. Will use secret from file.')) } try { // FIXME: pre-existing bug — `.trim()` is called on the Promise rather than on the resolved // string, so this always throws and DB_PASS_FILE never works. Preserved as-is to keep the // TypeScript migration behavior-neutral; the fix is `(await fs.readFile(...)).trim()`. appconfig.db.pass = await (fs.readFile(process.env.DB_PASS_FILE, 'utf8') as any).trim() } catch (err: any) { console.error( chalk.red.bold( '>>> Failed to read Docker Secret File using path defined in DB_PASS_FILE env variable!' ) ) console.error(err.message) process.exit(1) } } WIKI.config = appconfig WIKI.data = appdata WIKI.version = packageInfo.version WIKI.releaseDate = packageInfo.releaseDate WIKI.devMode = packageInfo.dev === true }, /** * Load config from DB */ async loadFromDb(): Promise { WIKI.logger.info('Loading settings from DB...') const conf = await WIKI.models.settings.getConfig() if (conf) { WIKI.config = toMerged(WIKI.config, conf) return true } else { return false } }, /** * Save config to DB * * @param keys Array of keys to save * @returns Promise */ async saveToDb(keys: string[], propagate = true): Promise { try { for (const key of keys) { let value = WIKI.config[key] ?? null if (!isPlainObject(value)) { value = { v: value } } await WIKI.models.settings.updateConfig(key, value) } if (propagate) { WIKI.events.outbound.emit('reloadConfig') } } catch (err: any) { WIKI.logger.error(`Failed to save configuration to DB: ${err.message}`) return false } return true }, /** * Initialize DB tables with default values */ async initDbValues(): Promise { const ids = { groupAdminId: uuid(), groupUserId: WIKI.data.systemIds.usersGroupId, groupGuestId: WIKI.data.systemIds.guestsGroupId, siteId: uuid(), authModuleId: WIKI.data.systemIds.localAuthId, userAdminId: uuid(), userGuestId: uuid() } await WIKI.models.settings.init(ids) await WIKI.models.sites.init(ids) await WIKI.models.groups.init(ids) await WIKI.models.authentication.init(ids) await WIKI.models.users.init(ids) await WIKI.models.jobs.init() await WIKI.models.icons.init() }, /** * Subscribe to HA propagation events */ subscribeToEvents(): void { WIKI.events.inbound.on('reloadConfig', async () => { await WIKI.configSvc.loadFromDb() }) } }