Adds routing library

parent 5a72a5d4
Pipeline #21119 failed with stages
in 2 minutes and 15 seconds
......@@ -9,82 +9,77 @@
"test": "mocha --opts src/tests/mocha.opts",
"prebuild:DEV": "npm run clean",
"prebuild:PROD": "npm run clean",
"build:DEV": "webpack --config webpack.dev.config.js --mode development --colors",
"build:DEV": "tsc -p tsconfig.json; cp -r assets dist/",
"build:PROD": "webpack --config webpack.prod.config.js --mode development --colors",
"start:NO_DB": "cd ./dist/ && node main.js --no-db"
},
"dependencies": {
"body-parser": "^1.18.2",
"bull": "^3.3.10",
"api-spec-converter": "^2.7.18",
"body-parser": "^1.18.3",
"bull": "^3.5.2",
"cas": "0.0.3",
"compression": "^1.7.2",
"compression": "^1.7.3",
"connect-busboy": "0.0.2",
"cookie-parser": "~1.4.3",
"cors": "^2.8.4",
"cors": "^2.8.5",
"crypto-js": "^3.1.9-1",
"cssstyle": "^0.2.37",
"excel4node": "^1.3.6",
"express": "^4.16.3",
"express-ws": "^3.0.0",
"file-type": "^7.6.0",
"gitlab": "^3.4.4",
"cssstyle": "^1.1.1",
"excel4node": "^1.7.0",
"express": "^4.16.4",
"express-ws": "^4.0.0",
"file-type": "^10.5.0",
"gitlab": "^4.2.7",
"i18n": "^0.8.3",
"jsonwebtoken": "^8.4.0",
"lowdb": "^1.0.0",
"mathjax-node": "^2.1.0",
"messageformat": "^1.1.1",
"mime": "^2.2.2",
"mime-types": "^2.1.18",
"mathjax-node": "^2.1.1",
"messageformat": "^2.0.4",
"mime": "^2.4.0",
"mime-types": "^2.1.21",
"minimist": "^1.2.0",
"morgan": "^1.9.0",
"morgan": "^1.9.1",
"node-sass-middleware": "0.11.0",
"nodemailer": "^4.6.6",
"request": "^2.85.0",
"source-map-support": "^0.5.6",
"ws": "^5.2.0",
"xml2js": "^0.4.19",
"jsonwebtoken": "latest"
"nodemailer": "^4.7.0",
"reflect-metadata": "^0.1.12",
"request": "^2.88.0",
"routing-controllers": "^0.7.7",
"routing-controllers-openapi": "^1.4.2",
"source-map-support": "^0.5.9",
"swagger-ui-express": "^4.0.1",
"ws": "^6.1.2",
"xml2js": "^0.4.19"
},
"devDependencies": {
"@types/body-parser": "1.16.8",
"@types/bull": "^3.3.10",
"@types/body-parser": "1.17.0",
"@types/bull": "^3.4.3",
"@types/busboy": "^0.2.3",
"@types/chai": "^4.1.2",
"@types/chai-http": "3.0.4",
"@types/chai": "^4.1.7",
"@types/chai-http": "3.0.5",
"@types/compression": "^0.0.36",
"@types/cors": "^2.8.3",
"@types/crypto-js": "^3.1.40",
"@types/express": "^4.11.1",
"@types/file-type": "^5.2.1",
"@types/cors": "^2.8.4",
"@types/crypto-js": "^3.1.43",
"@types/express": "^4.16.0",
"@types/file-type": "^5.2.2",
"@types/i18n": "^0.8.3",
"@types/jsonwebtoken": "^7.2.8",
"@types/lowdb": "^1.0.2",
"@types/jsonwebtoken": "^8.3.0",
"@types/lowdb": "^1.0.6",
"@types/minimist": "^1.2.0",
"@types/mocha": "^5.0.0",
"@types/mocha": "^5.2.5",
"@types/morgan": "^1.7.35",
"@types/node": "^9.6.2",
"@types/request": "^2.47.1",
"@types/websocket": "0.0.38",
"@types/ws": "^5.1.1",
"@types/node": "^10.12.10",
"@types/request": "^2.48.1",
"@types/websocket": "0.0.40",
"@types/ws": "^6.0.1",
"@types/xml2js": "^0.4.3",
"arsnova-click-v2-types": "git+https://git.thm.de/arsnova/arsnova-click-v2-types.git",
"chai": "^4.1.2",
"chai-http": "^4.0.0",
"copy-webpack-plugin": "^4.5.1",
"generate-json-webpack-plugin": "^0.3.1",
"html-webpack-plugin": "~3.2.0",
"mocha": "^5.0.5",
"mocha-typescript": "^1.1.12",
"mocha-webpack": "^1.1.0",
"nyc": "^11.6.0",
"start-server-webpack-plugin": "^2.2.5",
"ts-loader": "^4.2.0",
"ts-node": "^5.0.1",
"tslint": "^5.9.1",
"typescript": "^2.8.1",
"webpack": "^4.5.0",
"webpack-cli": "^2.0.14",
"webpack-dev-server": "^3.1.3",
"webpack-node-externals": "^1.7.2",
"webpack-shell-plugin": "^0.5.0"
"chai": "^4.2.0",
"chai-http": "^4.2.0",
"mocha": "^5.2.0",
"mocha-typescript": "^1.1.17",
"nyc": "^13.1.0",
"ts-loader": "^5.3.1",
"ts-node": "^7.0.1",
"tslint": "^5.11.0",
"typescript": "^3.2.1"
}
}
import * as bodyParser from 'body-parser';
import * as compress from 'compression';
import * as busboy from 'connect-busboy';
import * as cors from 'cors';
import * as express from 'express';
import { Request, Response, Router } from 'express';
import * as i18n from 'i18n';
import * as logger from 'morgan';
import * as path from 'path';
import { createExpressServer, RoutingControllersOptions } from 'routing-controllers';
import * as swaggerUi from 'swagger-ui-express';
import options from './cors.config';
import { IGlobal } from './main';
import { apiRouter } from './routes/api';
import { router as expiryQuizRouter } from './routes/expiry-quiz';
import { i18nApiRouter } from './routes/i18n-api';
import { legacyApiRouter } from './routes/legacy-api';
import { libRouter } from './routes/lib';
import { lobbyRouter } from './routes/lobby';
import { memberRouter } from './routes/member';
import { nicksRouter } from './routes/nicks';
import { quizRouter } from './routes/quiz';
import { dynamicStatistics, staticStatistics } from './statistics';
declare var require: any;
......@@ -80,6 +72,20 @@ i18n.configure({
},
});
export const routingControllerOptions: RoutingControllersOptions = {
defaults: {
nullResultCode: 404,
undefinedResultCode: 204,
paramOptions: {
required: true,
},
},
defaultErrorHandler: false,
cors: options,
controllers: [path.join(__dirname, 'routers', '/rest/*.js')],
middlewares: [path.join(__dirname, 'routers', '/middleware/*.js')],
};
// Creates and configures an ExpressJS web server.
class App {
......@@ -92,7 +98,7 @@ class App {
// Run configuration methods on the Express instance.
constructor() {
this._express = express();
this._express = createExpressServer(routingControllerOptions);
this.middleware();
this.routes();
}
......@@ -107,13 +113,15 @@ class App {
extended: true,
}));
this._express.use(i18n.init);
this._express.use(cors(options));
this._express.use(compress());
this._express.options('*', cors(options));
}
// Configure API endpoints.
private routes(): void {
this._express.use('/api/v1/api-docs', swaggerUi.serve, swaggerUi.setup(null, {
swaggerUrl: '/api/v1/api-docs.json',
}));
const router: Router = express.Router();
router.get(`/`, (req: Request, res: Response) => {
res.send(Object.assign({}, staticStatistics, dynamicStatistics()));
......@@ -121,21 +129,11 @@ class App {
router.get(`/err`, () => {
throw new Error('testerror');
});
this._express.use(`${staticStatistics.routePrefix}/`, router);
this._express.use(`${staticStatistics.routePrefix}/lib`, libRouter);
this._express.use(`${staticStatistics.routePrefix}/api`, legacyApiRouter);
this._express.use(`${staticStatistics.routePrefix}/api/v1`, apiRouter);
this._express.use(`${staticStatistics.routePrefix}/api/v1/quiz`, quizRouter);
this._express.use(`${staticStatistics.routePrefix}/api/v1/expiry-quiz`, expiryQuizRouter);
this._express.use(`${staticStatistics.routePrefix}/api/v1/member`, memberRouter);
this._express.use(`${staticStatistics.routePrefix}/api/v1/lobby`, lobbyRouter);
this._express.use(`${staticStatistics.routePrefix}/api/v1/nicks`, nicksRouter);
this._express.use(`${staticStatistics.routePrefix}/api/v1/plugin/i18nator`, i18nApiRouter);
this._express.use((err, req, res, next) => {
(
<IGlobal>global
).createDump(err);
(<IGlobal>global).createDump(err);
next();
});
}
......
......@@ -18,14 +18,14 @@ export abstract class AbstractDAO<T> implements IStorageDAO<T> {
}
protected isEmptyVars(...variables): boolean {
return variables.length > 0 && variables.filter(variable => AbstractDAO.isEmptyVar(variable)).length > 0;
return variables.length > 0 && variables.filter(variable => this.isEmptyVar(variable)).length > 0;
}
private static isEmptyVar(variable: any): boolean {
return typeof variable === 'undefined' || AbstractDAO.getLengthOfVar(variable) === 0;
private isEmptyVar(variable: any): boolean {
return typeof variable === 'undefined' || this.getLengthOfVar(variable) === 0;
}
private static getLengthOfVar(variable: any): number {
private getLengthOfVar(variable: any): number {
switch (typeof variable) {
case 'string':
return variable.length;
......
......@@ -4,6 +4,8 @@ import * as fs from 'fs';
import Gitlab from 'gitlab';
import * as path from 'path';
import { COMMIT_ACTION, GITLAB, LANGUAGES } from '../Enums';
import { IProjectMetaData } from '../interfaces/IProjectMetaData';
import { getProjectMetadata } from '../lib/projectMetaData';
import { availableLangs, i18nFileBaseLocation, projectAppLocation, projectGitLocation, staticStatistics } from '../statistics';
import { AbstractDAO } from './AbstractDAO';
import LoginDAO from './LoginDAO';
......@@ -46,11 +48,7 @@ class I18nDAO extends AbstractDAO<object> {
console.log(`* Fetching unused keys`);
const unusedKeysStart = new Date().getTime();
this.storage[projectName].unused = this.getUnusedKeys({
params: {},
projectAppLocation: projectAppLocation[projectName],
i18nFileBaseLocation: i18nFileBaseLocation[projectName],
});
this.storage[projectName].unused = this.getUnusedKeys(getProjectMetadata(projectName), null);
const unusedKeysEnd = new Date().getTime();
console.log(`-- Done. Took ${unusedKeysEnd - unusedKeysStart}ms`);
......@@ -101,14 +99,15 @@ class I18nDAO extends AbstractDAO<object> {
}
}
public getUnusedKeys(req): object {
public getUnusedKeys(metaData: IProjectMetaData, langRef: string): object {
const result = {};
const fileNames = this.fromDir(req.projectAppLocation, /\.(ts|html|js)$/);
const langRefs = req.params.langRef ? [req.params.langRef] : availableLangs;
const fileNames = this.fromDir(metaData.projectAppLocation, /\.(ts|html|js)$/);
const langRefs = langRef ? [langRef] : availableLangs;
for (let i = 0; i < langRefs.length; i++) {
result[langRefs[i]] = [];
const i18nFileContent = JSON.parse(fs.readFileSync(path.join(req.i18nFileBaseLocation, `${langRefs[i].toLowerCase()}.json`)).toString('UTF-8'));
const i18nFileContent = JSON.parse(
fs.readFileSync(path.join(metaData.i18nFileBaseLocation, `${langRefs[i].toLowerCase()}.json`)).toString('UTF-8'));
const objectPaths = this.objectPath(i18nFileContent);
objectPaths.forEach((
......
export interface IProjectMetaData {
i18nFileBaseLocation: string;
projectBaseLocation: string;
projectAppLocation: string;
projectGitLocation: string;
projectCache: string;
}
import { IProjectMetaData } from '../interfaces/IProjectMetaData';
import { i18nFileBaseLocation, projectAppLocation, projectBaseLocation, projectGitLocation } from '../statistics';
export function getProjectMetadata(project: string): IProjectMetaData {
return {
i18nFileBaseLocation: i18nFileBaseLocation[project],
projectBaseLocation: projectBaseLocation[project],
projectAppLocation: projectAppLocation[project],
projectGitLocation: projectGitLocation[project],
projectCache: project,
};
}
......@@ -16,7 +16,7 @@ import I18nDAO from './db/I18nDAO';
import LoginDAO from './db/LoginDAO';
import MathjaxDAO from './db/MathjaxDAO';
import QuizManagerDAO from './db/QuizManagerDAO';
import { WebSocketRouter } from './routes/websocket';
import { WebSocketRouter } from './routers/websocket/WebSocketRouter';
import { staticStatistics } from './statistics';
import { LoadTester } from './tests/LoadTester';
......@@ -48,11 +48,7 @@ function censor(data: any): any {
let i = 0;
return (key, value) => {
if (i !== 0 && typeof(
data
) === 'object' && typeof(
value
) === 'object' && data === value) {
if (i !== 0 && typeof(data) === 'object' && typeof(value) === 'object' && data === value) {
return '[Circular]';
}
......@@ -67,17 +63,13 @@ function censor(data: any): any {
}
function rejectionToCreateDump(reason): void {
(
<IGlobal>global
).createDump(reason);
(<IGlobal>global).createDump(reason);
}
process.on('unhandledRejection', rejectionToCreateDump);
// process.on('uncaughtException', rejectionToCreateDump); // Throws exceptions when debugging with IntelliJ
(
<IGlobal>global
).DAO = {
(<IGlobal>global).DAO = {
CasDAO,
I18nDAO,
MathjaxDAO,
......@@ -86,9 +78,7 @@ process.on('unhandledRejection', rejectionToCreateDump);
LoginDAO,
ExpiryQuizDAO,
};
(
<IGlobal>global
).createDump = (plainError) => {
(<IGlobal>global).createDump = (plainError) => {
const error = {
type: '',
code: '',
......@@ -112,12 +102,8 @@ process.on('unhandledRejection', rejectionToCreateDump);
const daoDump = { error };
Object.keys((
<IGlobal>global
).DAO).forEach((dao) => {
daoDump[dao] = (
<IGlobal>global
).DAO[dao].createDump();
Object.keys((<IGlobal>global).DAO).forEach((dao) => {
daoDump[dao] = (<IGlobal>global).DAO[dao].createDump();
});
const insecureDumpAsJson = JSON.stringify(daoDump, censor(daoDump));
......@@ -163,29 +149,13 @@ server.on('error', onError);
server.on('listening', onListening);
server.on('close', onClose);
let currentApp = App;
const argv = Minimist(process.argv.slice(2));
if (argv['load-test']) {
runTest();
}
if ((
<IHotModule>module
).hot) {
(
<IHotModule>module
).hot.accept('./main', () => {
server.removeListener('request', currentApp);
currentApp = require('./main');
server.on('request', currentApp);
});
}
function normalizePort(val: number | string): number | string | boolean {
const portCheck: number = (
typeof val === 'string'
) ? parseInt(val, 10) : val;
const portCheck: number = (typeof val === 'string') ? parseInt(val, 10) : val;
if (isNaN(portCheck)) {
return val;
} else if (portCheck >= 0) {
......@@ -199,9 +169,7 @@ function onError(error: NodeJS.ErrnoException): void {
if (error.syscall !== 'listen') {
throw error;
}
const bind: string = (
typeof port === 'string'
) ? 'Pipe ' + port : 'Port ' + port;
const bind: string = (typeof port === 'string') ? 'Pipe ' + port : 'Port ' + port;
switch (error.code) {
case 'EACCESS':
console.error(`${bind} requires elevated privileges`);
......@@ -218,9 +186,7 @@ function onError(error: NodeJS.ErrnoException): void {
function onListening(): void {
const addr: IInetAddress | string = server.address();
const bind: string = (
typeof addr === 'string'
) ? `pipe ${addr}` : `port ${addr.port}`;
const bind: string = (typeof addr === 'string') ? `pipe ${addr}` : `port ${addr.port}`;
console.log(`Listening on ${bind}`);
WebSocketRouter.wss = new WebSocket.Server({ server });
......@@ -237,9 +203,7 @@ function runTest(): void {
if (loadTest.done) {
clearInterval(interval);
console.log(`CPU Time Spent End: ${process.cpuUsage().user / 1000000}`);
console.log(`Load Test took ${(
new Date().getTime() - startTime
) / 1000}`);
console.log(`Load Test took ${(new Date().getTime() - startTime) / 1000}`);
console.log('----- Load Test Finished -----');
}
}, 100);
......
......@@ -7,7 +7,7 @@ import * as WebSocket from 'ws';
import CasDAO from '../db/CasDAO';
import QuizManagerDAO from '../db/QuizManagerDAO';
import illegalNicks from '../nicknames/illegalNicks';
import { WebSocketRouter } from '../routes/websocket';
import { WebSocketRouter } from '../routers/websocket/WebSocketRouter';
export class MemberGroup implements IMemberGroup {
get members(): Array<INickname> {
......
import * as express from 'express';
import { Router } from 'express';
export abstract class AbstractRouter {
get router(): express.Router {
return this._router;
}
protected readonly _router: express.Router;
protected constructor() {
this._router = Router();
}
}
import * as Converter from 'api-spec-converter';
import { Request, Response } from 'express';
import * as fileType from 'file-type';
import * as fs from 'fs';
import { OpenAPIObject } from 'openapi3-ts';
import * as path from 'path';
import { Get, getMetadataArgsStorage, JsonController, NotFoundError, Param, Res } from 'routing-controllers';
import { OpenAPI, routingControllersToSpec } from 'routing-controllers-openapi';
import { routingControllerOptions } from '../../App';
import { homedir, settings, staticStatistics } from '../../statistics';
import { AbstractRouter } from './AbstractRouter';
declare global {
interface IUploadRequest extends Request {
busboy: any;
}
interface I18nResponse extends Response {
__mf: any;
}
}
@JsonController('/api/v1')
export class ApiRouter extends AbstractRouter {
private static _specFile = path.join(homedir, 'spec.json');
private openAPISpec(): OpenAPIObject {
const storage = getMetadataArgsStorage();
return routingControllersToSpec(storage, routingControllerOptions, {
info: {
title: staticStatistics.appName,
version: staticStatistics.appVersion,
},
});
}
private regenerateSpecFile(): void {
const spec = this.openAPISpec();
if (!fs.existsSync(homedir)) {
fs.mkdirSync(homedir, { recursive: true });
}
fs.writeFileSync(ApiRouter._specFile, JSON.stringify(spec));
}
@Get('/')
private getAll(): object {
return {
serverConfig: settings.public,
};
}
@Get('/api-docs.json') //
@OpenAPI({
summary: 'Swagger v2 Spec',
description: 'Generates the Swagger Spec from the OpenAPI Spec',
})
private async swaggerSpec(): Promise<void> {
if (fs.existsSync(ApiRouter._specFile)) {
const statsOfSpec = fs.statSync(ApiRouter._specFile);
const statsOfIndex = fs.statSync(path.join(__dirname, '..', '..', 'main.js'));
if (!statsOfSpec || statsOfSpec.birthtime.getTime() < statsOfIndex.birthtime.getTime()) {
this.regenerateSpecFile();
}
} else {
this.regenerateSpecFile();
}
return new Promise<void>((resolve, reject) => {
Converter.convert({
from: 'openapi_3',
to: 'swagger_2',
source: ApiRouter._specFile,
})
.catch(reason => reject(reason))
.then(converted => {
if (!converted) {
return;
}
resolve(converted.spec);
});
});
}
@Get('/files/:directory/:subdirectory/:fileName')
private getFileByName(
@Param('directory') directory: string, //
@Param('subdirectory') subdirectory: string, //
@Param('fileName') fileName: string,
): object {
const pathToFiles: string = path.join(staticStatistics.pathToAssets, `${directory}`, `${subdirectory}`);
let file = '';
if (fileName.toLowerCase().includes('random')) {
file = this.randomFile(pathToFiles);
} else {
if (!fs.existsSync(path.join(`${pathToFiles}`, `${fileName}`))) {
throw new NotFoundError();
}
file = fileName;
}
return fs.readFileSync(path.join(`${pathToFiles}`, file));
}
@Get('/files/images/theme/:themeName/:fileName')
private getThemeImageFileByName(
@Param('themeName') themeName: string, //
@Param('fileName') fileName: string, //
@Res() res: Response,
): object {
const pathToFiles = path.join(staticStatistics.pathToAssets, 'images', 'theme', `${themeName}`, `${fileName}`);
if (fs.existsSync(pathToFiles)) {
const data: Buffer = fs.readFileSync(pathToFiles);
res.contentType(fileType(data).mime);
return data;
} else {
throw new NotFoundError('File not found');
}
}
private randomFile(dir: string): string {
const items = fs.readdirSync(dir);
return items[Math.floor(Math.random() * items.length)];
}
}
import { COMMUNICATION_PROTOCOL } from 'arsnova-click-v2-types/dist/communication_protocol';
import { IQuestionGroup } from 'arsnova-click-v2-types/dist/questions/interfaces';
import { Request, Response, Router } from 'express';
import { default as DbDAO } from '../db/DbDAO';
import ExpiryQuizDAO from '../db/ExpiryQuizDAO';
import LoginDAO from '../db/LoginDAO';
import QuizManagerDAO from '../db/QuizManagerDAO';
import { DATABASE_TYPE, USER_AUTHORIZATION } from '../Enums';
class ExpiryQuizRouter {
get router(): Router {
return this._router;
}
private readonly _router: Router;
/**
* Initialize the ExpiryQuizRouter
*/
constructor() {
this._router = Router();
this.init();
}
private init(): void {
this._router.get('/', ExpiryQuizRouter.getAll);
this._router.post('/init', ExpiryQuizRouter.initQuiz);
this._router.post('/quiz', ExpiryQuizRouter.postQuiz);
}
private static getAll(req: Request, res: Response): void {
import { BadRequestError, BodyParam, Get, JsonController, Post, UnauthorizedError } from 'routing-controllers';
import { default as DbDAO } from '../../db/DbDAO';
import ExpiryQuizDAO from '../../db/ExpiryQuizDAO';
import LoginDAO from '../../db/LoginDAO';
import QuizManagerDAO from '../../db/QuizManagerDAO';
import { DATABASE_TYPE, USER_AUTHORIZATION } from '../../Enums';
import { AbstractRouter } from './AbstractRouter';
@JsonController('/api/v1/expiry-quiz')
export class ExpiryQuizRouter extends AbstractRouter {
@Get('/')
private getAll(): object {
const quiz: IQuestionGroup = ExpiryQuizDAO.getCurrentQuestionGroup();
const expiry: Date = ExpiryQuizDAO.expiry;
res.json({
return {
quiz,
expiry,
});
};
}
private static initQuiz(req: Request, res: Response): void {
const username = req.body.username;
const token = req.body.token;
const privateKey = req.body.privateKey;
@Post('/init')
private initQuiz(
@BodyParam('username') username: string, //
@BodyParam('token') token: string, //
@BodyParam('privateKey') privateKey: string,
): object {
if (!privateKey || !token || !username || !LoginDAO.validateTokenForUser(username, token) || !LoginDAO.isUserAuthorizedFor(username,
USER_AUTHORIZATION.CREATE_QUIZ_FROM_EXPIRED)) {
res.status(500);
res.end(JSON.stringify({
throw new UnauthorizedError(JSON.stringify({
status: COMMUNICATION_PROTOCOL.STATUS.FAILED,
step: COMMUNICATION_PROTOCOL.AUTHORIZATION.NOT_AUTHORIZED,
}));
return;
}
const baseQuiz = JSON.parse(JSON.stringify(ExpiryQuizDAO.storage));
baseQuiz.hashtag = baseQuiz.hashtag + (
Object.keys(QuizManagerDAO.storage).length + 1
);
baseQuiz.hashtag = baseQuiz.hashtag + (Object.keys(QuizManagerDAO.storage).length + 1);
QuizManagerDAO.initInactiveQuiz(baseQuiz.hashtag);
const readyQuiz = QuizManagerDAO.initActiveQuiz(baseQuiz);
......@@ -66,49 +47,42 @@ class ExpiryQuizRouter {
privateKey: privateKey,
});
res.send({
return {
status: COMMUNICATION_PROTOCOL.STATUS.SUCCESSFUL,
step: COMMUNICATION_PROTOCOL.QUIZ.INIT,
payload: { questionGroup: readyQuiz.originalObject },