Add ApplicationComponent init lifecycle step and unstatic globals

This renames ApplicationComponent (previous) init to initRoutes and handle to handleRoutes
This commit is contained in:
Alice Gaudon 2021-05-13 16:03:59 +02:00
parent cdf95c0c0b
commit a3ebf46b54
20 changed files with 112 additions and 72 deletions

View File

@ -94,8 +94,11 @@ export default abstract class Application implements Extendable<ApplicationCompo
// Register migrations
MysqlConnectionManager.registerMigrations(this.getMigrations());
// Register all components and alike
// Register and initialize all components and alike
await this.init();
for (const component of this.components) {
await component.init?.();
}
// Process command line
if (!this.ignoreCommandLine) {
@ -218,14 +221,14 @@ export default abstract class Application implements Extendable<ApplicationCompo
// Components routes
for (const component of this.components) {
if (component.init) {
if (component.initRoutes) {
component.setCurrentRouter(initRouter);
await component.init(initRouter);
await component.initRoutes(initRouter);
}
if (component.handle) {
if (component.handleRoutes) {
component.setCurrentRouter(handleRouter);
await component.handle(handleRouter);
await component.handleRoutes(handleRouter);
}
component.setCurrentRouter(null);
@ -292,7 +295,6 @@ export default abstract class Application implements Extendable<ApplicationCompo
}
const frontendToolsComponent = this.as(FrontendToolsComponent);
frontendToolsComponent.setupGlobals();
await frontendToolsComponent.preCompileViews(flags.watch);
break;
}

View File

@ -13,11 +13,13 @@ export default abstract class ApplicationComponent {
public async checkSecuritySettings?(): Promise<void>;
public async init?(): Promise<void>;
public async start?(expressApp: Express): Promise<void>;
public async init?(router: Router): Promise<void>;
public async initRoutes?(router: Router): Promise<void>;
public async handle?(router: Router): Promise<void>;
public async handleRoutes?(router: Router): Promise<void>;
public async stop?(): Promise<void>;

View File

@ -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 {

View File

@ -18,7 +18,7 @@ export default class AuthComponent extends ApplicationComponent {
this.authGuard = new AuthGuard(app, ...authMethods);
}
public async init(): Promise<void> {
public async initRoutes(): Promise<void> {
this.use(AuthMiddleware);
}

View File

@ -16,7 +16,7 @@ export default class AutoUpdateComponent extends ApplicationComponent {
this.checkSecurityConfigField('gitlab_webhook_token');
}
public async init(router: Router): Promise<void> {
public async initRoutes(router: Router): Promise<void> {
router.post('/update/push.json', (req, res) => {
const token = req.header('X-Gitlab-Token');
if (!token || token !== config.get<string>('gitlab_webhook_token'))

View File

@ -20,7 +20,7 @@ export default class CsrfProtectionComponent extends ApplicationComponent {
this.excluders.push(excluder);
}
public async handle(router: Router): Promise<void> {
public async handleRoutes(router: Router): Promise<void> {
router.use(async (req, res, next) => {
for (const excluder of CsrfProtectionComponent.excluders) {
if (excluder(req)) return next();

View File

@ -30,7 +30,7 @@ export default class ExpressAppComponent extends ApplicationComponent {
this.expressApp = app;
}
public async init(router: Router): Promise<void> {
public async initRoutes(router: Router): Promise<void> {
router.use(preventContextCorruptionMiddleware(express.json({
type: req => req.headers['content-type']?.match(/^application\/(.+\+)?json$/),
})));

View File

@ -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<void> {
public async initRoutes(router: Router): Promise<void> {
router.use((req, res, next) => {
let _validation: unknown | null;
res.locals.validation = () => {

View File

@ -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<void> {
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<void> {
// Cache public assets
if (config.get<boolean>('asset_cache')) {
@ -55,9 +77,6 @@ export default class FrontendToolsComponent extends ApplicationComponent {
main = false;
}
}
// Add util globals
this.setupGlobals();
}
public async stop(): Promise<void> {
@ -66,7 +85,7 @@ export default class FrontendToolsComponent extends ApplicationComponent {
}
}
public async handle(router: Router): Promise<void> {
public async handleRoutes(router: Router): Promise<void> {
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;
}
}

View File

@ -75,7 +75,7 @@ export default class LogRequestsComponent extends ApplicationComponent {
return '';
}
public async init(router: Router): Promise<void> {
public async initRoutes(router: Router): Promise<void> {
router.use((req, res, next) => {
onFinished(res, (err) => {
if (!err) {

View File

@ -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<string>('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}`);

View File

@ -15,7 +15,7 @@ export default class MaintenanceComponent extends ApplicationComponent {
this.canServe = canServe;
}
public async handle(router: Router): Promise<void> {
public async handleRoutes(router: Router): Promise<void> {
router.use((req: Request, res: Response, next: NextFunction) => {
if (res.headersSent) {
return next();

View File

@ -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<void> {
const globals = this.getApp().asOptional(FrontendToolsComponent)?.getGlobals();
if (globals) {
globals.set('getPreviousUrl', () => null);
}
}
public async handle(router: Router): Promise<void> {
public async handleRoutes(router: Router): Promise<void> {
router.use((req, res, next) => {
req.getPreviousUrl = () => {
let url = req.header('referer');

View File

@ -15,7 +15,7 @@ export default class ServeStaticDirectoryComponent extends ApplicationComponent
this.path = routePath;
}
public async init(router: Router): Promise<void> {
public async initRoutes(router: Router): Promise<void> {
const resolvedRoot = path.resolve(this.root);
if (this.path) {

View File

@ -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<void> {
const globals = this.getApp().asOptional(FrontendToolsComponent)?.getGlobals();
if (globals) {
globals.set('flash', () => '');
}
}
public async checkSecuritySettings(): Promise<void> {
this.checkSecurityConfigField('session.secret');
if (!config.get<boolean>('session.cookie.secure')) {
@ -22,7 +31,7 @@ export default class SessionComponent extends ApplicationComponent {
}
}
public async init(router: Router): Promise<void> {
public async initRoutes(router: Router): Promise<void> {
router.use(session({
saveUninitialized: true,
secret: config.get('session.secret'),

View File

@ -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<void> {
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<void> {
const listeners: { [p: string]: WebSocketListener<Application> } = this.application.getWebSocketListeners();
const app = this.getApp();
const listeners: { [p: string]: WebSocketListener<Application> } = 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.');

View File

@ -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<void>)[] = [];
private inputChangeHandler?: (restart: boolean) => Promise<void>;
@ -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<void> {
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<void>;
public onPreCompile(afterPreCompileHandler: (watch: boolean) => Promise<void>): void {

11
src/frontend/Globals.ts Normal file
View File

@ -0,0 +1,11 @@
export default class Globals {
private readonly globals: Record<string, unknown> = {};
public get(): Record<string, unknown> {
return {...this.globals};
}
public set(key: string, value: unknown): void {
this.globals[key] = value;
}
}

View File

@ -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 ?

View File

@ -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<string, unknown> = {};
public static getGlobals(): Record<string, unknown> {
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))