Properly split routing in 2 steps: init, handle

This commit is contained in:
Alice Gaudon 2020-07-11 11:46:16 +02:00
parent 883ec737db
commit 0e96a285ac
17 changed files with 77 additions and 97 deletions

View File

@ -64,16 +64,14 @@ export default abstract class Application {
// Init express // Init express
const app = express(); const app = express();
const mainRouter = express.Router(); const initRouter = express.Router();
const fileUploadFormRouter = express.Router(); const handleRouter = express.Router();
app.use(fileUploadFormRouter); app.use(initRouter);
app.use(mainRouter); app.use(handleRouter);
// Error handler // Error handlers
app.use((err: any, req: Request, res: Response, next: NextFunction) => { app.use((err: any, req: Request, res: Response, next: NextFunction) => {
if (res.headersSent) { if (res.headersSent) return next(err);
return next(err);
}
if (err instanceof ValidationBag) { if (err instanceof ValidationBag) {
res.format({ res.format({
@ -135,13 +133,19 @@ export default abstract class Application {
}); });
}); });
// Start all components // Start components
for (const component of this.components) { for (const component of this.components) {
await component.start(app, mainRouter); await component.start(app);
}
// Components routes
for (const component of this.components) {
await component.init(initRouter);
await component.handle(handleRouter);
} }
// Routes // Routes
this.routes(mainRouter, fileUploadFormRouter); this.routes(initRouter, handleRouter);
this.ready = true; this.ready = true;
} }
@ -177,10 +181,10 @@ export default abstract class Application {
Logger.info(`${this.constructor.name} v${this.version} - bye`); Logger.info(`${this.constructor.name} v${this.version} - bye`);
} }
private routes(mainRootRouter: Router, rootFileUploadFormRouter: Router) { private routes(initRouter: Router, handleRouter: Router) {
for (const controller of this.controllers) { for (const controller of this.controllers) {
if (controller.hasGlobalHandlers()) { if (controller.hasGlobalHandlers()) {
controller.setupGlobalHandlers(mainRootRouter); controller.setupGlobalHandlers(handleRouter);
Logger.info(`Registered global middlewares for controller ${controller.constructor.name}`); Logger.info(`Registered global middlewares for controller ${controller.constructor.name}`);
} }
@ -188,13 +192,13 @@ export default abstract class Application {
for (const controller of this.controllers) { for (const controller of this.controllers) {
const {mainRouter, fileUploadFormRouter} = controller.setupRoutes(); const {mainRouter, fileUploadFormRouter} = controller.setupRoutes();
mainRootRouter.use(controller.getRoutesPrefix(), mainRouter); initRouter.use(controller.getRoutesPrefix(), fileUploadFormRouter);
rootFileUploadFormRouter.use(controller.getRoutesPrefix(), fileUploadFormRouter); handleRouter.use(controller.getRoutesPrefix(), mainRouter);
Logger.info(`> Registered routes for controller ${controller.constructor.name}`); Logger.info(`> Registered routes for controller ${controller.constructor.name}`);
} }
mainRootRouter.use((req: Request) => { handleRouter.use((req: Request) => {
throw new NotFoundHttpError('page', req.originalUrl); throw new NotFoundHttpError('page', req.originalUrl);
}); });
} }

View File

@ -7,9 +7,18 @@ export default abstract class ApplicationComponent<T> {
private val?: T; private val?: T;
protected app?: Application; protected app?: Application;
public abstract async start(app: Express, router: Router): Promise<void>; public async start(app: Express): Promise<void> {
}
public abstract async stop(): Promise<void>; public async init(router: Router): Promise<void> {
}
public async handle(router: Router): Promise<void> {
}
public async stop(): Promise<void> {
}
protected export(val: T) { protected export(val: T) {
this.val = val; this.val = val;

View File

@ -1,5 +1,5 @@
import ApplicationComponent from "../ApplicationComponent"; import ApplicationComponent from "../ApplicationComponent";
import {Express, NextFunction, Request, Response, Router} from "express"; import {NextFunction, Request, Response, Router} from "express";
import AuthGuard from "./AuthGuard"; import AuthGuard from "./AuthGuard";
import Controller from "../Controller"; import Controller from "../Controller";
import {ForbiddenHttpError} from "../HttpError"; import {ForbiddenHttpError} from "../HttpError";
@ -12,16 +12,13 @@ export default class AuthComponent extends ApplicationComponent<void> {
this.authGuard = authGuard; this.authGuard = authGuard;
} }
public async start(app: Express, router: Router): Promise<void> { public async init(router: Router): Promise<void> {
router.use(async (req, res, next) => { router.use(async (req, res, next) => {
req.authGuard = this.authGuard; req.authGuard = this.authGuard;
res.locals.user = await req.authGuard.getUserForSession(req.session!); res.locals.user = await req.authGuard.getUserForSession(req.session!);
next(); next();
}); });
} }
public async stop(): Promise<void> {
}
} }
export const REQUIRE_REQUEST_AUTH_MIDDLEWARE = async (req: Request, res: Response, next: NextFunction): Promise<void> => { export const REQUIRE_REQUEST_AUTH_MIDDLEWARE = async (req: Request, res: Response, next: NextFunction): Promise<void> => {

View File

@ -1,20 +1,17 @@
import {Express, Router} from "express"; import {Router} from "express";
import config from "config"; import config from "config";
import * as child_process from "child_process"; import * as child_process from "child_process";
import ApplicationComponent from "../ApplicationComponent"; import ApplicationComponent from "../ApplicationComponent";
import {ForbiddenHttpError} from "../HttpError"; import {ForbiddenHttpError} from "../HttpError";
import Logger from "../Logger"; import Logger from "../Logger";
const ROUTE = '/update/push.json';
export default class AutoUpdateComponent extends ApplicationComponent<void> { export default class AutoUpdateComponent extends ApplicationComponent<void> {
public async start(app: Express, router: Router): Promise<void> { public async init(router: Router): Promise<void> {
router.post(ROUTE, (req, res, next) => { router.post('/update/push.json', (req, res) => {
const token = req.header('X-Gitlab-Token'); const token = req.header('X-Gitlab-Token');
if (!token || token !== config.get<string>('gitlab_webhook_token')) throw new ForbiddenHttpError('Invalid token', req.url); if (!token || token !== config.get<string>('gitlab_webhook_token')) throw new ForbiddenHttpError('Invalid token', req.url);
this.update(req.body.checkout_sha) this.update(req.body).catch(Logger.error);
.catch(Logger.error);
res.json({ res.json({
'status': 'ok', 'status': 'ok',
@ -22,20 +19,14 @@ export default class AutoUpdateComponent extends ApplicationComponent<void> {
}); });
} }
public async stop(): Promise<void> { private async update(params: any) {
} Logger.info('Update params:', params);
private async update(checkout_sha: string) {
await this.app!.stop();
try { try {
Logger.info('Starting auto update...'); Logger.info('Starting auto update...');
// Fetch // Fetch
await this.runCommand(`git fetch`); await this.runCommand(`git pull`);
// Checkout new source
await this.runCommand(`git checkout ${checkout_sha}`);
// Install new dependencies // Install new dependencies
await this.runCommand(`yarn install --production=false`); await this.runCommand(`yarn install --production=false`);
@ -43,13 +34,16 @@ export default class AutoUpdateComponent extends ApplicationComponent<void> {
// Process assets // Process assets
await this.runCommand(`yarn dist`); await this.runCommand(`yarn dist`);
// Stop app
await this.app!.stop();
Logger.info('Success!'); Logger.info('Success!');
} catch (e) { } catch (e) {
Logger.error(e, 'An error occurred while running the auto update.'); Logger.error(e, 'An error occurred while running the auto update.');
} }
} }
private async runCommand(command: string) { private async runCommand(command: string): Promise<void> {
Logger.info(`> ${command}`); Logger.info(`> ${command}`);
Logger.info(child_process.execSync(command).toString()); Logger.info(child_process.execSync(command).toString());
} }

View File

@ -1,5 +1,5 @@
import ApplicationComponent from "../ApplicationComponent"; import ApplicationComponent from "../ApplicationComponent";
import {Express, Router} from "express"; import {Router} from "express";
import crypto from "crypto"; import crypto from "crypto";
import {BadRequestError} from "../HttpError"; import {BadRequestError} from "../HttpError";
@ -10,7 +10,7 @@ export default class CsrfProtectionComponent extends ApplicationComponent<void>
this.routeExcluders.push(excluder); this.routeExcluders.push(excluder);
} }
public async start(app: Express, router: Router): Promise<void> { public async handle(router: Router): Promise<void> {
router.use(async (req, res, next) => { router.use(async (req, res, next) => {
for (const excluder of CsrfProtectionComponent.routeExcluders) { for (const excluder of CsrfProtectionComponent.routeExcluders) {
if (excluder(req.path)) return next(); if (excluder(req.path)) return next();
@ -46,9 +46,6 @@ export default class CsrfProtectionComponent extends ApplicationComponent<void>
next(); next();
}); });
} }
public async stop(): Promise<void> {
}
} }
class InvalidCsrfTokenError extends BadRequestError { class InvalidCsrfTokenError extends BadRequestError {

View File

@ -1,9 +1,7 @@
import ApplicationComponent from "../ApplicationComponent"; import ApplicationComponent from "../ApplicationComponent";
import express, {Express, RequestHandler, Router} from "express"; import express, {Express, Router} from "express";
import Logger from "../Logger"; import Logger from "../Logger";
import {Server} from "http"; import {Server} from "http";
import {IncomingForm} from "formidable";
import {FileError, ValidationBag} from "../db/Validator";
import compression from "compression"; import compression from "compression";
export default class ExpressAppComponent extends ApplicationComponent<void> { export default class ExpressAppComponent extends ApplicationComponent<void> {
@ -15,11 +13,13 @@ export default class ExpressAppComponent extends ApplicationComponent<void> {
this.port = port; this.port = port;
} }
public async start(app: Express, router: Router): Promise<void> { public async start(app: Express): Promise<void> {
this.server = app.listen(this.port, 'localhost', () => { this.server = app.listen(this.port, 'localhost', () => {
Logger.info(`Web server running on localhost:${this.port}.`); Logger.info(`Web server running on localhost:${this.port}.`);
}); });
}
public async init(router: Router): Promise<void> {
router.use(express.json()); router.use(express.json());
router.use(express.urlencoded({ router.use(express.urlencoded({
extended: true, extended: true,

View File

@ -1,8 +1,8 @@
import ApplicationComponent from "../ApplicationComponent"; import ApplicationComponent from "../ApplicationComponent";
import {Express, Router} from "express"; import {Router} from "express";
export default class FormHelperComponent extends ApplicationComponent<void> { export default class FormHelperComponent extends ApplicationComponent<void> {
public async start(app: Express, router: Router): Promise<void> { public async init(router: Router): Promise<void> {
router.use((req, res, next) => { router.use((req, res, next) => {
if (!req.session) { if (!req.session) {
throw new Error('Session is unavailable.'); throw new Error('Session is unavailable.');
@ -41,8 +41,4 @@ export default class FormHelperComponent extends ApplicationComponent<void> {
next(); next();
}); });
} }
public async stop(): Promise<void> {
}
} }

View File

@ -1,7 +1,7 @@
import ApplicationComponent from "../ApplicationComponent"; import ApplicationComponent from "../ApplicationComponent";
import onFinished from "on-finished"; import onFinished from "on-finished";
import Logger from "../Logger"; import Logger from "../Logger";
import {Express, Request, Response, Router} from "express"; import {Request, Response, Router} from "express";
export default class LogRequestsComponent extends ApplicationComponent<void> { export default class LogRequestsComponent extends ApplicationComponent<void> {
private static fullRequests: boolean = false; private static fullRequests: boolean = false;
@ -51,7 +51,7 @@ export default class LogRequestsComponent extends ApplicationComponent<void> {
return ''; return '';
} }
public async start(app: Express, router: Router): Promise<void> { public async init(router: Router): Promise<void> {
router.use((req, res, next) => { router.use((req, res, next) => {
onFinished(res, (err) => { onFinished(res, (err) => {
if (!err) { if (!err) {
@ -61,8 +61,4 @@ export default class LogRequestsComponent extends ApplicationComponent<void> {
next(); next();
}); });
} }
public async stop(): Promise<void> {
}
} }

View File

@ -1,9 +1,9 @@
import ApplicationComponent from "../ApplicationComponent"; import ApplicationComponent from "../ApplicationComponent";
import {Express, Router} from "express"; import {Express} from "express";
import Mail from "../Mail"; import Mail from "../Mail";
export default class MailComponent extends ApplicationComponent<void> { export default class MailComponent extends ApplicationComponent<void> {
public async start(app: Express, router: Router): Promise<void> { public async start(app: Express): Promise<void> {
await this.prepare('Mail connection', () => Mail.prepare()); await this.prepare('Mail connection', () => Mail.prepare());
} }

View File

@ -1,5 +1,5 @@
import ApplicationComponent from "../ApplicationComponent"; import ApplicationComponent from "../ApplicationComponent";
import {Express, NextFunction, Request, Response, Router} from "express"; import {NextFunction, Request, Response, Router} from "express";
import {ServiceUnavailableHttpError} from "../HttpError"; import {ServiceUnavailableHttpError} from "../HttpError";
import Application from "../Application"; import Application from "../Application";
import config from "config"; import config from "config";
@ -14,7 +14,7 @@ export default class MaintenanceComponent extends ApplicationComponent<void> {
this.canServe = canServe; this.canServe = canServe;
} }
public async start(app: Express, router: Router): Promise<void> { public async handle(router: Router): Promise<void> {
router.use((req: Request, res: Response, next: NextFunction) => { router.use((req: Request, res: Response, next: NextFunction) => {
if (res.headersSent) { if (res.headersSent) {
return next(); return next();
@ -34,8 +34,4 @@ export default class MaintenanceComponent extends ApplicationComponent<void> {
next(); next();
}); });
} }
public async stop(): Promise<void> {
}
} }

View File

@ -1,9 +1,9 @@
import ApplicationComponent from "../ApplicationComponent"; import ApplicationComponent from "../ApplicationComponent";
import {Express, Router} from "express"; import {Express} from "express";
import MysqlConnectionManager from "../db/MysqlConnectionManager"; import MysqlConnectionManager from "../db/MysqlConnectionManager";
export default class MysqlComponent extends ApplicationComponent<void> { export default class MysqlComponent extends ApplicationComponent<void> {
public async start(app: Express, router: Router): Promise<void> { public async start(app: Express): Promise<void> {
await this.prepare('Mysql connection', () => MysqlConnectionManager.prepare()); await this.prepare('Mysql connection', () => MysqlConnectionManager.prepare());
} }

View File

@ -1,4 +1,4 @@
import nunjucks from "nunjucks"; import nunjucks, {Environment} from "nunjucks";
import config from "config"; import config from "config";
import {Express, Router} from "express"; import {Express, Router} from "express";
import ApplicationComponent from "../ApplicationComponent"; import ApplicationComponent from "../ApplicationComponent";
@ -7,13 +7,14 @@ import {ServerError} from "../HttpError";
export default class NunjucksComponent extends ApplicationComponent<void> { export default class NunjucksComponent extends ApplicationComponent<void> {
private readonly viewsPath: string; private readonly viewsPath: string;
private env?: Environment;
constructor(viewsPath: string = 'views') { constructor(viewsPath: string = 'views') {
super(); super();
this.viewsPath = viewsPath; this.viewsPath = viewsPath;
} }
public async start(app: Express, router: Router): Promise<void> { public async start(app: Express): Promise<void> {
let coreVersion = 'unknown'; let coreVersion = 'unknown';
try { try {
coreVersion = require('../../package.json').version; coreVersion = require('../../package.json').version;
@ -24,7 +25,7 @@ export default class NunjucksComponent extends ApplicationComponent<void> {
} }
} }
const env = nunjucks.configure(this.viewsPath, { this.env = nunjucks.configure(this.viewsPath, {
autoescape: true, autoescape: true,
express: app, express: app,
noCache: !config.get('view.cache'), noCache: !config.get('view.cache'),
@ -41,9 +42,11 @@ export default class NunjucksComponent extends ApplicationComponent<void> {
return v.toString(16); return v.toString(16);
}); });
app.set('view engine', 'njk'); app.set('view engine', 'njk');
}
public async init(router: Router): Promise<void> {
router.use((req, res, next) => { router.use((req, res, next) => {
req.env = env; req.env = this.env!;
res.locals.url = req.url; res.locals.url = req.url;
res.locals.params = () => req.params; res.locals.params = () => req.params;
@ -52,8 +55,4 @@ export default class NunjucksComponent extends ApplicationComponent<void> {
next(); next();
}); });
} }
public async stop(): Promise<void> {
}
} }

View File

@ -1,11 +1,11 @@
import ApplicationComponent from "../ApplicationComponent"; import ApplicationComponent from "../ApplicationComponent";
import {Express, Router} from "express"; import {Router} from "express";
import onFinished from "on-finished"; import onFinished from "on-finished";
import Logger from "../Logger"; import Logger from "../Logger";
import {ServerError} from "../HttpError"; import {ServerError} from "../HttpError";
export default class RedirectBackComponent extends ApplicationComponent<void> { export default class RedirectBackComponent extends ApplicationComponent<void> {
public async start(app: Express, router: Router): Promise<void> { public async init(router: Router): Promise<void> {
router.use((req, res, next) => { router.use((req, res, next) => {
if (!req.session) { if (!req.session) {
throw new Error('Session is unavailable.'); throw new Error('Session is unavailable.');
@ -36,8 +36,4 @@ export default class RedirectBackComponent extends ApplicationComponent<void> {
next(); next();
}); });
} }
public async stop(): Promise<void> {
}
} }

View File

@ -1,5 +1,5 @@
import ApplicationComponent from "../ApplicationComponent"; import ApplicationComponent from "../ApplicationComponent";
import {Express, Router} from "express"; import {Express} from "express";
import redis, {RedisClient} from "redis"; import redis, {RedisClient} from "redis";
import config from "config"; import config from "config";
import Logger from "../Logger"; import Logger from "../Logger";
@ -12,7 +12,7 @@ export default class RedisComponent extends ApplicationComponent<void> {
private redisClient?: RedisClient; private redisClient?: RedisClient;
private store?: Store; private store?: Store;
public async start(app: Express, router: Router): Promise<void> { public async start(app: Express): Promise<void> {
this.redisClient = redis.createClient(config.get('redis.port'), config.get('redis.host'), { this.redisClient = redis.createClient(config.get('redis.port'), config.get('redis.host'), {
password: config.has('redis.password') ? config.get<string>('redis.password') : undefined, password: config.has('redis.password') ? config.get<string>('redis.password') : undefined,
}); });

View File

@ -1,5 +1,5 @@
import ApplicationComponent from "../ApplicationComponent"; import ApplicationComponent from "../ApplicationComponent";
import express, {Express, Router} from "express"; import express, {Router} from "express";
import {PathParams} from "express-serve-static-core"; import {PathParams} from "express-serve-static-core";
export default class ServeStaticDirectoryComponent extends ApplicationComponent<void> { export default class ServeStaticDirectoryComponent extends ApplicationComponent<void> {
@ -12,7 +12,7 @@ export default class ServeStaticDirectoryComponent extends ApplicationComponent<
this.path = routePath; this.path = routePath;
} }
public async start(app: Express, router: Router): Promise<void> { public async handle(router: Router): Promise<void> {
if (typeof this.path !== 'undefined') { if (typeof this.path !== 'undefined') {
router.use(this.path, express.static(this.root, {maxAge: 1000 * 3600 * 72})); router.use(this.path, express.static(this.root, {maxAge: 1000 * 3600 * 72}));
} else { } else {

View File

@ -3,18 +3,17 @@ import session from "express-session";
import config from "config"; import config from "config";
import RedisComponent from "./RedisComponent"; import RedisComponent from "./RedisComponent";
import flash from "connect-flash"; import flash from "connect-flash";
import {Express, Router} from "express"; import {Router} from "express";
export default class SessionComponent extends ApplicationComponent<void> { export default class SessionComponent extends ApplicationComponent<void> {
private readonly storeComponent: RedisComponent; private readonly storeComponent: RedisComponent;
public constructor(storeComponent: RedisComponent) { public constructor(storeComponent: RedisComponent) {
super(); super();
this.storeComponent = storeComponent; this.storeComponent = storeComponent;
} }
public async start(app: Express, router: Router): Promise<void> { public async init(router: Router): Promise<void> {
router.use(session({ router.use(session({
saveUninitialized: true, saveUninitialized: true,
secret: config.get('session.secret'), secret: config.get('session.secret'),
@ -56,7 +55,4 @@ export default class SessionComponent extends ApplicationComponent<void> {
next(); next();
}); });
} }
public async stop(): Promise<void> {
}
} }

View File

@ -1,5 +1,5 @@
import ApplicationComponent from "../ApplicationComponent"; import ApplicationComponent from "../ApplicationComponent";
import {Express, Request, Router} from "express"; import {Express, Request} from "express";
import WebSocket, {Server as WebSocketServer} from "ws"; import WebSocket, {Server as WebSocketServer} from "ws";
import Logger from "../Logger"; import Logger from "../Logger";
import cookie from "cookie"; import cookie from "cookie";
@ -24,7 +24,7 @@ export default class WebSocketServerComponent extends ApplicationComponent<void>
this.storeComponent = storeComponent; this.storeComponent = storeComponent;
} }
public async start(app: Express, router: Router): Promise<void> { public async start(app: Express): Promise<void> {
const listeners: { [p: string]: WebSocketListener } = this.application.getWebSocketListeners(); const listeners: { [p: string]: WebSocketListener } = this.application.getWebSocketListeners();
this.wss = new WebSocketServer({ this.wss = new WebSocketServer({
server: this.expressAppComponent.getServer(), server: this.expressAppComponent.getServer(),