import express, {NextFunction, Request, Response, Router} from 'express'; import {BadRequestError, HttpError, NotFoundHttpError, ServerError, ServiceUnavailableHttpError} from "./HttpError"; import {lib} from "nunjucks"; import Logger from "./Logger"; import WebSocketListener from "./WebSocketListener"; import ApplicationComponent from "./ApplicationComponent"; import Controller from "./Controller"; import MysqlConnectionManager from "./db/MysqlConnectionManager"; import Migration from "./db/Migration"; import {Type} from "./Utils"; import LogRequestsComponent from "./components/LogRequestsComponent"; import TemplateError = lib.TemplateError; export default abstract class Application { private readonly version: string; private readonly controllers: Controller[] = []; private readonly webSocketListeners: { [p: string]: WebSocketListener } = {}; private readonly components: ApplicationComponent[] = []; private ready: boolean = false; protected constructor(version: string) { this.version = version; } protected abstract getMigrations(): Type[]; protected abstract async init(): Promise; protected use(thing: Controller | WebSocketListener | ApplicationComponent) { if (thing instanceof Controller) { this.controllers.push(thing); } else if (thing instanceof WebSocketListener) { const path = thing.path(); this.webSocketListeners[path] = thing; Logger.info(`Added websocket listener on ${path}`); } else { thing.setApp(this); this.components.push(thing); } } public async start(): Promise { Logger.info(`${this.constructor.name} v${this.version} - hi`); process.once('SIGINT', () => { this.stop().catch(console.error); }); // Register migrations MysqlConnectionManager.registerMigrations(this.getMigrations()); // Process command line if (await this.processCommandLine()) { await this.stop(); return; } // Register all components and alike await this.init(); // Init express const app = express(); const router = express.Router({}); app.use(router); // Error handler app.use((err: any, req: Request, res: Response, next: NextFunction) => { if (res.headersSent) { return next(err); } let errorID: string = LogRequestsComponent.logRequest(req, res, err, '500 Internal Error', err instanceof BadRequestError || err instanceof ServiceUnavailableHttpError); let httpError: HttpError; if (err instanceof HttpError) { httpError = err; } else if (err instanceof TemplateError && err.cause instanceof HttpError) { httpError = err.cause; } else { httpError = new ServerError('Internal server error.', err); } res.status(httpError.errorCode); res.format({ html: () => { res.render('errors/' + httpError.errorCode + '.njk', { error_code: httpError.errorCode, error_message: httpError.message, error_instructions: httpError.instructions, error_id: errorID, }); }, json: () => { res.json({ status: 'error', code: httpError.errorCode, message: httpError.message, instructions: httpError.instructions, error_id: errorID, }); }, default: () => { res.type('txt').send(`${httpError.errorCode} - ${httpError.message}\n\n${httpError.instructions}\n\nError ID: ${errorID}`); } }); }); // Start all components for (const component of this.components) { await component.start(app, router); } // Routes this.routes(router); this.ready = true; } protected async processCommandLine(): Promise { const args = process.argv; for (let i = 2; i < args.length; i++) { switch (args[i]) { case '--verbose': Logger.verbose(); break; case '--full-http-requests': LogRequestsComponent.logFullHttpRequests(); break; case 'migration': await MysqlConnectionManager.migrationCommand(args.slice(i + 1)); return true; default: Logger.warn('Unrecognized argument', args[i]); return true; } } return false; } async stop(): Promise { Logger.info('Stopping application...'); for (const component of this.components) { await component.stop(); } Logger.info(`${this.constructor.name} v${this.version} - bye`); } private routes(rootRouter: Router) { for (const controller of this.controllers) { if (controller.hasGlobalHandlers()) { controller.setupGlobalHandlers(rootRouter); Logger.info(`Registered global middlewares for controller ${controller.constructor.name}`); } } for (const controller of this.controllers) { const router = express.Router(); controller.setupRoutes(router); rootRouter.use(controller.getRoutesPrefix(), router); Logger.info(`> Registered routes for controller ${controller.constructor.name}`); } rootRouter.use((req: Request) => { throw new NotFoundHttpError('page', req.originalUrl); }); } public getWebSocketListeners(): { [p: string]: WebSocketListener } { return this.webSocketListeners; } public isReady(): boolean { return this.ready; } public getVersion(): string { return this.version; } }