Commit 00cc97e1 authored by Christopher Fullarton's avatar Christopher Fullarton

Refactors the backend code to make more use of the mongodb. Drops support for the legacy v1 api

parent 124c27b8
......@@ -15,7 +15,6 @@ import { AdminRouter } from './routers/rest/AdminRouter';
import { ApiRouter } from './routers/rest/ApiRouter';
import { ExpiryQuizRouter } from './routers/rest/ExpiryQuizRouter';
import { I18nApiRouter } from './routers/rest/I18nApiRouter';
import { LegacyApiRouter } from './routers/rest/LegacyApi';
import { LibRouter } from './routers/rest/LibRouter';
import { LobbyRouter } from './routers/rest/LobbyRouter';
import { MemberRouter } from './routers/rest/MemberRouter';
......@@ -23,8 +22,6 @@ import { NicksRouter } from './routers/rest/NicksRouter';
import { QuizRouter } from './routers/rest/QuizRouter';
import { dynamicStatistics, staticStatistics } from './statistics';
declare var global: any;
export const routingControllerOptions: RoutingControllersOptions = {
defaults: {
nullResultCode: 405,
......@@ -37,7 +34,7 @@ export const routingControllerOptions: RoutingControllersOptions = {
defaultErrorHandler: false,
cors: options,
controllers: [
AdminRouter, ApiRouter, ExpiryQuizRouter, I18nApiRouter, LegacyApiRouter, LibRouter, LobbyRouter, MemberRouter, NicksRouter, QuizRouter,
AdminRouter, ApiRouter, ExpiryQuizRouter, I18nApiRouter, LibRouter, LobbyRouter, MemberRouter, NicksRouter, QuizRouter,
],
middlewares: [I18nMiddleware],
};
......@@ -103,7 +100,6 @@ class App {
new Integrations.OnUncaughtException(), new Integrations.OnUnhandledRejection(),
],
enabled: process.env.NODE_ENV === 'production',
debug: true,
});
}
}
......
......@@ -3,6 +3,7 @@ import { settings } from '../statistics';
class AMQPConnector {
private static _instance: AMQPConnector;
public readonly globalExchange: string = 'global';
private _channel: Channel;
......@@ -37,6 +38,13 @@ class AMQPConnector {
this.initConnection();
});
}
public buildQuizExchange(quizname: string): string {
if (!quizname) {
throw new Error(`Could not build exchange name. Quizname '${quizname}' is not supported.`);
}
return encodeURI(`quiz_${quizname.trim()}`);
}
}
export default AMQPConnector.getInstance();
import { EventEmitter } from 'events';
import { IStorageDAO } from '../interfaces/database/IStorageDAO';
export abstract class AbstractDAO<T> implements IStorageDAO<T> {
export abstract class AbstractDAO {
protected static instance;
protected _isInitialized: boolean;
......@@ -10,26 +9,12 @@ export abstract class AbstractDAO<T> implements IStorageDAO<T> {
return this._isInitialized;
}
protected _storage: T;
get storage(): T {
return this._storage;
}
private _updateEmitter = new EventEmitter();
get updateEmitter(): NodeJS.EventEmitter {
return this._updateEmitter;
}
protected constructor(storage: T) {
this._storage = storage;
}
public createDump(): any {
return this.storage;
}
protected isEmptyVars(...variables): boolean {
return variables.length > 0 && variables.filter(variable => this.isEmptyVar(variable)).length > 0;
}
......
import { ObjectID, ObjectId } from 'bson';
import { AssetEntity } from '../entities/AssetEntity';
import { DbCollection, DbEvent } from '../enums/DbOperation';
import { IAsset, IAssetSerialized } from '../interfaces/IAsset';
import LoggerService from '../services/LoggerService';
import { DeleteWriteOpResultObject } from 'mongodb';
import { Document } from 'mongoose';
import { IAssetSerialized } from '../interfaces/IAsset';
import { AssetModel, AssetModelItem } from '../models/AssetModel';
import { AbstractDAO } from './AbstractDAO';
import DbDAO from './DbDAO';
class AssetDAO extends AbstractDAO<Array<AssetEntity>> {
class AssetDAO extends AbstractDAO {
constructor() {
super([]);
super();
DbDAO.isDbAvailable.on(DbEvent.Connected, async (isConnected) => {
if (isConnected) {
const cursor = DbDAO.readMany(DbCollection.Assets, {});
cursor.forEach(doc => {
this.addAsset(doc);
}).then(() => LoggerService.info(`${this.constructor.name} initialized with ${this.storage.length} entries`));
}
AssetModel.find().exec().then(assets => {
assets.forEach(asset => this.addAsset(asset));
});
}
......@@ -29,46 +23,32 @@ class AssetDAO extends AbstractDAO<Array<AssetEntity>> {
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 addAsset(document: IAssetSerialized): Promise<Document & AssetModelItem> {
return AssetModel.create(document);
}
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 updateAsset(id: ObjectId, updatedFields: any): Promise<Document & AssetModelItem> {
return AssetModel.findOneAndUpdate(id, updatedFields).exec();
}
public removeAllAssets(): void {
this.storage.forEach(asset => this.updateEmitter.emit(DbEvent.Delete, asset));
this.storage.splice(0, this.storage.length);
public removeAllAssets(): Promise<DeleteWriteOpResultObject['result'] & { deletedCount?: number }> {
return AssetModel.deleteMany({}).exec();
}
public removeAsset(id: ObjectId): void {
this.storage.splice(this.storage.findIndex(asset => asset.id.equals(id)), 1);
public removeAsset(id: ObjectId): Promise<DeleteWriteOpResultObject['result'] & { deletedCount?: number }> {
return AssetModel.deleteOne({ _id: id }).exec();
}
public getAssetByDigest(digest: string): IAsset {
return this.storage.find(val => val.digest === digest);
public getAssetByDigest(digest: string): Promise<AssetModelItem> {
return AssetModel.findOne({ digest }).exec();
}
public getAssetByUrl(url: string): IAsset {
return this.storage.find(asset => asset.url === url);
public getAssetByUrl(url: string): Promise<AssetModelItem> {
return AssetModel.findOne({ url }).exec();
}
private getAssetById(id: ObjectID): IAsset {
return this.storage.find(asset => asset.id.equals(id));
private getAssetById(id: ObjectID): Promise<AssetModelItem> {
return AssetModel.findById({ id }).exec();
}
}
......
import { ICasData } from '../interfaces/users/ICasData';
import { AbstractDAO } from './AbstractDAO';
class CasDAO extends AbstractDAO<{ [key: string]: ICasData }> {
class CasDAO extends AbstractDAO {
private _storage: object = {};
constructor() {
super({});
get storage(): object {
return this._storage;
}
public static getInstance(): CasDAO {
......
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 { DbCollection } from '../enums/DbOperation';
import LoggerService from '../services/LoggerService';
import { AbstractDAO } from './AbstractDAO';
import MongoDBConnector from './MongoDBConnector';
class DbDAO extends AbstractDAO<object> {
class DbDAO extends AbstractDAO {
private static DB_RECONNECT_INTERVAL = 1000 * 60 * 5; // 5 Minutes
public readonly DB = MongoDBConnector.dbName;
......@@ -17,86 +14,19 @@ class DbDAO extends AbstractDAO<object> {
return this._dbCon;
}
private _isDbAvailable = new EventEmitter();
get isDbAvailable(): EventEmitter {
return this._isDbAvailable;
}
private _isConnected = false;
constructor(_storage: object) {
super(_storage);
constructor() {
super();
this.connectToDb();
}
public static getInstance(): DbDAO {
if (!this.instance) {
this.instance = new DbDAO({});
this.instance = new DbDAO();
}
return this.instance;
}
public create(collection: string, elem: IDbObject | object): Promise<InsertOneWriteOpResult<any>> {
if (!this._isConnected || !this._dbCon) {
return;
}
return this._dbCon.collection(collection).insertOne(elem);
}
public readOne(collection: string, query: FilterQuery<any>, options?: FindOneOptions): Promise<any> {
if (!this._isConnected || !this._dbCon) {
return;
}
return this._dbCon.collection(collection).findOne(query, options);
}
public readMany(collection: string, query: FilterQuery<any>): Cursor<any> {
if (!this._isConnected || !this._dbCon) {
return;
}
return this._dbCon.collection(collection).find(query);
}
public updateOne(collection: string, query: FilterQuery<any>, update: object): Promise<UpdateWriteOpResult> {
if (!this._isConnected || !this._dbCon) {
return;
}
return this._dbCon.collection(collection).updateOne(query, { $set: update });
}
public updateMany(collection: string, query: FilterQuery<any>, update: object): Promise<UpdateWriteOpResult> {
if (!this._isConnected || !this._dbCon) {
return;
}
return this._dbCon.collection(collection).updateMany(query, { $set: update });
}
public deleteOne(collection: string, query: FilterQuery<any>): Promise<DeleteWriteOpResultObject> {
if (!this._isConnected || !this._dbCon) {
return;
}
return this._dbCon.collection(collection).deleteOne(query);
}
public deleteMany(collection: string, query: FilterQuery<any>): Promise<DeleteWriteOpResultObject> {
if (!this._isConnected || !this._dbCon) {
return;
}
return this._dbCon.collection(collection).deleteMany(query);
}
public clearStorage(): void {
}
private connectToDb(): Promise<void> {
return MongoDBConnector.connect(this.DB).then((db: Connection) => {
this._dbCon = db;
......@@ -115,20 +45,11 @@ class DbDAO extends AbstractDAO<object> {
});
LoggerService.info(`Db connected`);
this._isConnected = true;
this._isDbAvailable.emit(DbEvent.Connected, true);
db.on('error', () => {
this._isDbAvailable.emit(DbEvent.Connected, false);
});
db.on('error', () => {});
}).catch((error) => {
LoggerService.error(`Db connection failed with error ${error}, will retry in ${DbDAO.DB_RECONNECT_INTERVAL / 1000} seconds`);
this._isConnected = false;
this._isDbAvailable.emit(DbEvent.Connected, false);
setTimeout(this.connectToDb.bind(this), DbDAO.DB_RECONNECT_INTERVAL);
});
}
......
......@@ -6,14 +6,20 @@ import LoggerService from '../services/LoggerService';
import { availableLangs } from '../statistics';
import { AbstractDAO } from './AbstractDAO';
class I18nDAO extends AbstractDAO<object> {
class I18nDAO extends AbstractDAO {
private _storage: object;
get storage(): object {
return this._storage;
}
private readonly mergeRequestTitle = 'WIP: Update i18n keys';
private readonly commitMessage = 'Updates i18n keys';
private readonly gitlabAccessToken = process.env.GITLAB_TOKEN;
constructor(storage: object) {
super(storage);
super();
this._storage = storage;
}
public static getInstance(): I18nDAO {
......
import { AbstractDAO } from './AbstractDAO';
class MathjaxDAO extends AbstractDAO<object> {
class MathjaxDAO extends AbstractDAO {
private _storage: object = {};
constructor() {
super({});
get storage(): object {
return this._storage;
}
public static getInstance(): MathjaxDAO {
......
This diff is collapsed.
......@@ -33,7 +33,7 @@ class MongoDbConnector {
});
AMQPConnector.initConnection().then(() => {
AMQPConnector.channel.assertExchange('global', 'fanout');
AMQPConnector.channel.assertExchange(AMQPConnector.globalExchange, 'fanout');
});
await mongoose.connect(this._mongoURL, {
......@@ -41,6 +41,7 @@ class MongoDbConnector {
autoIndex: true,
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: false,
} as any);
});
}
......
import { ObjectId } from 'bson';
import { UserEntity } from '../entities/UserEntity';
import { DbCollection, DbEvent } from '../enums/DbOperation';
import { DeleteWriteOpResultObject } from 'mongodb';
import { Document } from 'mongoose';
import { UserRole } from '../enums/UserRole';
import { IUserEntity } from '../interfaces/users/IUserEntity';
import { IUserSerialized } from '../interfaces/users/IUserSerialized';
import LoggerService from '../services/LoggerService';
import { UserModel, UserModelItem } from '../models/UserModelItem/UserModel';
import { AuthService } from '../services/AuthService';
import { AbstractDAO } from './AbstractDAO';
import { default as DbDAO } from './DbDAO';
class UserDAO extends AbstractDAO<{ [key: string]: IUserEntity }> {
constructor() {
super({});
DbDAO.isDbAvailable.on(DbEvent.Connected, isConnected => {
if (isConnected) {
const cursor = DbDAO.readMany(DbCollection.Users, {});
cursor.forEach(doc => {
this.initUser(doc);
}).then(() => LoggerService.info(`${this.constructor.name} initialized with ${Object.keys(this.storage).length} entries`));
}
});
}
class UserDAO extends AbstractDAO {
public static getInstance(): UserDAO {
if (typeof this.instance === 'undefined') {
......@@ -30,111 +16,82 @@ class UserDAO extends AbstractDAO<{ [key: string]: IUserEntity }> {
return this.instance;
}
public initUser(user: IUserSerialized): void {
if (typeof this.storage[user.name] !== 'undefined') {
public async initUser(user: IUserSerialized): Promise<Document & UserModelItem> {
if (await UserModel.findOne({ name: user.name }).exec()) {
throw new Error(`Trying to initiate a duplicate login`);
}
this.storage[user.name] = new UserEntity(user);
}
public validateUser(name: string, passwordHash: string): boolean {
if (this.isEmptyVars(name, passwordHash, this.storage[name])) {
return false;
}
return this.storage[name].passwordHash === passwordHash;
return UserModel.create(user);
}
public createDump(): Array<IUserSerialized> {
return Object.keys(this.storage).map(name => {
return this.storage[name].serialize();
public validateUser(name: string, passwordHash: string): Promise<boolean> {
return UserModel.exists({
name,
passwordHash,
});
}
public setTokenForUser(name: string, token: string): void {
this.storage[name].token = token;
public setTokenForUser(name: string, token: string): Promise<Document & UserModelItem> {
return UserModel.updateOne({ name }, { token }).exec();
}
public validateTokenForUser(name: string, token: string): boolean {
if (this.isEmptyVars(name, token, this.storage[name])) {
return false;
}
try {
this.storage[name].validateToken(token);
return true;
} catch (ex) {
LoggerService.error(ex.message);
return false;
}
}
public validateTokenForUser(name: string, token: string): Promise<boolean> {
return UserModel.exists({
name,
token,
$where: function (): boolean {
const decodedToken = AuthService.decodeToken(token);
public getGitlabTokenForUser(name: string, token: string): string {
if (this.isEmptyVars(name, token, this.storage[name])) {
return null;
}
if (typeof decodedToken !== 'object' || !(decodedToken as any).name) {
return false;
}
return this.storage[name].gitlabToken;
return (decodedToken as any).name === name;
},
});
}
public isUserAuthorizedFor(name: string, userAuthorization: UserRole): boolean {
if (this.isEmptyVars(name, userAuthorization, this.storage[name])) {
return null;
}
return this.storage[name].userAuthorizations.includes(userAuthorization);
public async getGitlabTokenForUser(name: string): Promise<string> {
return (await UserModel.findOne({ name }).exec()).gitlabToken;
}
public getUser(name: string): IUserEntity {
if (this.isEmptyVars(name, this.storage[name])) {
return null;
}
return this.storage[name];
public isUserAuthorizedFor(name: string, userAuthorization: UserRole): Promise<boolean> {
return UserModel.exists({
name,
userAuthorizations: { $all: userAuthorization },
});
}
public getUserByTokenHash(tokenHash: string): IUserEntity {
if (this.isEmptyVars(tokenHash)) {
return null;
}
return Object.values(this.storage).find(user => user.tokenHash === tokenHash);
public getUser(name: string): Promise<Document & UserModelItem> {
return UserModel.findOne({ name }).exec();
}
public getUserById(id: ObjectId): IUserEntity {
return Object.values(this.storage).find(val => val.id.equals(id));
public getUserByTokenHash(tokenHash: string): Promise<Document & UserModelItem> {
return UserModel.findOne({ tokenHash }).exec();
}
public clearStorage(): void {
Object.keys(this.storage).forEach(name => delete this.storage[name]);
public getUserById(id: ObjectId): Promise<Document & UserModelItem> {
return UserModel.findOne({ _id: id }).exec();
}
public removeUser(id: ObjectId): void {
delete this.storage[Object.values(this.storage).find(val => val.id.equals(id)).name];
public clearStorage(): Promise<DeleteWriteOpResultObject['result'] & { deletedCount?: number }> {
return UserModel.deleteMany({}).exec();
}
public addUser(user: IUserSerialized): void {
this.storage[user.name] = new UserEntity(user);
public removeUser(id: ObjectId): Promise<DeleteWriteOpResultObject['result'] & { deletedCount?: number }> {
return UserModel.deleteOne({ _id: id }).exec();
}
public updateUser(id: ObjectId, changedFields: IUserSerialized): void {
const originalUser = this.getUserById(id);
if (!originalUser) {
return;
}
const userEntity = new UserEntity(Object.assign({}, originalUser.serialize(), changedFields));
public addUser(user: IUserSerialized): Promise<Document & UserModelItem> {
return UserModel.create(user);
}
if (changedFields.name && originalUser.name !== changedFields.name) {
this.storage[changedFields.name] = userEntity;
delete this.storage[originalUser.name];
} else {
this.storage[originalUser.name] = userEntity;
}
public updateUser(id: ObjectId, changedFields: object): Promise<Document & UserModelItem> {
return UserModel.updateOne({ _id: new ObjectId(id) }, changedFields).exec();
}
public getUserByToken(token: string): IUserEntity {
return Object.values(this.storage).find(val => val.token === token);
public getUserByToken(token: string): Promise<Document & UserModelItem> {
return UserModel.findOne({ token }).exec();
}
}
......
This diff is collapsed.
import { ObjectId } from 'bson';
export abstract class AbstractEntity {
public _id: ObjectId;
get id(): ObjectId {
return this._id;
}
set id(value: ObjectId) {
this._id = value;
}
}
import { ObjectId } from 'bson';
import { IAssetSerialized } from '../interfaces/IAsset';
import { AbstractEntity } from './AbstractEntity';
export class AssetEntity extends AbstractEntity {
private _url: string;
get url(): string {
return this._url;
}
set url(value: string) {
this._url = value;
}
private _digest: string;
get digest(): string {
return this._digest;
}
set digest(value: string) {
this._digest = value;
}
private _data: Uint8Array;
get data(): Uint8Array {
return this._data;
}
set data(value: Uint8Array) {
this._data = value;
}
private _mimeType: string;
get mimeType(): string {
return this._mimeType;
}
set mimeType(value: string) {
this._mimeType = value;
}
constructor(data: IAssetSerialized) {
super();
this._id = new ObjectId(data.id || data._id);
this._url = data.url;
this._digest = data.digest;
this._data = data.data;
this._mimeType = data.mimeType;
}
public serialize(): IAssetSerialized {
return {
id: this.id.toHexString(),
url: this.url,
digest: this.digest,
data: this.data,
mimeType: this.mimeType,
};
}
}
import { ObjectId } from 'bson';
import { UserRole } from '../enums/UserRole';
import { IUserEntity } from '../interfaces/users/IUserEntity';
import { IUserSerialized } from '../interfaces/users/IUserSerialized';
import { AuthService } from '../services/AuthService';
import { AbstractEntity } from './AbstractEntity';
export class UserEntity extends AbstractEntity implements IUserEntity {