157 lines
5.1 KiB
TypeScript
157 lines
5.1 KiB
TypeScript
|
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<any>[] = [];
|
||
|
|
||
|
private ready: boolean = false;
|
||
|
|
||
|
protected constructor(version: string) {
|
||
|
this.version = version;
|
||
|
}
|
||
|
|
||
|
protected abstract async init(): Promise<void>;
|
||
|
|
||
|
protected use(thing: Controller | WebSocketListener | ApplicationComponent<any>) {
|
||
|
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<void> {
|
||
|
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<void> {
|
||
|
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;
|
||
|
}
|
||
|
}
|