diff --git a/src/Application.ts b/src/Application.ts index add9925..41a0127 100644 --- a/src/Application.ts +++ b/src/Application.ts @@ -94,8 +94,11 @@ export default abstract class Application implements Extendable; + public async init?(): Promise; + public async start?(expressApp: Express): Promise; - public async init?(router: Router): Promise; + public async initRoutes?(router: Router): Promise; - public async handle?(router: Router): Promise; + public async handleRoutes?(router: Router): Promise; public async stop?(): Promise; diff --git a/src/TestApp.ts b/src/TestApp.ts index a646e86..c9b5a0b 100644 --- a/src/TestApp.ts +++ b/src/TestApp.ts @@ -108,7 +108,7 @@ export default class TestApp extends Application { this.use(new AuthComponent(this, new MagicLinkAuthMethod(this, MAGIC_LINK_MAIL), new PasswordAuthMethod(this))); // WebSocket server - this.use(new WebSocketServerComponent(this, this.as(ExpressAppComponent), this.as(RedisComponent))); + this.use(new WebSocketServerComponent()); } protected registerWebSocketListeners(): void { diff --git a/src/auth/AuthComponent.ts b/src/auth/AuthComponent.ts index 312c7cf..83a2295 100644 --- a/src/auth/AuthComponent.ts +++ b/src/auth/AuthComponent.ts @@ -18,7 +18,7 @@ export default class AuthComponent extends ApplicationComponent { this.authGuard = new AuthGuard(app, ...authMethods); } - public async init(): Promise { + public async initRoutes(): Promise { this.use(AuthMiddleware); } diff --git a/src/components/AutoUpdateComponent.ts b/src/components/AutoUpdateComponent.ts index 74b4345..5331d8d 100644 --- a/src/components/AutoUpdateComponent.ts +++ b/src/components/AutoUpdateComponent.ts @@ -16,7 +16,7 @@ export default class AutoUpdateComponent extends ApplicationComponent { this.checkSecurityConfigField('gitlab_webhook_token'); } - public async init(router: Router): Promise { + public async initRoutes(router: Router): Promise { router.post('/update/push.json', (req, res) => { const token = req.header('X-Gitlab-Token'); if (!token || token !== config.get('gitlab_webhook_token')) diff --git a/src/components/CsrfProtectionComponent.ts b/src/components/CsrfProtectionComponent.ts index 1d09236..c817c0d 100644 --- a/src/components/CsrfProtectionComponent.ts +++ b/src/components/CsrfProtectionComponent.ts @@ -20,7 +20,7 @@ export default class CsrfProtectionComponent extends ApplicationComponent { this.excluders.push(excluder); } - public async handle(router: Router): Promise { + public async handleRoutes(router: Router): Promise { router.use(async (req, res, next) => { for (const excluder of CsrfProtectionComponent.excluders) { if (excluder(req)) return next(); diff --git a/src/components/ExpressAppComponent.ts b/src/components/ExpressAppComponent.ts index 778e48d..541a9df 100644 --- a/src/components/ExpressAppComponent.ts +++ b/src/components/ExpressAppComponent.ts @@ -30,7 +30,7 @@ export default class ExpressAppComponent extends ApplicationComponent { this.expressApp = app; } - public async init(router: Router): Promise { + public async initRoutes(router: Router): Promise { router.use(preventContextCorruptionMiddleware(express.json({ type: req => req.headers['content-type']?.match(/^application\/(.+\+)?json$/), }))); diff --git a/src/components/FormHelperComponent.ts b/src/components/FormHelperComponent.ts index 800f296..05e6f24 100644 --- a/src/components/FormHelperComponent.ts +++ b/src/components/FormHelperComponent.ts @@ -3,7 +3,7 @@ import {Router} from "express"; import ApplicationComponent from "../ApplicationComponent.js"; export default class FormHelperComponent extends ApplicationComponent { - public async init(router: Router): Promise { + public async initRoutes(router: Router): Promise { router.use((req, res, next) => { let _validation: unknown | null; res.locals.validation = () => { diff --git a/src/components/FrontendToolsComponent.ts b/src/components/FrontendToolsComponent.ts index 00dfc49..b4184d5 100644 --- a/src/components/FrontendToolsComponent.ts +++ b/src/components/FrontendToolsComponent.ts @@ -1,7 +1,6 @@ import config from "config"; import {Express, Router} from "express"; import path from "path"; -import * as querystring from "querystring"; import {ParsedUrlQueryInput} from "querystring"; import util from "util"; @@ -9,6 +8,7 @@ import ApplicationComponent from "../ApplicationComponent.js"; import Controller, {RouteParams} from "../Controller.js"; import AssetCompiler from "../frontend/AssetCompiler.js"; import AssetPreCompiler from "../frontend/AssetPreCompiler.js"; +import Globals from "../frontend/Globals.js"; import ViewEngine from "../frontend/ViewEngine.js"; import {logger} from "../Logger.js"; import {listFilesRecursively} from "../Utils.js"; @@ -18,6 +18,7 @@ export default class FrontendToolsComponent extends ApplicationComponent { private readonly publicDir: string; private readonly publicAssetsCache: FileCache = new FileCache(); private readonly assetPreCompilers: AssetPreCompiler[]; + private readonly globals: Globals = new Globals(); public constructor( private readonly assetCompiler: AssetCompiler, @@ -31,9 +32,30 @@ export default class FrontendToolsComponent extends ApplicationComponent { if (assetPreCompiler.isPublic()) { this.assetCompiler.addExtension(assetPreCompiler.getExtension()); } + + assetPreCompiler.setGlobals(this.globals); } } + + public async init(): Promise { + this.globals.set('route', ( + route: string, + params: RouteParams = [], + query: ParsedUrlQueryInput = {}, + absolute: boolean = false, + ) => Controller.route(route, params, query, absolute)); + this.globals.set('app_version', this.getApp().getVersion()); + this.globals.set('core_version', this.getApp().getCoreVersion()); + this.globals.set('app', config.get('app')); + this.globals.set('dump', (val: unknown) => { + return util.inspect(val); + }); + this.globals.set('hex', (v: number) => { + return v.toString(16); + }); + } + public async start(app: Express): Promise { // Cache public assets if (config.get('asset_cache')) { @@ -55,9 +77,6 @@ export default class FrontendToolsComponent extends ApplicationComponent { main = false; } } - - // Add util globals - this.setupGlobals(); } public async stop(): Promise { @@ -66,7 +85,7 @@ export default class FrontendToolsComponent extends ApplicationComponent { } } - public async handle(router: Router): Promise { + public async handleRoutes(router: Router): Promise { router.use((req, res, next) => { res.locals.inlineAsset = (urlPath: string) => { return this.publicAssetsCache.getOrFail(path.join(this.publicDir, urlPath)); @@ -86,25 +105,6 @@ export default class FrontendToolsComponent extends ApplicationComponent { }); } - public setupGlobals(): void { - ViewEngine.setGlobal('route', ( - route: string, - params: RouteParams = [], - query: ParsedUrlQueryInput = {}, - absolute: boolean = false, - ) => Controller.route(route, params, query, absolute)); - ViewEngine.setGlobal('app_version', this.getApp().getVersion()); - ViewEngine.setGlobal('core_version', this.getApp().getCoreVersion()); - ViewEngine.setGlobal('querystring', querystring); - ViewEngine.setGlobal('app', config.get('app')); - ViewEngine.setGlobal('dump', (val: unknown) => { - return util.inspect(val); - }); - ViewEngine.setGlobal('hex', (v: number) => { - return v.toString(16); - }); - } - public hookPreCompilers(): void { for (const assetPreCompiler of this.assetPreCompilers) { assetPreCompiler.onPreCompile(async watch => { @@ -127,4 +127,8 @@ export default class FrontendToolsComponent extends ApplicationComponent { this.hookPreCompilers(); } } + + public getGlobals(): Globals { + return this.globals; + } } diff --git a/src/components/LogRequestsComponent.ts b/src/components/LogRequestsComponent.ts index 38d358e..f9065f1 100644 --- a/src/components/LogRequestsComponent.ts +++ b/src/components/LogRequestsComponent.ts @@ -75,7 +75,7 @@ export default class LogRequestsComponent extends ApplicationComponent { return ''; } - public async init(router: Router): Promise { + public async initRoutes(router: Router): Promise { router.use((req, res, next) => { onFinished(res, (err) => { if (!err) { diff --git a/src/components/MailComponent.ts b/src/components/MailComponent.ts index 7f80d08..f4de64a 100644 --- a/src/components/MailComponent.ts +++ b/src/components/MailComponent.ts @@ -11,6 +11,7 @@ import {logger} from "../Logger.js"; import Mail from "../mail/Mail.js"; import MailError from "../mail/MailError.js"; import SecurityError from "../SecurityError.js"; +import FrontendToolsComponent from "./FrontendToolsComponent.js"; export default class MailComponent extends ApplicationComponent { private transporter?: Transporter; @@ -91,7 +92,7 @@ export default class MailComponent extends ApplicationComponent { locals.mail_to = options.to; locals.mail_link = config.get('public_url') + Controller.route('mail', [template.template], locals); - Object.assign(locals, ViewEngine.getGlobals()); + Object.assign(locals, this.getApp().as(FrontendToolsComponent).getGlobals().get()); // Log logger.debug(`Send mail from ${options.from.address} to ${options.to}`); diff --git a/src/components/MaintenanceComponent.ts b/src/components/MaintenanceComponent.ts index d5edb5d..fea7c63 100644 --- a/src/components/MaintenanceComponent.ts +++ b/src/components/MaintenanceComponent.ts @@ -15,7 +15,7 @@ export default class MaintenanceComponent extends ApplicationComponent { this.canServe = canServe; } - public async handle(router: Router): Promise { + public async handleRoutes(router: Router): Promise { router.use((req: Request, res: Response, next: NextFunction) => { if (res.headersSent) { return next(); diff --git a/src/components/PreviousUrlComponent.ts b/src/components/PreviousUrlComponent.ts index 8e6b6ba..79f29c0 100644 --- a/src/components/PreviousUrlComponent.ts +++ b/src/components/PreviousUrlComponent.ts @@ -3,11 +3,18 @@ import onFinished from "on-finished"; import ApplicationComponent from "../ApplicationComponent.js"; import {logger} from "../Logger.js"; +import FrontendToolsComponent from "./FrontendToolsComponent.js"; import SessionComponent from "./SessionComponent.js"; export default class PreviousUrlComponent extends ApplicationComponent { + public async init(): Promise { + const globals = this.getApp().asOptional(FrontendToolsComponent)?.getGlobals(); + if (globals) { + globals.set('getPreviousUrl', () => null); + } + } - public async handle(router: Router): Promise { + public async handleRoutes(router: Router): Promise { router.use((req, res, next) => { req.getPreviousUrl = () => { let url = req.header('referer'); diff --git a/src/components/ServeStaticDirectoryComponent.ts b/src/components/ServeStaticDirectoryComponent.ts index 02350ac..a817ce7 100644 --- a/src/components/ServeStaticDirectoryComponent.ts +++ b/src/components/ServeStaticDirectoryComponent.ts @@ -15,7 +15,7 @@ export default class ServeStaticDirectoryComponent extends ApplicationComponent this.path = routePath; } - public async init(router: Router): Promise { + public async initRoutes(router: Router): Promise { const resolvedRoot = path.resolve(this.root); if (this.path) { diff --git a/src/components/SessionComponent.ts b/src/components/SessionComponent.ts index f841f3d..fab2d04 100644 --- a/src/components/SessionComponent.ts +++ b/src/components/SessionComponent.ts @@ -4,7 +4,9 @@ import {Router} from "express"; import session from "express-session"; import ApplicationComponent from "../ApplicationComponent.js"; +import ViewEngine from "../frontend/ViewEngine.js"; import SecurityError from "../SecurityError.js"; +import FrontendToolsComponent from "./FrontendToolsComponent.js"; import RedisComponent from "./RedisComponent.js"; export default class SessionComponent extends ApplicationComponent { @@ -15,6 +17,13 @@ export default class SessionComponent extends ApplicationComponent { this.storeComponent = storeComponent; } + public async init(): Promise { + const globals = this.getApp().asOptional(FrontendToolsComponent)?.getGlobals(); + if (globals) { + globals.set('flash', () => ''); + } + } + public async checkSecuritySettings(): Promise { this.checkSecurityConfigField('session.secret'); if (!config.get('session.cookie.secure')) { @@ -22,7 +31,7 @@ export default class SessionComponent extends ApplicationComponent { } } - public async init(router: Router): Promise { + public async initRoutes(router: Router): Promise { router.use(session({ saveUninitialized: true, secret: config.get('session.secret'), diff --git a/src/components/WebSocketServerComponent.ts b/src/components/WebSocketServerComponent.ts index b72f722..ec11a64 100644 --- a/src/components/WebSocketServerComponent.ts +++ b/src/components/WebSocketServerComponent.ts @@ -7,29 +7,33 @@ import WebSocket from "ws"; import Application from "../Application.js"; import ApplicationComponent from "../ApplicationComponent.js"; -import ViewEngine from "../frontend/ViewEngine.js"; import {logger} from "../Logger.js"; import WebSocketListener from "../WebSocketListener.js"; import ExpressAppComponent from "./ExpressAppComponent.js"; +import FrontendToolsComponent from "./FrontendToolsComponent.js"; import RedisComponent from "./RedisComponent.js"; export default class WebSocketServerComponent extends ApplicationComponent { private wss?: WebSocket.Server; - public constructor( - private readonly application: Application, - private readonly expressAppComponent: ExpressAppComponent, - private readonly storeComponent: RedisComponent, - ) { - super(); + public async init(): Promise { + const app = this.getApp(); - ViewEngine.setGlobal('websocketUrl', config.get('public_websocket_url')); + app.require(ExpressAppComponent); + app.require(RedisComponent); + + const globals = app.asOptional(FrontendToolsComponent)?.getGlobals(); + if (globals) { + globals.set('websocketUrl', config.get('public_websocket_url')); + } } public async start(_app: Express): Promise { - const listeners: { [p: string]: WebSocketListener } = this.application.getWebSocketListeners(); + const app = this.getApp(); + + const listeners: { [p: string]: WebSocketListener } = app.getWebSocketListeners(); this.wss = new WebSocket.Server({ - server: this.expressAppComponent.getServer(), + server: app.as(ExpressAppComponent).getServer(), }, () => { logger.info(`Websocket server started over webserver.`); }).on('error', (err) => { @@ -57,7 +61,7 @@ export default class WebSocketServerComponent extends ApplicationComponent { return; } - const store = this.storeComponent.getStore(); + const store = app.as(RedisComponent).getStore(); store.get(sid, (err, session) => { if (err || !session) { logger.error(err, 'Error while initializing session in websocket.'); diff --git a/src/frontend/AssetPreCompiler.ts b/src/frontend/AssetPreCompiler.ts index 6f9b9e0..8176ef8 100644 --- a/src/frontend/AssetPreCompiler.ts +++ b/src/frontend/AssetPreCompiler.ts @@ -4,9 +4,11 @@ import path from "path"; import {logger} from "../Logger.js"; import {doesFileExist, listFilesRecursively} from "../Utils.js"; +import Globals from "./Globals.js"; export default abstract class AssetPreCompiler { protected readonly assetPaths: string[]; + private globals?: Globals; private watcher?: FSWatcher; private afterPreCompileHandlers: ((watch: boolean) => Promise)[] = []; private inputChangeHandler?: (restart: boolean) => Promise; @@ -51,6 +53,19 @@ export default abstract class AssetPreCompiler { return this.outputToPublicDir; } + public getViewPaths(): string[] { + return this.assetPaths; + } + + protected getGlobals(): Globals { + if (!this.globals) throw new Error('globals field not intialized.'); + return this.globals; + } + + public setGlobals(globals: Globals): void { + this.globals = globals; + } + public async stop(): Promise { if (this.watcher) { await this.watcher.close(); @@ -58,10 +73,6 @@ export default abstract class AssetPreCompiler { } } - public getViewPaths(): string[] { - return this.assetPaths; - } - public abstract preCompile(canonicalName: string, alsoCompileDependents: boolean): Promise; public onPreCompile(afterPreCompileHandler: (watch: boolean) => Promise): void { diff --git a/src/frontend/Globals.ts b/src/frontend/Globals.ts new file mode 100644 index 0000000..ed237da --- /dev/null +++ b/src/frontend/Globals.ts @@ -0,0 +1,11 @@ +export default class Globals { + private readonly globals: Record = {}; + + public get(): Record { + return {...this.globals}; + } + + public set(key: string, value: unknown): void { + this.globals[key] = value; + } +} diff --git a/src/frontend/SvelteViewEngine.ts b/src/frontend/SvelteViewEngine.ts index efbc307..3a9b9dc 100644 --- a/src/frontend/SvelteViewEngine.ts +++ b/src/frontend/SvelteViewEngine.ts @@ -278,7 +278,7 @@ export default class SvelteViewEngine extends ViewEngine { // Load locals into locals store const localsModulePath = "../../build/ts/stores.js"; const localsModule = await import(localsModulePath); - const locals = ViewEngine.getGlobals(); + const locals = this.getGlobals().get(); const localMap = this.compileBackendCalls(backendCalls, locals, true); localsModule.locals.set((key: string, args: string) => { return localMap[args ? diff --git a/src/frontend/ViewEngine.ts b/src/frontend/ViewEngine.ts index 30a344c..2df27e5 100644 --- a/src/frontend/ViewEngine.ts +++ b/src/frontend/ViewEngine.ts @@ -3,17 +3,6 @@ import {Express} from "express"; import AssetPreCompiler from "./AssetPreCompiler.js"; export default abstract class ViewEngine extends AssetPreCompiler { - private static readonly globals: Record = {}; - - public static getGlobals(): Record { - return {...this.globals}; - } - - public static setGlobal(key: string, value: unknown): void { - this.globals[key] = value; - } - - protected constructor( targetDir: string, assetType: string, @@ -32,7 +21,7 @@ export default abstract class ViewEngine extends AssetPreCompiler { public setup(app: Express, main: boolean): void { app.engine(this.extension, (path, options, callback) => { // Props (locals) - const locals = Object.assign(options, ViewEngine.getGlobals()); + const locals = Object.assign(options, this.getGlobals().get()); this.render(path, locals) .then(value => callback(null, value))