diff --git a/src/controllers/custom/renameAccountController.ts b/src/controllers/custom/renameAccountController.ts index d30b44ae..5f950550 100644 --- a/src/controllers/custom/renameAccountController.ts +++ b/src/controllers/custom/renameAccountController.ts @@ -1,6 +1,7 @@ import { RequestHandler } from "express"; import { getAccountForRequest, isAdministrator, isNameTaken } from "@/src/services/loginService"; -import { config, saveConfig } from "@/src/services/configService"; +import { config } from "@/src/services/configService"; +import { saveConfig } from "@/src/services/configWatcherService"; export const renameAccountController: RequestHandler = async (req, res) => { const account = await getAccountForRequest(req); diff --git a/src/controllers/custom/updateConfigDataController.ts b/src/controllers/custom/updateConfigDataController.ts index 961cccb1..534dfe0f 100644 --- a/src/controllers/custom/updateConfigDataController.ts +++ b/src/controllers/custom/updateConfigDataController.ts @@ -1,5 +1,5 @@ import { RequestHandler } from "express"; -import { updateConfig } from "@/src/services/configService"; +import { updateConfig } from "@/src/services/configWatcherService"; import { getAccountForRequest, isAdministrator } from "@/src/services/loginService"; const updateConfigDataController: RequestHandler = async (req, res) => { diff --git a/src/index.ts b/src/index.ts index 8c38237f..9a942606 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,22 +1,30 @@ -import { logger } from "./utils/logger"; +// First, init config. +import { config, loadConfig } from "@/src/services/configService"; +try { + loadConfig(); +} catch (e) { + console.log("ERROR: Failed to load config.json. You can copy config.json.example to create your config.json."); + process.exit(1); +} +// Now we can init the logger with the settings provided in the config. +import { logger } from "@/src/utils/logger"; logger.info("Starting up..."); +// Proceed with normal startup: bring up config watcher service, validate config, connect to MongoDB, and finally start listening for HTTP. import http from "http"; import https from "https"; import fs from "node:fs"; import { app } from "./app"; -import { config, validateConfig } from "./services/configService"; -import { registerLogFileCreationListener } from "@/src/utils/logger"; import mongoose from "mongoose"; import { Json, JSONStringify } from "json-with-bigint"; +import { validateConfig } from "@/src/services/configWatcherService"; // Patch JSON.stringify to work flawlessly with Bigints. JSON.stringify = (obj: Exclude, _replacer?: unknown, space?: string | number): string => { return JSONStringify(obj, space); }; -registerLogFileCreationListener(); validateConfig(); mongoose diff --git a/src/services/configService.ts b/src/services/configService.ts index 6ceaf9d0..c01fee7e 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -1,33 +1,13 @@ import fs from "fs"; -import fsPromises from "fs/promises"; import path from "path"; import { repoDir } from "@/src/helpers/pathHelper"; -import { logger } from "@/src/utils/logger"; - -const configPath = path.join(repoDir, "config.json"); -export const config = JSON.parse(fs.readFileSync(configPath, "utf-8")) as IConfig; - -let amnesia = false; -fs.watchFile(configPath, () => { - if (amnesia) { - amnesia = false; - } else { - logger.info("Detected a change to config.json, reloading its contents."); - - // Set all values to undefined now so if the new config.json omits some fields that were previously present, it's correct in-memory. - for (const key of Object.keys(config)) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - (config as any)[key] = undefined; - } - - Object.assign(config, JSON.parse(fs.readFileSync(configPath, "utf-8"))); - validateConfig(); - } -}); interface IConfig { mongodbUrl: string; - logger: ILoggerConfig; + logger: { + files: boolean; + level: string; // "fatal" | "error" | "warn" | "info" | "http" | "debug" | "trace"; + }; myAddress: string; httpPort?: number; httpsPort?: number; @@ -72,26 +52,23 @@ interface IConfig { }; } -interface ILoggerConfig { - files: boolean; - level: string; // "fatal" | "error" | "warn" | "info" | "http" | "debug" | "trace"; -} +export const configPath = path.join(repoDir, "config.json"); -export const updateConfig = async (data: string): Promise => { - amnesia = true; - await fsPromises.writeFile(configPath, data); - Object.assign(config, JSON.parse(data)); +export const config: IConfig = { + mongodbUrl: "mongodb://127.0.0.1:27017/openWF", + logger: { + files: true, + level: "trace" + }, + myAddress: "localhost" }; -export const saveConfig = async (): Promise => { - amnesia = true; - await fsPromises.writeFile(configPath, JSON.stringify(config, null, 2)); -}; - -export const validateConfig = (): void => { - if (typeof config.administratorNames == "string") { - logger.info(`Updating config.json to make administratorNames an array.`); - config.administratorNames = [config.administratorNames]; - void saveConfig(); +export const loadConfig = (): void => { + // Set all values to undefined now so if the new config.json omits some fields that were previously present, it's correct in-memory. + for (const key of Object.keys(config)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + (config as any)[key] = undefined; } + + Object.assign(config, JSON.parse(fs.readFileSync(configPath, "utf-8"))); }; diff --git a/src/services/configWatcherService.ts b/src/services/configWatcherService.ts new file mode 100644 index 00000000..e8584785 --- /dev/null +++ b/src/services/configWatcherService.ts @@ -0,0 +1,39 @@ +import fs from "fs"; +import fsPromises from "fs/promises"; +import { logger } from "../utils/logger"; +import { config, configPath, loadConfig } from "./configService"; + +let amnesia = false; +fs.watchFile(configPath, () => { + if (amnesia) { + amnesia = false; + } else { + logger.info("Detected a change to config.json, reloading its contents."); + try { + loadConfig(); + } catch (e) { + logger.error("Failed to reload config.json. Did you delete it?! Execution cannot continue."); + process.exit(1); + } + validateConfig(); + } +}); + +export const validateConfig = (): void => { + if (typeof config.administratorNames == "string") { + logger.info(`Updating config.json to make administratorNames an array.`); + config.administratorNames = [config.administratorNames]; + void saveConfig(); + } +}; + +export const updateConfig = async (data: string): Promise => { + amnesia = true; + await fsPromises.writeFile(configPath, data); + Object.assign(config, JSON.parse(data)); +}; + +export const saveConfig = async (): Promise => { + amnesia = true; + await fsPromises.writeFile(configPath, JSON.stringify(config, null, 2)); +}; diff --git a/src/utils/logger.ts b/src/utils/logger.ts index f3873591..f02c0db4 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -104,9 +104,7 @@ export const logger = createLogger({ addColors(logLevels.colors); -export function registerLogFileCreationListener(): void { - errorLog.on("new", filename => logger.info(`Using error log file: ${filename}`)); - combinedLog.on("new", filename => logger.info(`Using combined log file: ${filename}`)); - errorLog.on("rotate", filename => logger.info(`Rotated error log file: ${filename}`)); - combinedLog.on("rotate", filename => logger.info(`Rotated combined log file: ${filename}`)); -} +errorLog.on("new", filename => logger.info(`Using error log file: ${filename}`)); +combinedLog.on("new", filename => logger.info(`Using combined log file: ${filename}`)); +errorLog.on("rotate", filename => logger.info(`Rotated error log file: ${filename}`)); +combinedLog.on("rotate", filename => logger.info(`Rotated combined log file: ${filename}`));