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 TemplateError = lib.TemplateError; import Controller from "./Controller"; 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 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 { 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 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; let logStr = `${req.method} ${req.originalUrl} - `; if (err instanceof BadRequestError || err instanceof ServiceUnavailableHttpError) { logStr += `${err.errorCode} ${err.name}`; errorID = Logger.silentError(err, logStr); } else { errorID = Logger.error(err, logStr + `500 Internal Error`, err); } 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; } 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; } }