import fs from 'node:fs'; import _ from 'lodash'; import { randomBytes } from 'crypto'; import { Logger } from 'tslog'; export type configObject = Record; /** * This class is responsible to save/edit config files. * * @export * @class config * @typedef {config} */ export default class config { #configPath: string; //global = {[key: string] : string} global: configObject; replaceSecrets: boolean; #logger: Logger | typeof console; /** * Creates an instance of config. * * @constructor * @param {string} configPath Path to config file. * @param {boolean} replaceSecrets Whether to replace secrets with generated values. * @param {object} configPreset Default config object with default values. * @param {Logger | typeof console} [logger] Optional (tslog) logger. */ constructor(configPath: string, replaceSecrets: boolean, configPreset: object, logger?: Logger | typeof console) { this.#configPath = configPath; this.global = configPreset; this.replaceSecrets = replaceSecrets; this.#logger = logger ?? console; this.#logger.info(`Initializing config manager with path: ${this.#configPath}`); try { // Read config const data = fs.readFileSync(this.#configPath, 'utf8'); // Extend config with missing parameters from configPreset. this.global = _.defaultsDeep(JSON.parse(data), this.global); // Save config. this.save_config(); } catch (err: any) { // If file does not exist, create it. if (err.code === 'ENOENT') { this.#logger.info(`Config file does not exist. Creating it at ${this.#configPath} now.`); this.save_config(); return; } this.#logger.error(`Could not read config file at ${this.#configPath} due to: ${err}`); // Exit process. process.exit(1); } } /** * Saves the jsonified config object to the config file. */ save_config() { try { // If enabled replace tokens defines as "gen" with random token if (this.replaceSecrets) { // Replace tokens with value "gen" this.generate_secrets(this.global, 'gen'); } fs.writeFileSync(this.#configPath, JSON.stringify(this.global, null, 8)); } catch (err) { this.#logger.error(`Could not write config file at ${this.#configPath} due to: ${err}`); return; } this.#logger.info(`Successfully written config file to ${this.#configPath}`); } /** * Replaces each item matching the value of placeholder with a random UUID. * Thanks to https://stackoverflow.com/questions/8085004/iterate-through-nested-javascript-objects * @param {configObject} obj */ generate_secrets(obj: configObject, placeholder: string) { const stack = [obj]; while (stack?.length > 0) { const currentObj: any = stack.pop(); Object.keys(currentObj).forEach((key) => { if (currentObj[key] === placeholder) { this.#logger.info('Generating secret: ' + key); currentObj[key] = randomBytes(48).toString('base64').replace(/\W/g, ''); } if (typeof currentObj[key] === 'object' && currentObj[key] !== null) { stack.push(currentObj[key]); } }); } } } /* **** Example **** import ConfigHandlerNG from './assets/configHandlerNG.js'; // Create a new config instance. export const config = new ConfigHandler(__path + '/config.json', true, { test1: 't1', test2: 't2', test3: 'gen', test4: 't4', test5: 'gen', testObj: { local: { active: true, users: { user1: 'gen', user2: 'gen', user3: 'gen', user4: 'gen', } }, oidc: { active: false } } }); console.log('Base Config:'); console.log(config.global); console.log('Add some new key to config and call save_config().'); config.global.NewKey = 'ThisIsANewKey!' config.save_config() console.log('This will add a new key with value gen, but gen gets replaced with a random UUID when save_config() is called.'); config.global.someSecret = 'gen' config.save_config() // global.someSecret is getting replaced with some random UUID since it was set to 'gen'. console.log('Complete Config:'); console.log(config.global); */