From f05006bf3907a6ed59d9795b34ca1cff0e79b083 Mon Sep 17 00:00:00 2001 From: Nikita Melnikov Date: Wed, 28 Sep 2022 20:35:02 +0800 Subject: [PATCH] implement configuration through YAML --- .codexdocsrc.sample | 15 ----- .dockerignore | 2 +- .gitignore | 1 + README.md | 2 +- app-config.yaml | 31 +++++++++++ config/development.json | 8 --- config/production.json | 8 --- config/testing.json | 8 --- package.json | 5 +- src/backend/app.ts | 20 +++---- src/backend/controllers/transport.ts | 6 +- src/backend/routes/api/transport.ts | 4 +- src/backend/routes/auth.ts | 4 +- src/backend/routes/middlewares/token.ts | 4 +- src/backend/utils/appConfig.ts | 73 +++++++++++++++++++++++++ src/backend/utils/database/initDb.ts | 6 +- src/backend/utils/rcparser.ts | 3 +- src/bin/server.ts | 4 +- yarn.lock | 26 ++++++++- 19 files changed, 160 insertions(+), 70 deletions(-) delete mode 100644 .codexdocsrc.sample create mode 100644 app-config.yaml delete mode 100644 config/development.json delete mode 100644 config/production.json delete mode 100644 config/testing.json create mode 100644 src/backend/utils/appConfig.ts diff --git a/.codexdocsrc.sample b/.codexdocsrc.sample deleted file mode 100644 index 43b9f94..0000000 --- a/.codexdocsrc.sample +++ /dev/null @@ -1,15 +0,0 @@ -{ - "title": "CodeX Docs", - "description": "A block-styled editor with clean JSON output", - "menu": [ - "Guides", - {"title": "CodeX", "uri": "https://codex.so"} - ], - "startPage": "", - "misprintsChatId": "12344564", - "yandexMetrikaId": "", - "carbon": { - "serve": "", - "placement": "" - } -} diff --git a/.dockerignore b/.dockerignore index c90035e..7acb8c6 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,6 +3,6 @@ !src !package.json !yarn.lock -!webpack.config.js +!webpack.appConfig.js !tsconfig.json !.postcssrc diff --git a/.gitignore b/.gitignore index 167bc65..693fcab 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,7 @@ typings/ # Database files .db/ +db/ .testdb/ # Cache of babel and others diff --git a/README.md b/README.md index 0e0b490..7218534 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Here is our [Demo Application](https://docs-demo.codex.so/) where you can try Co git clone https://github.com/codex-team/codex.docs ``` -### 2. Fill the config +### 2. Fill the appConfig Read about available [configuration](https://docs.codex.so/configuration) options. diff --git a/app-config.yaml b/app-config.yaml new file mode 100644 index 0000000..71284f9 --- /dev/null +++ b/app-config.yaml @@ -0,0 +1,31 @@ +port: 4000 +host: "localhost" +uploads: "./uploads" + +frontend: + title: "CodeX Docs" + description: "A block-styled editor with clean JSON output" + startPage: "" + misprintsChatId: "12344564" + yandexMetrikaId: "" + carbon: + serve: "" + placement: "" + menu: + - "Guides" + - title: "CodeX" + uri: "https://codex.so" + +auth: + secret: supersecret + +hawk: +# frontendToken: "123" +# backendToken: "123" + +database: + driver: local + local: + path: ./db + mongodb: + uri: mongodb://localhost:27017 diff --git a/config/development.json b/config/development.json deleted file mode 100644 index c7968be..0000000 --- a/config/development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "port": 3000, - "database": ".db", - "rcFile": "./.codexdocsrc", - "uploads": "public/uploads", - "secret": "iamasecretstring", - "favicon": "" -} diff --git a/config/production.json b/config/production.json deleted file mode 100644 index 102f201..0000000 --- a/config/production.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "port": 3000, - "database": ".db", - "rcFile": "./.codexdocsrc", - "uploads": "/uploads", - "secret": "iamasecretstring", - "favicon": "" -} diff --git a/config/testing.json b/config/testing.json deleted file mode 100644 index cb29838..0000000 --- a/config/testing.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "port": 3001, - "database": ".testdb", - "rcFile": "./src/test/.codexdocsrc", - "uploads": "public/uploads_test", - "secret": "iamasecretstring", - "favicon": "" -} diff --git a/package.json b/package.json index 2f0f951..6a33fb4 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,9 @@ "@codexteam/shortcuts": "^1.2.0", "@hawk.so/javascript": "^3.0.1", "@hawk.so/nodejs": "^3.1.4", + "arg": "^5.0.2", "config": "^3.3.6", + "config-loader": "https://github.com/codex-team/config-loader#081ad636684e9d1e5efa6dd757e1e0535f0a2b26", "cookie-parser": "^1.4.5", "csurf": "^1.11.0", "debug": "^4.3.2", @@ -40,7 +42,8 @@ "node-fetch": "^3.2.10", "open-graph-scraper": "^4.9.0", "twig": "^1.15.4", - "uuid4": "^2.0.2" + "uuid4": "^2.0.2", + "zod": "^3.19.1" }, "devDependencies": { "@babel/core": "^7.17.5", diff --git a/src/backend/app.ts b/src/backend/app.ts index 23e184d..fd7b7ce 100644 --- a/src/backend/app.ts +++ b/src/backend/app.ts @@ -3,15 +3,13 @@ import path from 'path'; import { fileURLToPath } from 'url'; import cookieParser from 'cookie-parser'; import morgan from 'morgan'; -import rcParser from './utils/rcparser.js'; import routes from './routes/index.js'; import HttpException from './exceptions/httpException.js'; import * as dotenv from 'dotenv'; -import config from 'config'; import HawkCatcher from '@hawk.so/nodejs'; import os from 'os'; -import appConfig from 'config'; import { downloadFavicon, FaviconData } from './utils/downloadFavicon.js'; +import appConfig from "./utils/appConfig.js"; /** * The __dirname CommonJS variables are not available in ES modules. @@ -22,20 +20,20 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); dotenv.config(); const app = express(); -const localConfig = rcParser.getConfiguration(); +const localConfig = appConfig.frontend; // Initialize the backend error tracking catcher. -if (process.env.HAWK_TOKEN_BACKEND) { - HawkCatcher.init(process.env.HAWK_TOKEN_BACKEND); +if (appConfig.hawk?.backendToken) { + HawkCatcher.init(appConfig.hawk.backendToken); } // Get url to upload favicon from config -const favicon: string = appConfig.get('favicon'); +const favicon = appConfig.favicon app.locals.config = localConfig; // Set client error tracking token as app local. -if (process.env.HAWK_TOKEN_CLIENT) { - app.locals.config.hawkClientToken = process.env.HAWK_TOKEN_CLIENT; +if (appConfig.hawk?.frontendToken) { + app.locals.config.hawkClientToken = appConfig.hawk.frontendToken; } // view engine setup @@ -69,7 +67,7 @@ app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, '../../public'))); -app.use('/uploads', express.static(config.get('uploads'))); +app.use('/uploads', express.static(appConfig.uploads)); app.use('/favicon', express.static(downloadedFaviconFolder)); app.use('/', routes); @@ -78,7 +76,7 @@ app.use('/', routes); // global error handler app.use(function (err: unknown, req: Request, res: Response, next: NextFunction) { // send any type of error to hawk server. - if (process.env.HAWK_TOKEN_BACKEND && err instanceof Error) { + if (appConfig.hawk?.backendToken && err instanceof Error) { HawkCatcher.send(err); } // only send Http based exception to client. diff --git a/src/backend/controllers/transport.ts b/src/backend/controllers/transport.ts index 4bd5918..b81822c 100644 --- a/src/backend/controllers/transport.ts +++ b/src/backend/controllers/transport.ts @@ -2,10 +2,10 @@ import fileType from 'file-type'; import fetch from 'node-fetch'; import fs from 'fs'; import nodePath from 'path'; -import config from 'config'; import File, { FileData } from '../models/file.js'; import crypto from '../utils/crypto.js'; import deepMerge from '../utils/objects.js'; +import appConfig from "../utils/appConfig.js"; const random16 = crypto.random16; @@ -71,7 +71,7 @@ class Transport { const type = await fileType.fromBuffer(buffer); const ext = type ? type.ext : nodePath.extname(url).slice(1); - fs.writeFileSync(`${config.get('uploads')}/${filename}.${ext}`, buffer); + fs.writeFileSync(`${appConfig.uploads}/${filename}.${ext}`, buffer); const fetchedContentType: string | null = fetchedFile.headers.get('content-type'); let fetchedMimeType: string | undefined; @@ -87,7 +87,7 @@ class Transport { const file = new File({ name: url, filename: `${filename}.${ext}`, - path: `${config.get('uploads')}/${filename}.${ext}`, + path: `${appConfig.uploads}/${filename}.${ext}`, size: buffer.length, mimetype: mimeType, }); diff --git a/src/backend/routes/api/transport.ts b/src/backend/routes/api/transport.ts index f492506..47187a5 100644 --- a/src/backend/routes/api/transport.ts +++ b/src/backend/routes/api/transport.ts @@ -2,9 +2,9 @@ import { Request, Response, Router } from 'express'; import multer, { StorageEngine } from 'multer'; import mime from 'mime'; import mkdirp from 'mkdirp'; -import config from 'config'; import Transport from '../../controllers/transport.js'; import { random16 } from '../../utils/crypto.js'; +import appConfig from "../../utils/appConfig.js"; const router = Router(); @@ -15,7 +15,7 @@ const router = Router(); */ const storage: StorageEngine = multer.diskStorage({ destination: (req, file, cb) => { - const dir: string = config.get('uploads') || 'public/uploads'; + const dir: string = appConfig.uploads || 'public/uploads'; mkdirp(dir); cb(null, dir); diff --git a/src/backend/routes/auth.ts b/src/backend/routes/auth.ts index 8ee3a66..455ff6f 100644 --- a/src/backend/routes/auth.ts +++ b/src/backend/routes/auth.ts @@ -1,7 +1,7 @@ import express, { Request, Response } from 'express'; import jwt from 'jsonwebtoken'; -import config from 'config'; import csrf from 'csurf'; +import appConfig from "../utils/appConfig.js"; const router = express.Router(); const csrfProtection = csrf({ cookie: true }); @@ -46,7 +46,7 @@ router.post('/auth', parseForm, csrfProtection, async (req: Request, res: Respon iss: 'Codex Team', sub: 'auth', iat: Date.now(), - }, process.env.PASSWORD + config.get('secret')); + }, process.env.PASSWORD + appConfig.auth.secret); res.cookie('authToken', token, { httpOnly: true, diff --git a/src/backend/routes/middlewares/token.ts b/src/backend/routes/middlewares/token.ts index 2afb5c4..c455b8c 100644 --- a/src/backend/routes/middlewares/token.ts +++ b/src/backend/routes/middlewares/token.ts @@ -1,6 +1,6 @@ -import config from 'config'; import { NextFunction, Request, Response } from 'express'; import jwt from 'jsonwebtoken'; +import appConfig from "../../utils/appConfig.js"; /** @@ -21,7 +21,7 @@ export default async function verifyToken(req: Request, res: Response, next: Nex return; } - const decodedToken = jwt.verify(token, process.env.PASSWORD + config.get('secret')); + const decodedToken = jwt.verify(token, process.env.PASSWORD + appConfig.auth.secret); res.locals.isAuthorized = !!decodedToken; diff --git a/src/backend/utils/appConfig.ts b/src/backend/utils/appConfig.ts new file mode 100644 index 0000000..726dddb --- /dev/null +++ b/src/backend/utils/appConfig.ts @@ -0,0 +1,73 @@ +import { loadConfig } from 'config-loader'; +import * as process from 'process'; +import arg from 'arg'; +import path from 'path'; +import { z } from "zod"; + +/** + * Configuration for Hawk errors catcher + */ +const HawkConfig = z.object({ + backendToken: z.string().optional(), // Hawk backend token + frontendToken: z.string().optional(), // Hawk frontend token +}) + +const LocalDatabaseConfig = z.object({ + driver: z.literal('local'), + local: z.object({ + path: z.string() + }) +}) + +const AuthConfig = z.object({ + secret: z.string() // Secret for JWT +}) + +const FrontendConfig = z.object({ + title: z.string(), // Title for pages + description: z.string(), // Description for pages + startPage: z.string(), // Start page + misprintsChatId: z.string().optional(), // Telegram chat id for misprints + yandexMetrikaId: z.string().optional(), // Yandex metrika id + carbon: z.object({ + serve: z.string().optional(), // Carbon serve url + placement: z.string().optional(), // Carbon placement + }), + menu: z.array(z.union([z.string(), z.object({title: z.string(), uri: z.string()})])), // Menu for pages +}) + +/** + * Application configuration + */ +const AppConfig = z.object({ + port: z.number(), // Port to listen on + host: z.string(), // Host to listen on + favicon: z.string().optional(), // Path or URL to favicon + uploads: z.string(), // Path to uploads folder + hawk: HawkConfig.optional().nullable(), // Hawk configuration + frontend: FrontendConfig, // Frontend configuration + auth: AuthConfig, // Auth configuration + database: LocalDatabaseConfig, // Database configuration +}) + +export type AppConfig = z.infer; + +const args = arg({ /* eslint-disable @typescript-eslint/naming-convention */ + '--config': [ String ], + '-c': '--config', +}); + +const cwd = process.cwd(); +const paths = (args['--config'] || ['./app-config.yaml']).map((configPath) => { + if (path.isAbsolute(configPath)) { + return configPath; + } + + return path.join(cwd, configPath); +}); + +const loadedConfig = loadConfig(...paths); + +const appConfig = AppConfig.parse(loadedConfig) + +export default appConfig; diff --git a/src/backend/utils/database/initDb.ts b/src/backend/utils/database/initDb.ts index b48d4ce..ae24d41 100644 --- a/src/backend/utils/database/initDb.ts +++ b/src/backend/utils/database/initDb.ts @@ -1,6 +1,6 @@ import Datastore from 'nedb'; -import config from 'config'; import path from 'path'; +import appConfig from "../appConfig.js"; /** * Init function for nedb instance @@ -10,7 +10,7 @@ import path from 'path'; */ export default function initDb(name: string): Datastore { return new Datastore({ - filename: path.resolve(`${config.get('database')}/${name}.db`), + filename: path.resolve(`${appConfig.database.local.path}/${name}.db`), autoload: true, }); -} \ No newline at end of file +} diff --git a/src/backend/utils/rcparser.ts b/src/backend/utils/rcparser.ts index 1b1befd..968e4d5 100644 --- a/src/backend/utils/rcparser.ts +++ b/src/backend/utils/rcparser.ts @@ -1,6 +1,5 @@ import fs from 'fs'; import path from 'path'; -import config from 'config'; import { fileURLToPath } from 'url'; /** @@ -10,7 +9,7 @@ import { fileURLToPath } from 'url'; // eslint-disable-next-line @typescript-eslint/naming-convention const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const rcPath = path.resolve(__dirname, '../../../', config.get('rcFile') || './.codexdocsrc'); +const rcPath = path.resolve(__dirname, '../../../', './.codexdocsrc'); /** * @typedef {object} menu diff --git a/src/bin/server.ts b/src/bin/server.ts index 9f6c225..141c6e8 100644 --- a/src/bin/server.ts +++ b/src/bin/server.ts @@ -3,15 +3,15 @@ */ import app from '../backend/app.js'; import http from 'http'; -import config from 'config'; import Debug from 'debug'; +import appConfig from "../backend/utils/appConfig.js"; const debug = Debug.debug('codex.editor.docs:server'); /** * Get port from environment and store in Express. */ -const port = normalizePort(config.get('port') || '3000'); +const port = normalizePort(appConfig.port.toString() || '3000'); app.set('port', port); diff --git a/yarn.lock b/yarn.lock index c25a97d..e48e266 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1658,6 +1658,11 @@ arg@^4.1.0: version "4.1.3" resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" +arg@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" + integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -2182,6 +2187,14 @@ concurrently@^7.1.0: tree-kill "^1.2.2" yargs "^17.3.1" +"config-loader@https://github.com/codex-team/config-loader#081ad636684e9d1e5efa6dd757e1e0535f0a2b26": + version "0.0.1" + resolved "https://github.com/codex-team/config-loader#081ad636684e9d1e5efa6dd757e1e0535f0a2b26" + dependencies: + js-yaml "^4.1.0" + lodash.isarray "^4.0.0" + lodash.merge "^4.6.2" + config@^3.3.6: version "3.3.7" resolved "https://registry.yarnpkg.com/config/-/config-3.3.7.tgz#4310410dc2bf4e0effdca21a12a4035860a24ee4" @@ -3761,9 +3774,10 @@ js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" -js-yaml@4.1.0: +js-yaml@4.1.0, js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== dependencies: argparse "^2.0.1" @@ -3978,6 +3992,11 @@ lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" +lodash.isarray@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-4.0.0.tgz#2aca496b28c4ca6d726715313590c02e6ea34403" + integrity sha512-V8ViWvoNlXpCrB6Ewaj3ScRXUpmCvqp4tJUxa3dlovuJj/8lp3SND5Kw4v5OeuHgoyw4qJN+gl36qZqp6WYQ6g== + lodash.isboolean@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" @@ -6333,3 +6352,8 @@ yn@^2.0.0: yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + +zod@^3.19.1: + version "3.19.1" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.19.1.tgz#112f074a97b50bfc4772d4ad1576814bd8ac4473" + integrity sha512-LYjZsEDhCdYET9ikFu6dVPGp2YH9DegXjdJToSzD9rO6fy4qiRYFoyEYwps88OseJlPyl2NOe2iJuhEhL7IpEA==