swaf/src/Application.ts

213 lines
7.5 KiB
TypeScript
Raw Normal View History

import express, {NextFunction, Request, Response, Router} from 'express';
import {BadRequestError, HttpError, NotFoundHttpError, ServerError, ServiceUnavailableHttpError} from "./HttpError";
2020-04-22 15:52:17 +02:00
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";
2020-06-14 11:43:00 +02:00
import LogRequestsComponent from "./components/LogRequestsComponent";
import {ValidationBag} from "./db/Validator";
2020-06-27 17:11:31 +02:00
import config from "config";
import TemplateError = lib.TemplateError;
2020-04-22 15:52:17 +02:00
export default abstract class Application {
private readonly version: string;
private readonly ignoreCommandLine: boolean;
2020-04-22 15:52:17 +02:00
private readonly controllers: Controller[] = [];
private readonly webSocketListeners: { [p: string]: WebSocketListener } = {};
private readonly components: ApplicationComponent<any>[] = [];
private ready: boolean = false;
protected constructor(version: string, ignoreCommandLine: boolean = false) {
2020-04-22 15:52:17 +02:00
this.version = version;
this.ignoreCommandLine = ignoreCommandLine;
2020-04-22 15:52:17 +02:00
}
protected abstract getMigrations(): Type<Migration>[];
2020-04-22 15:52:17 +02:00
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 {
thing.setApp(this);
2020-04-22 15:52:17 +02:00
this.components.push(thing);
}
}
public async start(): Promise<void> {
2020-06-27 17:11:31 +02:00
Logger.info(`${config.get('app.name')} v${this.version} - hi`);
2020-04-22 15:52:17 +02:00
process.once('SIGINT', () => {
this.stop().catch(console.error);
});
// Register migrations
MysqlConnectionManager.registerMigrations(this.getMigrations());
// Process command line
if (!this.ignoreCommandLine && await this.processCommandLine()) {
await this.stop();
return;
}
2020-04-22 15:52:17 +02:00
// Register all components and alike
await this.init();
// Init express
const app = express();
const mainRouter = express.Router();
const fileUploadFormRouter = express.Router();
app.use(fileUploadFormRouter);
app.use(mainRouter);
2020-04-22 15:52:17 +02:00
// Error handler
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
if (res.headersSent) {
return next(err);
}
if (err instanceof ValidationBag) {
2020-06-15 12:58:15 +02:00
res.format({
json: () => {
res.status(401);
res.json({
status: 'error',
code: 401,
message: 'Invalid form data',
messages: err.getMessages(),
});
},
text: () => {
res.status(401);
res.send('Error: ' + err.getMessages())
},
html: () => {
req.flash('validation', err.getMessages());
res.redirectBack();
},
});
return;
}
2020-06-14 11:43:00 +02:00
let errorID: string = LogRequestsComponent.logRequest(req, res, err, '500 Internal Error', err instanceof BadRequestError || err instanceof ServiceUnavailableHttpError);
2020-04-22 15:52:17 +02:00
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, mainRouter);
2020-04-22 15:52:17 +02:00
}
// Routes
this.routes(mainRouter, fileUploadFormRouter);
2020-04-22 15:52:17 +02:00
this.ready = true;
}
protected async processCommandLine(): Promise<boolean> {
const args = process.argv;
for (let i = 2; i < args.length; i++) {
switch (args[i]) {
case '--verbose':
Logger.verbose();
break;
2020-06-14 11:43:00 +02:00
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;
}
2020-04-22 15:52:17 +02:00
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(mainRootRouter: Router, rootFileUploadFormRouter: Router) {
2020-04-22 15:52:17 +02:00
for (const controller of this.controllers) {
if (controller.hasGlobalHandlers()) {
controller.setupGlobalHandlers(mainRootRouter);
2020-04-22 15:52:17 +02:00
Logger.info(`Registered global middlewares for controller ${controller.constructor.name}`);
}
}
for (const controller of this.controllers) {
const {mainRouter, fileUploadFormRouter} = controller.setupRoutes();
mainRootRouter.use(controller.getRoutesPrefix(), mainRouter);
rootFileUploadFormRouter.use(controller.getRoutesPrefix(), fileUploadFormRouter);
2020-04-22 15:52:17 +02:00
Logger.info(`> Registered routes for controller ${controller.constructor.name}`);
}
mainRootRouter.use((req: Request) => {
2020-04-22 15:52:17 +02:00
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;
}
2020-04-22 15:52:17 +02:00
}