Big stuff update

parent e309b338
{
"hashtag": "ABCD Quiz 1",
"isFirstStart": false,
"name": "ABCD Quiz 1",
"questionList": [
{ "hashtag": "ABCD Quiz 1",
{
"isFirstStart": false,
"questionText": "",
"timer": 60,
......@@ -13,27 +12,33 @@
"type": "ABCDSingleChoiceQuestion"
}
],
"configuration": {
"hashtag": "ABCD Quiz 1",
"sessionConfig": {
"music": {
"hashtag": "ABCD Quiz 1",
"isUsingGlobalVolume": false,
"lobbyEnabled": true,
"lobbyTitle": "Song2",
"lobbyVolume": 50,
"countdownRunningEnabled": true,
"countdownRunningTitle": "Song0",
"countdownRunningVolume": 50,
"countdownEndEnabled": true,
"countdownEndTitle": "Song1",
"countdownEndVolume": 100
"enabled": {
"lobby": true,
"countdownRunning": true,
"countdownEnd": true
},
"volumeConfig": {
"global": 50,
"useGlobalVolume": false,
"lobby": 50,
"countdownRunning": 50,
"countdownEnd": 100
},
"titleConfig": {
"lobby": "Song2",
"countdownRunning": "Song0",
"countdownEnd": "Song1"
}
},
"nicks": {
"hashtag": "ABCD Quiz 1",
"selectedValues": [],
"blockIllegal": true,
"restrictToCASLogin": false,
"memberGroups": ["Default"],
"selectedNicks": [],
"blockIllegalNicks": true,
"restrictToCasLogin": false,
"memberGroups": [
"Default"
],
"maxMembersPerGroup": 10,
"autoJoinToGroup": false
},
......
......@@ -9,8 +9,8 @@
"test": "mocha --opts src/tests/mocha.opts",
"prebuild:DEV": "npm run clean",
"prebuild:PROD": "npm run clean",
"build:DEV": "tsc -p tsconfig.json; cp -r assets dist/",
"build:PROD": "tsc -p tsconfig.json; cp -r assets dist/",
"build:DEV": "tsc && cp -r assets dist/",
"build:PROD": "tsc && cp -r assets dist/",
"start:NO_DB": "cd ./dist/ && node main.js --no-db",
"dependency-check": "npx --ignore-existing madge --circular --extensions ts src"
},
......@@ -40,7 +40,6 @@
"minimist": "^1.2.0",
"morgan": "^1.9.1",
"multer": "^1.4.1",
"arsnova-click-v2-types": "git+https://git.thm.de/arsnova/arsnova-click-v2-types.git",
"node-sass-middleware": "0.11.0",
"nodemailer": "^4.7.0",
"reflect-metadata": "^0.1.12",
......@@ -50,11 +49,16 @@
"source-map-support": "^0.5.9",
"swagger-ui-express": "^4.0.1",
"ws": "^6.1.2",
"xml2js": "^0.4.19"
"xml2js": "^0.4.19",
"mongoose": "latest",
"splunk-bunyan-logger": "latest",
"bunyan": "latest",
"typegoose": "latest"
},
"devDependencies": {
"@types/body-parser": "1.17.0",
"@types/bull": "^3.4.3",
"@types/bunyan": "^1.8.5",
"@types/busboy": "^0.2.3",
"@types/chai": "^4.1.7",
"@types/chai-http": "3.0.5",
......@@ -63,14 +67,17 @@
"@types/crypto-js": "^3.1.43",
"@types/express": "^4.16.0",
"@types/file-type": "^5.2.2",
"@types/gitlab": "^2.0.0",
"@types/i18n": "^0.8.3",
"@types/jsonwebtoken": "^8.3.0",
"@types/lowdb": "^1.0.6",
"@types/minimist": "^1.2.0",
"@types/mocha": "^5.2.5",
"@types/mongoose": "^5.3.2",
"@types/morgan": "^1.7.35",
"@types/node": "^10.12.10",
"@types/request": "^2.48.1",
"@types/splunk-bunyan-logger": "^0.9.0",
"@types/websocket": "0.0.40",
"@types/ws": "^6.0.1",
"@types/xml2js": "^0.4.3",
......
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 { RoutingControllersOptions, useExpressServer } from 'routing-controllers';
import * as swaggerUi from 'swagger-ui-express';
import options from './cors.config';
import options from './lib/cors.config';
import { IGlobal } from './main';
import { roleAuthorizationChecker } from './routers/middleware/roleAuthorizationChecker';
import { dynamicStatistics, staticStatistics } from './statistics';
declare var require: any;
declare var global: any;
i18n.configure({
// setup some locales - other locales default to en silently
locales: ['en', 'de', 'it', 'es', 'fr'],
// fall back from Dutch to German
fallbacks: { 'nl': 'de' },
// where to store json files - defaults to './locales' relative to modules directory
directory: path.join(staticStatistics.pathToAssets, 'i18n'),
// watch for changes in json files to reload locale on updates - defaults to false
autoReload: true,
// whether to write new locale information to disk - defaults to true
updateFiles: false,
// sync locale information across all files - defaults to false
syncFiles: false,
// what to use as the indentation unit - defaults to "\t"
indent: '\t',
// setting extension of json files - defaults to '.json' (you might want to set this to '.js' according to webtranslateit)
extension: '.json',
// setting prefix of json files name - default to none ''
// (in case you use different locale files naming scheme (webapp-en.json), rather then just en.json)
prefix: '',
// enable object notation
objectNotation: true,
// setting of log level DEBUG - default to require('debug')('i18n:debug')
logDebugFn: require('debug')('i18n:debug'),
// setting of log level WARN - default to require('debug')('i18n:warn')
logWarnFn: msg => {
console.log('warn', msg);
},
// setting of log level ERROR - default to require('debug')('i18n:error')
logErrorFn: msg => {
console.log('error', msg);
},
// object or [obj1, obj2] to bind the i18n api and current locale to - defaults to null
register: global,
// hash to specify different aliases for i18n's internal methods to apply on the request/response objects (method -> alias).
// note that this will *not* overwrite existing properties with the same name
api: {
'__': 't', // now req.__ becomes req.t
'__n': 'tn', // and req.__n can be called as req.tn
},
});
export const routingControllerOptions: RoutingControllersOptions = {
defaults: {
nullResultCode: 404,
nullResultCode: 405,
undefinedResultCode: 204,
paramOptions: {
required: true,
},
},
defaultErrorHandler: false,
authorizationChecker: roleAuthorizationChecker,
defaultErrorHandler: true,
cors: options,
controllers: [path.join(__dirname, 'routers', '/rest/*.js')],
middlewares: [path.join(__dirname, 'routers', '/middleware/*.js')],
......@@ -98,21 +42,23 @@ class App {
// Run configuration methods on the Express instance.
constructor() {
this._express = createExpressServer(routingControllerOptions);
this._express = express();
this.middleware();
this.routes();
useExpressServer(this._express, routingControllerOptions);
}
// Configure Express middleware.
private middleware(): void {
this._express.use(logger('dev'));
this._express.use(busboy());
this._express.use(bodyParser.json({ limit: '50mb' }));
this._express.use(bodyParser.urlencoded({
limit: '50mb',
extended: true,
}));
this._express.use(i18n.init);
this._express.options('*', cors(options));
this._express.use(compress());
}
......@@ -133,7 +79,9 @@ class App {
this._express.use(`${staticStatistics.routePrefix}/`, router);
this._express.use((err, req, res, next) => {
(<IGlobal>global).createDump(err);
if (process.env.NODE_ENV === 'production') {
(<IGlobal>global).createDump(err);
}
next();
});
}
......
export enum DATABASE_TYPE {
QUIZ = 'quiz', //
ASSETS = 'assets', //
USERS = 'users', //
}
export enum GITLAB {
PROJECT_ID = 3909, //
TARGET_BRANCH = 'master', //
}
export enum COMMIT_ACTION {
CREATE = 'create', //
DELETE = 'delete', //
MOVE = 'move', //
UPDATE = 'update', //
}
export enum LANGUAGES {
DE = 'de', //
EN = 'en', //
FR = 'fr', //
ES = 'es', //
IT = 'it', //
}
export enum USER_AUTHORIZATION {
CREATE_EXPIRED_QUIZ = 'CREATE_EXPIRED_QUIZ', //
CREATE_QUIZ_FROM_EXPIRED = 'CREATE_QUIZ_FROM_EXPIRED', //
CREATE_QUIZ = 'CREATE_QUIZ', //
EDIT_I18N = 'EDIT_I18N', //
}
declare function require(name: string): any;
import * as fs from 'fs';
import * as path from 'path';
import { staticStatistics } from './statistics';
const homedir = require('os').homedir();
function createPath(basePath, pathRelativeToBase): void {
const exists = fs.existsSync(path.join(basePath, pathRelativeToBase));
if (!exists) {
fs.mkdir(path.join(basePath, pathRelativeToBase), (err) => console.log('app_bootstrap:createPathError', err));
}
}
function createAssetPath(pathRelativeToBase): void {
createPath(staticStatistics.pathToAssets, pathRelativeToBase);
}
function createCachePath(pathRelativeToBase): void {
createPath(staticStatistics.pathToCache, pathRelativeToBase);
}
export function createHomePath(): void {
const pathToOutput = path.join(homedir, '.arsnova-click-v2-backend');
if (!fs.existsSync(pathToOutput)) {
fs.mkdirSync(pathToOutput);
}
}
export function createDefaultPaths(): void {
createCachePath('');
createHomePath();
}
import { IStorageDAO } from 'arsnova-click-v2-types/dist/common';
import { EventEmitter } from 'events';
import { IStorageDAO } from '../interfaces/database/IStorageDAO';
export abstract class AbstractDAO<T> implements IStorageDAO<T> {
protected static instance;
......@@ -9,6 +10,12 @@ export abstract class AbstractDAO<T> implements IStorageDAO<T> {
return this._storage;
}
private _updateEmitter = new EventEmitter();
get updateEmitter(): NodeJS.EventEmitter {
return this._updateEmitter;
}
protected constructor(storage: T) {
this._storage = storage;
}
......
import { ObjectID, ObjectId } from 'bson';
import { AssetEntity } from '../entities/AssetEntity';
import { DbCollection, DbEvent } from '../enums/DbOperation';
import { IAsset, IAssetSerialized } from '../interfaces/IAsset';
import { AbstractDAO } from './AbstractDAO';
import DbDAO from './DbDAO';
class AssetDAO extends AbstractDAO<Array<AssetEntity>> {
constructor() {
super([]);
DbDAO.isDbAvailable.on(DbEvent.Connected, async (isConnected) => {
if (isConnected) {
const cursor = DbDAO.readMany(DbCollection.Assets, {});
cursor.forEach(doc => {
this.addAsset(doc);
});
}
});
}
public static getInstance(): AssetDAO {
if (!this.instance) {
this.instance = new AssetDAO();
}
return this.instance;
}
public addAsset(document: IAssetSerialized): void {
if (this.getAssetById(new ObjectId(document.id))) {
throw new Error(`Duplicate asset insertion: (id: ${document.id}, url: ${document.url})`);
}
const asset = new AssetEntity(document);
this.storage.push(asset);
this.updateEmitter.emit(DbEvent.Create, asset);
}
public updateAsset(id: ObjectId, updatedFields: any): void {
const asset = this.getAssetById(id);
if (!asset) {
throw new Error(`Unkown updated quiz: ${id.toHexString()}`);
}
Object.keys(updatedFields).forEach(key => asset[key] = updatedFields[key]);
this.updateEmitter.emit(DbEvent.Change, asset);
}
public removeAllAssets(): void {
this.storage.forEach(asset => this.updateEmitter.emit(DbEvent.Delete, asset));
this.storage.splice(0, this.storage.length);
}
public removeAsset(id: ObjectId): void {
this.storage.splice(this.storage.findIndex(asset => asset.id.equals(id)), 1);
}
public getAssetByDigest(digest: string): IAsset {
return this.storage.find(val => val.digest === digest);
}
public getAssetByUrl(url: string): IAsset {
return this.storage.find(asset => asset.url === url);
}
private getAssetById(id: ObjectID): IAsset {
return this.storage.find(asset => asset.id.equals(id));
}
}
export default AssetDAO.getInstance();
import { ICasData } from 'arsnova-click-v2-types/dist/common';
import { ICasData } from '../interfaces/users/ICasData';
import { AbstractDAO } from './AbstractDAO';
class CasDAO extends AbstractDAO<{ [key: string]: ICasData }> {
constructor() {
super({});
}
public static getInstance(): CasDAO {
if (!this.instance) {
this.instance = new CasDAO({});
this.instance = new CasDAO();
}
return this.instance;
}
......
declare function require(name: string): any;
import * as fs from 'fs';
import * as lowdb from 'lowdb';
import { AdapterSync } from 'lowdb';
import * as FileSync from 'lowdb/adapters/FileSync';
import * as MemoryDb from 'lowdb/adapters/Memory';
import * as Minimist from 'minimist';
import * as path from 'path';
import * as process from 'process';
import { createHomePath } from '../app_bootstrap';
import { DATABASE_TYPE } from '../Enums';
import { EventEmitter } from 'events';
import { Cursor, DeleteWriteOpResultObject, FilterQuery, FindOneOptions, InsertOneWriteOpResult, UpdateWriteOpResult } from 'mongodb';
import { Connection } from 'mongoose';
import { DbCollection, DbEvent } from '../enums/DbOperation';
import { IDbObject } from '../interfaces/database/IDbObject';
import LoggerService from '../services/LoggerService';
import { AbstractDAO } from './AbstractDAO';
import MongoDBConnector from './MongoDBConnector';
const argv = Minimist(process.argv.slice(2));
const homedir = require('os').homedir();
if (createHomePath) {
createHomePath();
} else {
// Reimplement the createHomePath function because it is not defined in mocha
const pathToOutput = path.join(homedir, '.arsnova-click-v2-backend');
if (!fs.existsSync(pathToOutput)) {
fs.mkdirSync(pathToOutput);
class DbDAO extends AbstractDAO<object> {
private static DB_RECONNECT_INTERVAL = 1000 * 60 * 5; // 5 Minutes
public readonly DB = MongoDBConnector.dbName;
private _dbCon: Connection = null;
get dbCon(): Connection {
return this._dbCon;
}
}
// DB Lib: https://github.com/typicode/lowdb
let adapter: AdapterSync;
if (typeof argv.db !== 'undefined' && !argv.db) {
adapter = new MemoryDb('');
} else {
let pathToDb: string;
if (typeof argv.db === 'string') {
pathToDb = path.join(homedir, '.arsnova-click-v2-backend', argv.db);
} else {
pathToDb = path.join(homedir, '.arsnova-click-v2-backend', 'arsnova-click-v2-db-v1.json');
private _isDbAvailable = new EventEmitter();
get isDbAvailable(): EventEmitter {
return this._isDbAvailable;
}
adapter = new FileSync(pathToDb);
}
const db = lowdb(adapter);
class DbDAO extends AbstractDAO<typeof db> {
private _isConnected = false;
constructor() {
super(db);
const state = this.getState();
if (!state[DATABASE_TYPE.QUIZ]) {
this.initDb(DATABASE_TYPE.QUIZ, []);
}
if (!state[DATABASE_TYPE.ASSETS]) {
this.initDb(DATABASE_TYPE.ASSETS, {});
}
if (!state[DATABASE_TYPE.USERS]) {
this.initDb(DATABASE_TYPE.USERS, {});
}
constructor(_storage: object) {
super(_storage);
this.connectToDb();
}
public static getInstance(): DbDAO {
if (!this.instance) {
this.instance = new DbDAO();
this.instance = new DbDAO({});
}
return this.instance;
}
public create(database: DATABASE_TYPE, data: object, ref?: string): void {
if (ref) {
this.storage.set(`${database}.${ref}`, data).write();
} else {
this.storage.get(database).push(data).write();
public create(collection: string, elem: IDbObject | object): Promise<InsertOneWriteOpResult> {
if (!this._isConnected || !this._dbCon) {
return;
}
return this._dbCon.collection(collection).insertOne(elem);
}
public read(database: DATABASE_TYPE, query?: object): object {
if (query) {
return this.storage.get(database).find(query).value();
public readOne(collection: string, query: FilterQuery<any>, options?: FindOneOptions): Promise<any> {
if (!this._isConnected || !this._dbCon) {
return;
}
return this.storage.get(database).value();
return this._dbCon.collection(collection).findOne(query, options);
}
public update(database: DATABASE_TYPE, query: object, update: object): void {
this.storage.get(database).find(query).assign(update).write();
public readMany(collection: string, query: FilterQuery<any>): Cursor<any> {
if (!this._isConnected || !this._dbCon) {
return;
}
return this._dbCon.collection(collection).find(query);
}
public delete(database: DATABASE_TYPE, query: { quizName: string, privateKey: string }): boolean {
const dbContent: any = this.read(database, query);
if (!dbContent || dbContent.privateKey !== query.privateKey) {
return false;
public update(collection: string, query: FilterQuery<any>, update: object): Promise<UpdateWriteOpResult> {
if (!this._isConnected || !this._dbCon) {
return;
}
this.storage.get(database).remove(query).write();
return true;
return this._dbCon.collection(collection).updateOne(query, { $set: update });
}
public closeConnections(): void {
Object.keys(DATABASE_TYPE).forEach((type) => this.closeConnection(DATABASE_TYPE[type]));
public deleteOne(collection: string, query: FilterQuery<any>): Promise<DeleteWriteOpResultObject> {
if (!this._isConnected || !this._dbCon) {
return;
}
return this._dbCon.collection(collection).deleteOne(query);
}
public getState(): typeof lowdb {
return (
<lowdb.LowdbBase<any>>this.storage
).getState();
public deleteMany(collection: string, query: FilterQuery<any>): Promise<DeleteWriteOpResultObject> {
if (!this._isConnected || !this._dbCon) {
return;
}
return this._dbCon.collection(collection).deleteMany(query);
}
private closeConnection(database: DATABASE_TYPE): void {
this.storage.get(database);
public clearStorage(): void {
}
private initDb(type: DATABASE_TYPE, initialValue: any): void {
this.storage.set(type, initialValue).write();
private connectToDb(): Promise<void> {
return MongoDBConnector.connect(this.DB).then((db: Connection) => {
this._dbCon = db;
this._dbCon.useDb(this.DB);
Object.keys(DbCollection).forEach(collection => {
collection = collection.toLowerCase();
if (!this._dbCon.collection(collection)) {
LoggerService.info('Creating not existing collection', collection);
this._dbCon.createCollection(collection, {
validationLevel: 'strict',
validationAction: 'error',
});
}
});
LoggerService.info(`Db connected`);
this._isConnected = true;
this._isDbAvailable.emit(DbEvent.Connected, true);
db.on('error', () => {
this._isDbAvailable.emit(DbEvent.Connected, false);
});