Add many eslint rules and fix all linting issues
This commit is contained in:
parent
8210642684
commit
79d704083a
@ -4,15 +4,106 @@
|
|||||||
"plugins": [
|
"plugins": [
|
||||||
"@typescript-eslint"
|
"@typescript-eslint"
|
||||||
],
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"project": [
|
||||||
|
"./tsconfig.json",
|
||||||
|
"./tsconfig.test.json"
|
||||||
|
]
|
||||||
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:@typescript-eslint/recommended"
|
"plugin:@typescript-eslint/recommended"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/no-inferrable-types": 0
|
"indent": [
|
||||||
|
"error",
|
||||||
|
4,
|
||||||
|
{
|
||||||
|
"SwitchCase": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-trailing-spaces": "error",
|
||||||
|
"max-len": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"code": 120,
|
||||||
|
"ignoreTemplateLiterals": true,
|
||||||
|
"ignoreRegExpLiterals": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"semi": "off",
|
||||||
|
"@typescript-eslint/semi": [
|
||||||
|
"error"
|
||||||
|
],
|
||||||
|
"no-extra-semi": "error",
|
||||||
|
"eol-last": "error",
|
||||||
|
"comma-dangle": "off",
|
||||||
|
"@typescript-eslint/comma-dangle": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"arrays": "always-multiline",
|
||||||
|
"objects": "always-multiline",
|
||||||
|
"imports": "always-multiline",
|
||||||
|
"exports": "always-multiline",
|
||||||
|
"functions": "always-multiline",
|
||||||
|
"enums": "always-multiline",
|
||||||
|
"generics": "always-multiline",
|
||||||
|
"tuples": "always-multiline"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-extra-parens": "off",
|
||||||
|
"@typescript-eslint/no-extra-parens": [
|
||||||
|
"error"
|
||||||
|
],
|
||||||
|
"no-nested-ternary": "error",
|
||||||
|
"@typescript-eslint/no-inferrable-types": "off",
|
||||||
|
"@typescript-eslint/explicit-module-boundary-types": "error",
|
||||||
|
"@typescript-eslint/no-unnecessary-condition": "error",
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"argsIgnorePattern": "^_"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "error",
|
||||||
|
"no-useless-return": "error",
|
||||||
|
"no-useless-constructor": "off",
|
||||||
|
"@typescript-eslint/no-useless-constructor": [
|
||||||
|
"error"
|
||||||
|
],
|
||||||
|
"no-return-await": "off",
|
||||||
|
"@typescript-eslint/return-await": [
|
||||||
|
"error",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"@typescript-eslint/explicit-member-accessibility": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"accessibility": "explicit"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"ignorePatterns": [
|
"ignorePatterns": [
|
||||||
"jest.config.js",
|
"jest.config.js",
|
||||||
"dist/**/*"
|
"dist/**/*",
|
||||||
|
"config/**/*"
|
||||||
|
],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"test/**/*"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"max-len": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"code": 120,
|
||||||
|
"ignoreTemplateLiterals": true,
|
||||||
|
"ignoreRegExpLiterals": true,
|
||||||
|
"ignoreStrings": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -1,4 +1,9 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
|
globals: {
|
||||||
|
'ts-jest': {
|
||||||
|
tsconfig: 'tsconfig.test.json',
|
||||||
|
}
|
||||||
|
},
|
||||||
transform: {
|
transform: {
|
||||||
"^.+\\.ts$": "ts-jest"
|
"^.+\\.ts$": "ts-jest"
|
||||||
},
|
},
|
||||||
@ -10,5 +15,5 @@ module.exports = {
|
|||||||
testMatch: [
|
testMatch: [
|
||||||
'**/test/**/*.test.ts'
|
'**/test/**/*.test.ts'
|
||||||
],
|
],
|
||||||
testEnvironment: 'node'
|
testEnvironment: 'node',
|
||||||
};
|
};
|
@ -6,7 +6,7 @@ import WebSocketListener from "./WebSocketListener";
|
|||||||
import ApplicationComponent from "./ApplicationComponent";
|
import ApplicationComponent from "./ApplicationComponent";
|
||||||
import Controller from "./Controller";
|
import Controller from "./Controller";
|
||||||
import MysqlConnectionManager from "./db/MysqlConnectionManager";
|
import MysqlConnectionManager from "./db/MysqlConnectionManager";
|
||||||
import Migration from "./db/Migration";
|
import Migration, {MigrationType} from "./db/Migration";
|
||||||
import {Type} from "./Utils";
|
import {Type} from "./Utils";
|
||||||
import LogRequestsComponent from "./components/LogRequestsComponent";
|
import LogRequestsComponent from "./components/LogRequestsComponent";
|
||||||
import {ValidationBag} from "./db/Validator";
|
import {ValidationBag} from "./db/Validator";
|
||||||
@ -19,7 +19,7 @@ import RedisComponent from "./components/RedisComponent";
|
|||||||
import Extendable from "./Extendable";
|
import Extendable from "./Extendable";
|
||||||
import TemplateError = lib.TemplateError;
|
import TemplateError = lib.TemplateError;
|
||||||
|
|
||||||
export default abstract class Application implements Extendable<ApplicationComponent> {
|
export default abstract class Application implements Extendable<ApplicationComponent | WebSocketListener<Application>> {
|
||||||
private readonly version: string;
|
private readonly version: string;
|
||||||
private readonly ignoreCommandLine: boolean;
|
private readonly ignoreCommandLine: boolean;
|
||||||
private readonly controllers: Controller[] = [];
|
private readonly controllers: Controller[] = [];
|
||||||
@ -34,11 +34,11 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
|||||||
this.ignoreCommandLine = ignoreCommandLine;
|
this.ignoreCommandLine = ignoreCommandLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract getMigrations(): Type<Migration>[];
|
protected abstract getMigrations(): MigrationType<Migration>[];
|
||||||
|
|
||||||
protected abstract async init(): Promise<void>;
|
protected abstract async init(): Promise<void>;
|
||||||
|
|
||||||
protected use(thing: Controller | WebSocketListener<this> | ApplicationComponent) {
|
protected use(thing: Controller | WebSocketListener<this> | ApplicationComponent): void {
|
||||||
if (thing instanceof Controller) {
|
if (thing instanceof Controller) {
|
||||||
thing.setApp(this);
|
thing.setApp(this);
|
||||||
this.controllers.push(thing);
|
this.controllers.push(thing);
|
||||||
@ -88,7 +88,7 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
|||||||
app.use(handleRouter);
|
app.use(handleRouter);
|
||||||
|
|
||||||
// Error handlers
|
// Error handlers
|
||||||
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
|
app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
|
||||||
if (res.headersSent) return next(err);
|
if (res.headersSent) return next(err);
|
||||||
|
|
||||||
if (err instanceof ValidationBag) {
|
if (err instanceof ValidationBag) {
|
||||||
@ -104,17 +104,18 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
|||||||
},
|
},
|
||||||
text: () => {
|
text: () => {
|
||||||
res.status(401);
|
res.status(401);
|
||||||
res.send('Error: ' + err.getMessages())
|
res.send('Error: ' + err.getMessages());
|
||||||
},
|
},
|
||||||
html: () => {
|
html: () => {
|
||||||
req.flash('validation', err.getMessages());
|
req.flash('validation', err.getMessages().toString());
|
||||||
res.redirectBack();
|
res.redirectBack();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let errorID: string = LogRequestsComponent.logRequest(req, res, err, '500 Internal Error', err instanceof BadRequestError || err instanceof ServiceUnavailableHttpError);
|
const errorId: string = LogRequestsComponent.logRequest(req, res, err, '500 Internal Error',
|
||||||
|
err instanceof BadRequestError || err instanceof ServiceUnavailableHttpError);
|
||||||
|
|
||||||
let httpError: HttpError;
|
let httpError: HttpError;
|
||||||
|
|
||||||
@ -123,7 +124,7 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
|||||||
} else if (err instanceof TemplateError && err.cause instanceof HttpError) {
|
} else if (err instanceof TemplateError && err.cause instanceof HttpError) {
|
||||||
httpError = err.cause;
|
httpError = err.cause;
|
||||||
} else {
|
} else {
|
||||||
httpError = new ServerError('Internal server error.', err);
|
httpError = new ServerError('Internal server error.', err instanceof Error ? err : undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(httpError.errorCode);
|
res.status(httpError.errorCode);
|
||||||
@ -133,7 +134,7 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
|||||||
error_code: httpError.errorCode,
|
error_code: httpError.errorCode,
|
||||||
error_message: httpError.message,
|
error_message: httpError.message,
|
||||||
error_instructions: httpError.instructions,
|
error_instructions: httpError.instructions,
|
||||||
error_id: errorID,
|
error_id: errorId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
json: () => {
|
json: () => {
|
||||||
@ -142,12 +143,12 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
|||||||
code: httpError.errorCode,
|
code: httpError.errorCode,
|
||||||
message: httpError.message,
|
message: httpError.message,
|
||||||
instructions: httpError.instructions,
|
instructions: httpError.instructions,
|
||||||
error_id: errorID,
|
error_id: errorId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
default: () => {
|
default: () => {
|
||||||
res.type('txt').send(`${httpError.errorCode} - ${httpError.message}\n\n${httpError.instructions}\n\nError ID: ${errorID}`);
|
res.type('txt').send(`${httpError.errorCode} - ${httpError.message}\n\n${httpError.instructions}\n\nError ID: ${errorId}`);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -262,18 +263,20 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
|||||||
return this.webSocketListeners;
|
return this.webSocketListeners;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCache(): CacheProvider | undefined {
|
public getCache(): CacheProvider | null {
|
||||||
return this.cacheProvider;
|
return this.cacheProvider || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public as<C extends ApplicationComponent>(type: Type<C>): C {
|
public as<C extends ApplicationComponent | WebSocketListener<Application>>(type: Type<C>): C {
|
||||||
const component = this.components.find(component => component.constructor === type);
|
const module = this.components.find(component => component.constructor === type) ||
|
||||||
if (!component) throw new Error(`This app doesn't have a ${type.name} component.`);
|
Object.values(this.webSocketListeners).find(listener => listener.constructor === type);
|
||||||
return component as C;
|
if (!module) throw new Error(`This app doesn't have a ${type.name} component.`);
|
||||||
|
return module as C;
|
||||||
}
|
}
|
||||||
|
|
||||||
public asOptional<C extends ApplicationComponent>(type: Type<C>): C | null {
|
public asOptional<C extends ApplicationComponent | WebSocketListener<Application>>(type: Type<C>): C | null {
|
||||||
const component = this.components.find(component => component.constructor === type);
|
const module = this.components.find(component => component.constructor === type) ||
|
||||||
return component ? component as C : null;
|
Object.values(this.webSocketListeners).find(listener => listener.constructor === type);
|
||||||
|
return module ? module as C : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import {Express, Router} from "express";
|
import {Express, Router} from "express";
|
||||||
import Logger from "./Logger";
|
import Logger from "./Logger";
|
||||||
import {sleep, Type} from "./Utils";
|
import {sleep} from "./Utils";
|
||||||
import Application from "./Application";
|
import Application from "./Application";
|
||||||
import config from "config";
|
import config from "config";
|
||||||
import SecurityError from "./SecurityError";
|
import SecurityError from "./SecurityError";
|
||||||
import Middleware from "./Middleware";
|
import Middleware, {MiddlewareType} from "./Middleware";
|
||||||
|
|
||||||
export default abstract class ApplicationComponent {
|
export default abstract class ApplicationComponent {
|
||||||
private currentRouter?: Router;
|
private currentRouter?: Router;
|
||||||
@ -28,16 +28,16 @@ export default abstract class ApplicationComponent {
|
|||||||
err = null;
|
err = null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
err = e;
|
err = e;
|
||||||
Logger.error(err, `${name} failed to prepare; retrying in 5s...`)
|
Logger.error(err, `${name} failed to prepare; retrying in 5s...`);
|
||||||
await sleep(5000);
|
await sleep(5000);
|
||||||
}
|
}
|
||||||
} while (err);
|
} while (err);
|
||||||
Logger.info(`${name} ready!`);
|
Logger.info(`${name} ready!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async close(thingName: string, thing: any, fn: Function): Promise<void> {
|
protected async close(thingName: string, fn: (callback: (err?: Error | null) => void) => void): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve, reject) => fn.call(thing, (err: any) => {
|
await new Promise((resolve, reject) => fn((err?: Error | null) => {
|
||||||
if (err) reject(err);
|
if (err) reject(err);
|
||||||
else resolve();
|
else resolve();
|
||||||
}));
|
}));
|
||||||
@ -48,13 +48,13 @@ export default abstract class ApplicationComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected checkSecurityConfigField(field: string) {
|
protected checkSecurityConfigField(field: string): void {
|
||||||
if (!config.has(field) || config.get<string>(field) === 'default') {
|
if (!config.has(field) || config.get<string>(field) === 'default') {
|
||||||
throw new SecurityError(`${field} field not configured.`);
|
throw new SecurityError(`${field} field not configured.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected use<M extends Middleware>(middleware: Type<M>): void {
|
protected use<M extends Middleware>(middleware: MiddlewareType<M>): void {
|
||||||
if (!this.currentRouter) throw new Error('Cannot call this method outside init() and handle().');
|
if (!this.currentRouter) throw new Error('Cannot call this method outside init() and handle().');
|
||||||
|
|
||||||
const instance = new middleware(this.getApp());
|
const instance = new middleware(this.getApp());
|
||||||
@ -67,10 +67,6 @@ export default abstract class ApplicationComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getCurrentRouter(): Router | null {
|
|
||||||
return this.currentRouter || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setCurrentRouter(router: Router | null): void {
|
public setCurrentRouter(router: Router | null): void {
|
||||||
this.currentRouter = router || undefined;
|
this.currentRouter = router || undefined;
|
||||||
}
|
}
|
||||||
@ -80,7 +76,7 @@ export default abstract class ApplicationComponent {
|
|||||||
return this.app;
|
return this.app;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setApp(app: Application) {
|
public setApp(app: Application): void {
|
||||||
this.app = app;
|
this.app = app;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,4 +11,4 @@ export default interface CacheProvider {
|
|||||||
* @param ttl in ms
|
* @param ttl in ms
|
||||||
*/
|
*/
|
||||||
remember(key: string, value: string, ttl: number): Promise<void>;
|
remember(key: string, value: string, ttl: number): Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -6,14 +6,18 @@ import Validator, {ValidationBag} from "./db/Validator";
|
|||||||
import FileUploadMiddleware from "./FileUploadMiddleware";
|
import FileUploadMiddleware from "./FileUploadMiddleware";
|
||||||
import * as querystring from "querystring";
|
import * as querystring from "querystring";
|
||||||
import {ParsedUrlQueryInput} from "querystring";
|
import {ParsedUrlQueryInput} from "querystring";
|
||||||
import Middleware from "./Middleware";
|
import Middleware, {MiddlewareType} from "./Middleware";
|
||||||
import {Type} from "./Utils";
|
|
||||||
import Application from "./Application";
|
import Application from "./Application";
|
||||||
|
|
||||||
export default abstract class Controller {
|
export default abstract class Controller {
|
||||||
private static readonly routes: { [p: string]: string } = {};
|
private static readonly routes: { [p: string]: string | undefined } = {};
|
||||||
|
|
||||||
public static route(route: string, params: RouteParams = [], query: ParsedUrlQueryInput = {}, absolute: boolean = false): string {
|
public static route(
|
||||||
|
route: string,
|
||||||
|
params: RouteParams = [],
|
||||||
|
query: ParsedUrlQueryInput = {},
|
||||||
|
absolute: boolean = false,
|
||||||
|
): string {
|
||||||
let path = this.routes[route];
|
let path = this.routes[route];
|
||||||
if (path === undefined) throw new Error(`Unknown route for name ${route}.`);
|
if (path === undefined) throw new Error(`Unknown route for name ${route}.`);
|
||||||
|
|
||||||
@ -29,10 +33,8 @@ export default abstract class Controller {
|
|||||||
}
|
}
|
||||||
path = path.replace(/\/+/g, '/');
|
path = path.replace(/\/+/g, '/');
|
||||||
} else {
|
} else {
|
||||||
for (const key in params) {
|
for (const key of Object.keys(params)) {
|
||||||
if (params.hasOwnProperty(key)) {
|
path = path.replace(new RegExp(`:${key}\\??`), params[key]);
|
||||||
path = path.replace(new RegExp(`:${key}\\??`), params[key]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,10 +66,7 @@ export default abstract class Controller {
|
|||||||
|
|
||||||
public abstract routes(): void;
|
public abstract routes(): void;
|
||||||
|
|
||||||
public setupRoutes(): {
|
public setupRoutes(): { mainRouter: Router, fileUploadFormRouter: Router } {
|
||||||
mainRouter: Router,
|
|
||||||
fileUploadFormRouter: Router
|
|
||||||
} {
|
|
||||||
this.routes();
|
this.routes();
|
||||||
return {
|
return {
|
||||||
mainRouter: this.router,
|
mainRouter: this.router,
|
||||||
@ -75,23 +74,43 @@ export default abstract class Controller {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected use(handler: RequestHandler) {
|
protected use(handler: RequestHandler): void {
|
||||||
this.router.use(handler);
|
this.router.use(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get(path: PathParams, handler: RequestHandler, routeName?: string, ...middlewares: (Type<Middleware>)[]) {
|
protected get(
|
||||||
|
path: PathParams,
|
||||||
|
handler: RequestHandler,
|
||||||
|
routeName?: string,
|
||||||
|
...middlewares: (MiddlewareType<Middleware>)[]
|
||||||
|
): void {
|
||||||
this.handle('get', path, handler, routeName, ...middlewares);
|
this.handle('get', path, handler, routeName, ...middlewares);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected post(path: PathParams, handler: RequestHandler, routeName?: string, ...middlewares: (Type<Middleware>)[]) {
|
protected post(
|
||||||
|
path: PathParams,
|
||||||
|
handler: RequestHandler,
|
||||||
|
routeName?: string,
|
||||||
|
...middlewares: (MiddlewareType<Middleware>)[]
|
||||||
|
): void {
|
||||||
this.handle('post', path, handler, routeName, ...middlewares);
|
this.handle('post', path, handler, routeName, ...middlewares);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected put(path: PathParams, handler: RequestHandler, routeName?: string, ...middlewares: (Type<Middleware>)[]) {
|
protected put(
|
||||||
|
path: PathParams,
|
||||||
|
handler: RequestHandler,
|
||||||
|
routeName?: string,
|
||||||
|
...middlewares: (MiddlewareType<Middleware>)[]
|
||||||
|
): void {
|
||||||
this.handle('put', path, handler, routeName, ...middlewares);
|
this.handle('put', path, handler, routeName, ...middlewares);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected delete(path: PathParams, handler: RequestHandler, routeName?: string, ...middlewares: (Type<Middleware>)[]) {
|
protected delete(
|
||||||
|
path: PathParams,
|
||||||
|
handler: RequestHandler,
|
||||||
|
routeName?: string,
|
||||||
|
...middlewares: (MiddlewareType<Middleware>)[]
|
||||||
|
): void {
|
||||||
this.handle('delete', path, handler, routeName, ...middlewares);
|
this.handle('delete', path, handler, routeName, ...middlewares);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +119,7 @@ export default abstract class Controller {
|
|||||||
path: PathParams,
|
path: PathParams,
|
||||||
handler: RequestHandler,
|
handler: RequestHandler,
|
||||||
routeName?: string,
|
routeName?: string,
|
||||||
...middlewares: (Type<Middleware>)[]
|
...middlewares: (MiddlewareType<Middleware>)[]
|
||||||
): void {
|
): void {
|
||||||
this.registerRoutes(path, handler, routeName);
|
this.registerRoutes(path, handler, routeName);
|
||||||
for (const middleware of middlewares) {
|
for (const middleware of middlewares) {
|
||||||
@ -152,18 +171,19 @@ export default abstract class Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async validate(validationMap: { [p: string]: Validator<any> }, body: any): Promise<void> {
|
protected async validate(
|
||||||
|
validationMap: { [p: string]: Validator<unknown> },
|
||||||
|
body: { [p: string]: unknown },
|
||||||
|
): Promise<void> {
|
||||||
const bag = new ValidationBag();
|
const bag = new ValidationBag();
|
||||||
|
|
||||||
for (const p in validationMap) {
|
for (const p of Object.keys(validationMap)) {
|
||||||
if (validationMap.hasOwnProperty(p)) {
|
try {
|
||||||
try {
|
await validationMap[p].execute(p, body[p], false);
|
||||||
await validationMap[p].execute(p, body[p], false);
|
} catch (e) {
|
||||||
} catch (e) {
|
if (e instanceof ValidationBag) {
|
||||||
if (e instanceof ValidationBag) {
|
bag.addBag(e);
|
||||||
bag.addBag(e);
|
} else throw e;
|
||||||
} else throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,7 +195,7 @@ export default abstract class Controller {
|
|||||||
return this.app;
|
return this.app;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setApp(app: Application) {
|
public setApp(app: Application): void {
|
||||||
this.app = app;
|
this.app = app;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,4 +4,4 @@ export default interface Extendable<ComponentClass> {
|
|||||||
as<C extends ComponentClass>(type: Type<C>): C;
|
as<C extends ComponentClass>(type: Type<C>): C;
|
||||||
|
|
||||||
asOptional<C extends ComponentClass>(type: Type<C>): C | null;
|
asOptional<C extends ComponentClass>(type: Type<C>): C | null;
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ export default abstract class FileUploadMiddleware extends Middleware {
|
|||||||
public async handle(req: Request, res: Response, next: NextFunction): Promise<void> {
|
public async handle(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||||
const form = this.makeForm();
|
const form = this.makeForm();
|
||||||
try {
|
try {
|
||||||
await new Promise<any>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
form.parse(req, (err, fields, files) => {
|
form.parse(req, (err, fields, files) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
@ -32,4 +32,4 @@ export default abstract class FileUploadMiddleware extends Middleware {
|
|||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ export abstract class HttpError extends WrappingError {
|
|||||||
this.instructions = instructions;
|
this.instructions = instructions;
|
||||||
}
|
}
|
||||||
|
|
||||||
get name(): string {
|
public get name(): string {
|
||||||
return this.constructor.name;
|
return this.constructor.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,87 +18,83 @@ export abstract class HttpError extends WrappingError {
|
|||||||
export class BadRequestError extends HttpError {
|
export class BadRequestError extends HttpError {
|
||||||
public readonly url: string;
|
public readonly url: string;
|
||||||
|
|
||||||
constructor(message: string, instructions: string, url: string, cause?: Error) {
|
public constructor(message: string, instructions: string, url: string, cause?: Error) {
|
||||||
super(message, instructions, cause);
|
super(message, instructions, cause);
|
||||||
this.url = url;
|
this.url = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
get errorCode(): number {
|
public get errorCode(): number {
|
||||||
return 400;
|
return 400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UnauthorizedHttpError extends BadRequestError {
|
export class UnauthorizedHttpError extends BadRequestError {
|
||||||
constructor(message: string, url: string, cause?: Error) {
|
public constructor(message: string, url: string, cause?: Error) {
|
||||||
super(message, '', url, cause);
|
super(message, '', url, cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
get errorCode(): number {
|
public get errorCode(): number {
|
||||||
return 401;
|
return 401;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ForbiddenHttpError extends BadRequestError {
|
export class ForbiddenHttpError extends BadRequestError {
|
||||||
constructor(thing: string, url: string, cause?: Error) {
|
public constructor(thing: string, url: string, cause?: Error) {
|
||||||
super(
|
super(
|
||||||
`You don't have access to this ${thing}.`,
|
`You don't have access to this ${thing}.`,
|
||||||
`${url} doesn't belong to *you*.`,
|
`${url} doesn't belong to *you*.`,
|
||||||
url,
|
url,
|
||||||
cause
|
cause,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get errorCode(): number {
|
public get errorCode(): number {
|
||||||
return 403;
|
return 403;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NotFoundHttpError extends BadRequestError {
|
export class NotFoundHttpError extends BadRequestError {
|
||||||
constructor(thing: string, url: string, cause?: Error) {
|
public constructor(thing: string, url: string, cause?: Error) {
|
||||||
super(
|
super(
|
||||||
`${thing.charAt(0).toUpperCase()}${thing.substr(1)} not found.`,
|
`${thing.charAt(0).toUpperCase()}${thing.substr(1)} not found.`,
|
||||||
`${url} doesn't exist or was deleted.`,
|
`${url} doesn't exist or was deleted.`,
|
||||||
url,
|
url,
|
||||||
cause
|
cause,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get errorCode(): number {
|
public get errorCode(): number {
|
||||||
return 404;
|
return 404;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TooManyRequestsHttpError extends BadRequestError {
|
export class TooManyRequestsHttpError extends BadRequestError {
|
||||||
constructor(retryIn: number, cause?: Error) {
|
public constructor(retryIn: number, cause?: Error) {
|
||||||
super(
|
super(
|
||||||
`You're making too many requests!`,
|
`You're making too many requests!`,
|
||||||
`We need some rest. Please retry in ${Math.floor(retryIn / 1000)} seconds.`,
|
`We need some rest. Please retry in ${Math.floor(retryIn / 1000)} seconds.`,
|
||||||
'',
|
'',
|
||||||
cause
|
cause,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get errorCode(): number {
|
public get errorCode(): number {
|
||||||
return 429;
|
return 429;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ServerError extends HttpError {
|
export class ServerError extends HttpError {
|
||||||
constructor(message: string, cause?: Error) {
|
public constructor(message: string, cause?: Error) {
|
||||||
super(message, `Maybe you should contact us; see instructions below.`, cause);
|
super(message, `Maybe you should contact us; see instructions below.`, cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
get errorCode(): number {
|
public get errorCode(): number {
|
||||||
return 500;
|
return 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ServiceUnavailableHttpError extends ServerError {
|
export class ServiceUnavailableHttpError extends ServerError {
|
||||||
constructor(message: string, cause?: Error) {
|
public get errorCode(): number {
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
get errorCode(): number {
|
|
||||||
return 503;
|
return 503;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
102
src/Logger.ts
102
src/Logger.ts
@ -1,17 +1,31 @@
|
|||||||
import config from "config";
|
import config from "config";
|
||||||
import {v4 as uuid} from "uuid";
|
import {v4 as uuid} from "uuid";
|
||||||
import Log from "./models/Log";
|
import Log from "./models/Log";
|
||||||
import {bufferToUUID} from "./Utils";
|
import {bufferToUuid} from "./Utils";
|
||||||
|
import ModelFactory from "./db/ModelFactory";
|
||||||
|
|
||||||
|
export enum LogLevel {
|
||||||
|
ERROR,
|
||||||
|
WARN,
|
||||||
|
INFO,
|
||||||
|
DEBUG,
|
||||||
|
DEV,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LogLevelKeys = keyof typeof LogLevel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: make logger not static
|
||||||
|
*/
|
||||||
export default class Logger {
|
export default class Logger {
|
||||||
private static logLevel: LogLevelKeys = <LogLevelKeys>config.get<string>('log.level');
|
private static logLevel: LogLevel = LogLevel[<LogLevelKeys>config.get<string>('log.level')];
|
||||||
private static dbLogLevel: LogLevelKeys = <LogLevelKeys>config.get<string>('log.db_level');
|
private static dbLogLevel: LogLevel = LogLevel[<LogLevelKeys>config.get<string>('log.db_level')];
|
||||||
private static verboseMode: boolean = config.get<boolean>('log.verbose');
|
private static verboseMode: boolean = config.get<boolean>('log.verbose');
|
||||||
|
|
||||||
public static verbose() {
|
public static verbose(): void {
|
||||||
this.verboseMode = true;
|
this.verboseMode = true;
|
||||||
this.logLevel = <LogLevelKeys>LogLevel[LogLevel[this.logLevel] + 1] || this.logLevel;
|
if (LogLevel[this.logLevel + 1]) this.logLevel++;
|
||||||
this.dbLogLevel = <LogLevelKeys>LogLevel[LogLevel[this.dbLogLevel] + 1] || this.dbLogLevel;
|
if (LogLevel[this.dbLogLevel + 1]) this.dbLogLevel++;
|
||||||
Logger.info('Verbose mode');
|
Logger.info('Verbose mode');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,46 +33,45 @@ export default class Logger {
|
|||||||
return this.verboseMode;
|
return this.verboseMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static silentError(error: Error, ...message: any[]): string {
|
public static silentError(error: Error, ...message: unknown[]): string {
|
||||||
return this.log('ERROR', message, error, true) || '';
|
return this.log(LogLevel.ERROR, message, error, true) || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public static error(error: Error, ...message: any[]): string {
|
public static error(error: Error, ...message: unknown[]): string {
|
||||||
return this.log('ERROR', message, error) || '';
|
return this.log(LogLevel.ERROR, message, error) || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public static warn(...message: any[]) {
|
public static warn(...message: unknown[]): void {
|
||||||
this.log('WARN', message);
|
this.log(LogLevel.WARN, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static info(...message: any[]) {
|
public static info(...message: unknown[]): void {
|
||||||
this.log('INFO', message);
|
this.log(LogLevel.INFO, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static debug(...message: any[]) {
|
public static debug(...message: unknown[]): void {
|
||||||
this.log('DEBUG', message);
|
this.log(LogLevel.DEBUG, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static dev(...message: any[]) {
|
public static dev(...message: unknown[]): void {
|
||||||
this.log('DEV', message);
|
this.log(LogLevel.DEV, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static log(level: LogLevelKeys, message: any[], error?: Error, silent: boolean = false): string | null {
|
private static log(level: LogLevel, message: unknown[], error?: Error, silent: boolean = false): string | null {
|
||||||
const levelIndex = LogLevel[level];
|
if (level <= this.logLevel) {
|
||||||
if (levelIndex <= LogLevel[this.logLevel]) {
|
|
||||||
if (error) {
|
if (error) {
|
||||||
if (levelIndex > LogLevel.ERROR) this.warn(`Wrong log level ${level} with attached error.`);
|
if (level > LogLevel.ERROR) this.warn(`Wrong log level ${level} with attached error.`);
|
||||||
} else {
|
} else {
|
||||||
if (levelIndex <= LogLevel.ERROR) this.warn(`No error attached with log level ${level}.`);
|
if (level <= LogLevel.ERROR) this.warn(`No error attached with log level ${level}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const computedMsg = message.map(v => {
|
const computedMsg = message.map(v => {
|
||||||
if (typeof v === 'string') {
|
if (typeof v === 'string') {
|
||||||
return v;
|
return v;
|
||||||
} else {
|
} else {
|
||||||
return JSON.stringify(v, (key: string, value: any) => {
|
return JSON.stringify(v, (key: string, value: string | unknown[] | Record<string, unknown>) => {
|
||||||
if (value instanceof Object) {
|
if (!Array.isArray(value) && value instanceof Object) {
|
||||||
if (value.type === 'Buffer') {
|
if (value.type === 'Buffer' && typeof value.data === 'string') {
|
||||||
return `Buffer<${Buffer.from(value.data).toString('hex')}>`;
|
return `Buffer<${Buffer.from(value.data).toString('hex')}>`;
|
||||||
} else if (value !== v) {
|
} else if (value !== v) {
|
||||||
return `[object Object]`;
|
return `[object Object]`;
|
||||||
@ -72,65 +85,56 @@ export default class Logger {
|
|||||||
}
|
}
|
||||||
}).join(' ');
|
}).join(' ');
|
||||||
|
|
||||||
const shouldSaveToDB = levelIndex <= LogLevel[this.dbLogLevel];
|
const shouldSaveToDB = level <= this.dbLogLevel;
|
||||||
|
|
||||||
let output = `[${level}] `;
|
let output = `[${level}] `;
|
||||||
const pad = output.length;
|
const pad = output.length;
|
||||||
|
|
||||||
const logID = Buffer.alloc(16);
|
const logId = Buffer.alloc(16);
|
||||||
uuid({}, logID);
|
uuid({}, logId);
|
||||||
let strLogID = bufferToUUID(logID);
|
const strLogId = bufferToUuid(logId);
|
||||||
if (shouldSaveToDB) output += `${strLogID} - `;
|
if (shouldSaveToDB) output += `${strLogId} - `;
|
||||||
|
|
||||||
output += computedMsg.replace(/\n/g, '\n' + ' '.repeat(pad));
|
output += computedMsg.replace(/\n/g, '\n' + ' '.repeat(pad));
|
||||||
|
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case "ERROR":
|
case LogLevel.ERROR:
|
||||||
if (silent || !error) {
|
if (silent || !error) {
|
||||||
console.error(output);
|
console.error(output);
|
||||||
} else {
|
} else {
|
||||||
console.error(output, error);
|
console.error(output, error);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "WARN":
|
case LogLevel.WARN:
|
||||||
console.warn(output);
|
console.warn(output);
|
||||||
break;
|
break;
|
||||||
case "INFO":
|
case LogLevel.INFO:
|
||||||
console.info(output);
|
console.info(output);
|
||||||
break;
|
break;
|
||||||
case "DEBUG":
|
case LogLevel.DEBUG:
|
||||||
case "DEV":
|
case LogLevel.DEV:
|
||||||
console.debug(output);
|
console.debug(output);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldSaveToDB) {
|
if (shouldSaveToDB && ModelFactory.has(Log)) {
|
||||||
const log = Log.create({});
|
const log = Log.create({});
|
||||||
log.setLevel(level);
|
log.setLevel(level);
|
||||||
log.message = computedMsg;
|
log.message = computedMsg;
|
||||||
log.setError(error);
|
log.setError(error);
|
||||||
log.setLogID(logID);
|
log.setLogId(logId);
|
||||||
log.save().catch(err => {
|
log.save().catch(err => {
|
||||||
if (!silent && err.message.indexOf('ECONNREFUSED') < 0) {
|
if (!silent && err.message.indexOf('ECONNREFUSED') < 0) {
|
||||||
console.error({save_err: err, error});
|
console.error({save_err: err, error});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return strLogID;
|
return strLogId;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
|
// disable constructor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LogLevel {
|
|
||||||
ERROR,
|
|
||||||
WARN,
|
|
||||||
INFO,
|
|
||||||
DEBUG,
|
|
||||||
DEV,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LogLevelKeys = keyof typeof LogLevel;
|
|
||||||
|
28
src/Mail.ts
28
src/Mail.ts
@ -7,9 +7,10 @@ import {WrappingError} from "./Utils";
|
|||||||
import mjml2html from "mjml";
|
import mjml2html from "mjml";
|
||||||
import Logger from "./Logger";
|
import Logger from "./Logger";
|
||||||
import Controller from "./Controller";
|
import Controller from "./Controller";
|
||||||
|
import {ParsedUrlQueryInput} from "querystring";
|
||||||
|
|
||||||
export default class Mail {
|
export default class Mail {
|
||||||
private static transporter: Transporter;
|
private static transporter?: Transporter;
|
||||||
|
|
||||||
private static getTransporter(): Transporter {
|
private static getTransporter(): Transporter {
|
||||||
if (!this.transporter) throw new MailError('Mail system was not prepared.');
|
if (!this.transporter) throw new MailError('Mail system was not prepared.');
|
||||||
@ -26,8 +27,8 @@ export default class Mail {
|
|||||||
pass: config.get('mail.password'),
|
pass: config.get('mail.password'),
|
||||||
},
|
},
|
||||||
tls: {
|
tls: {
|
||||||
rejectUnauthorized: !config.get('mail.allow_invalid_tls')
|
rejectUnauthorized: !config.get('mail.allow_invalid_tls'),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -40,11 +41,11 @@ export default class Mail {
|
|||||||
Logger.info(`Mail ready to be distributed via ${config.get('mail.host')}:${config.get('mail.port')}`);
|
Logger.info(`Mail ready to be distributed via ${config.get('mail.host')}:${config.get('mail.port')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static end() {
|
public static end(): void {
|
||||||
if (this.transporter) this.transporter.close();
|
if (this.transporter) this.transporter.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parse(template: string, data: any, textOnly: boolean): string {
|
public static parse(template: string, data: { [p: string]: unknown }, textOnly: boolean): string {
|
||||||
data.text = textOnly;
|
data.text = textOnly;
|
||||||
const nunjucksResult = nunjucks.render(template, data);
|
const nunjucksResult = nunjucks.render(template, data);
|
||||||
if (textOnly) return nunjucksResult;
|
if (textOnly) return nunjucksResult;
|
||||||
@ -60,9 +61,9 @@ export default class Mail {
|
|||||||
|
|
||||||
private readonly template: MailTemplate;
|
private readonly template: MailTemplate;
|
||||||
private readonly options: Options = {};
|
private readonly options: Options = {};
|
||||||
private readonly data: { [p: string]: any };
|
private readonly data: ParsedUrlQueryInput;
|
||||||
|
|
||||||
constructor(template: MailTemplate, data: { [p: string]: any } = {}) {
|
public constructor(template: MailTemplate, data: ParsedUrlQueryInput = {}) {
|
||||||
this.template = template;
|
this.template = template;
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.options.subject = this.template.getSubject(data);
|
this.options.subject = this.template.getSubject(data);
|
||||||
@ -97,7 +98,8 @@ export default class Mail {
|
|||||||
// Set data
|
// Set data
|
||||||
this.data.mail_subject = this.options.subject;
|
this.data.mail_subject = this.options.subject;
|
||||||
this.data.mail_to = this.options.to;
|
this.data.mail_to = this.options.to;
|
||||||
this.data.mail_link = config.get<string>('base_url') + Controller.route('mail', [this.template.template], this.data);
|
this.data.mail_link = config.get<string>('base_url') +
|
||||||
|
Controller.route('mail', [this.template.template], this.data);
|
||||||
this.data.app = config.get('app');
|
this.data.app = config.get('app');
|
||||||
|
|
||||||
// Log
|
// Log
|
||||||
@ -117,9 +119,9 @@ export default class Mail {
|
|||||||
|
|
||||||
export class MailTemplate {
|
export class MailTemplate {
|
||||||
private readonly _template: string;
|
private readonly _template: string;
|
||||||
private readonly subject: (data: any) => string;
|
private readonly subject: (data: { [p: string]: unknown }) => string;
|
||||||
|
|
||||||
constructor(template: string, subject: (data: any) => string) {
|
public constructor(template: string, subject: (data: { [p: string]: unknown }) => string) {
|
||||||
this._template = template;
|
this._template = template;
|
||||||
this.subject = subject;
|
this.subject = subject;
|
||||||
}
|
}
|
||||||
@ -128,13 +130,13 @@ export class MailTemplate {
|
|||||||
return this._template;
|
return this._template;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSubject(data: any): string {
|
public getSubject(data: { [p: string]: unknown }): string {
|
||||||
return `${config.get('app.name')} - ${this.subject(data)}`;
|
return `${config.get('app.name')} - ${this.subject(data)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MailError extends WrappingError {
|
class MailError extends WrappingError {
|
||||||
constructor(message: string = 'An error occurred while sending mail.', cause?: Error) {
|
public constructor(message: string = 'An error occurred while sending mail.', cause?: Error) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,12 @@ import {MailTemplate} from "./Mail";
|
|||||||
|
|
||||||
export const MAGIC_LINK_MAIL = new MailTemplate(
|
export const MAGIC_LINK_MAIL = new MailTemplate(
|
||||||
'magic_link',
|
'magic_link',
|
||||||
data => data.type === 'register' ? 'Registration' : 'Login magic link'
|
data => data.type === 'register' ? 'Registration' : 'Login magic link',
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE: MailTemplate = new MailTemplate(
|
export const ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE: MailTemplate = new MailTemplate(
|
||||||
'account_review_notice',
|
'account_review_notice',
|
||||||
data => `Your account was ${data.approved ? 'approved' : 'rejected'}.`
|
data => `Your account was ${data.approved ? 'approved' : 'rejected'}.`,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE: MailTemplate = new MailTemplate(
|
export const PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE: MailTemplate = new MailTemplate(
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {RequestHandler} from "express";
|
import {RequestHandler} from "express";
|
||||||
import {NextFunction, Request, Response} from "express-serve-static-core";
|
import {NextFunction, Request, Response} from "express-serve-static-core";
|
||||||
import Application from "./Application";
|
import Application from "./Application";
|
||||||
|
import {Type} from "./Utils";
|
||||||
|
|
||||||
export default abstract class Middleware {
|
export default abstract class Middleware {
|
||||||
public constructor(
|
public constructor(
|
||||||
@ -26,3 +27,7 @@ export default abstract class Middleware {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MiddlewareType<M extends Middleware> extends Type<M> {
|
||||||
|
new(app: Application): M;
|
||||||
|
}
|
||||||
|
@ -6,7 +6,7 @@ export default class Pagination<T extends Model> {
|
|||||||
public readonly perPage: number;
|
public readonly perPage: number;
|
||||||
public readonly totalCount: number;
|
public readonly totalCount: number;
|
||||||
|
|
||||||
constructor(models: T[], page: number, perPage: number, totalCount: number) {
|
public constructor(models: T[], page: number, perPage: number, totalCount: number) {
|
||||||
this.models = models;
|
this.models = models;
|
||||||
this.page = page;
|
this.page = page;
|
||||||
this.perPage = perPage;
|
this.perPage = perPage;
|
||||||
@ -21,4 +21,4 @@ export default class Pagination<T extends Model> {
|
|||||||
return this.models.length >= this.perPage && this.page * this.perPage < this.totalCount;
|
return this.models.length >= this.perPage && this.page * this.perPage < this.totalCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,4 +5,4 @@ export default class SecurityError implements Error {
|
|||||||
public constructor(message: string) {
|
public constructor(message: string) {
|
||||||
this.message = message;
|
this.message = message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {TooManyRequestsHttpError} from "./HttpError";
|
import {TooManyRequestsHttpError} from "./HttpError";
|
||||||
|
|
||||||
export default class Throttler {
|
export default class Throttler {
|
||||||
private static readonly throttles: { [throttleName: string]: Throttle } = {};
|
private static readonly throttles: Record<string, Throttle | undefined> = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Throttle function; will throw a TooManyRequestsHttpError when the threshold is reached.
|
* Throttle function; will throw a TooManyRequestsHttpError when the threshold is reached.
|
||||||
@ -16,13 +16,21 @@ export default class Throttler {
|
|||||||
* @param holdPeriod time in ms after each call before the threshold begins to decrease.
|
* @param holdPeriod time in ms after each call before the threshold begins to decrease.
|
||||||
* @param jailPeriod time in ms for which the throttle will throw when it is triggered.
|
* @param jailPeriod time in ms for which the throttle will throw when it is triggered.
|
||||||
*/
|
*/
|
||||||
public static throttle(action: string, max: number, resetPeriod: number, id: string, holdPeriod: number = 100, jailPeriod: number = 30 * 1000) {
|
public static throttle(
|
||||||
|
action: string,
|
||||||
|
max: number,
|
||||||
|
resetPeriod: number,
|
||||||
|
id: string,
|
||||||
|
holdPeriod: number = 100,
|
||||||
|
jailPeriod: number = 30 * 1000,
|
||||||
|
): void {
|
||||||
let throttle = this.throttles[action];
|
let throttle = this.throttles[action];
|
||||||
if (!throttle) throttle = this.throttles[action] = new Throttle(max, resetPeriod, holdPeriod, jailPeriod);
|
if (!throttle) throttle = this.throttles[action] = new Throttle(max, resetPeriod, holdPeriod, jailPeriod);
|
||||||
throttle.trigger(id);
|
throttle.trigger(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
|
// Disable constructor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,15 +39,13 @@ class Throttle {
|
|||||||
private readonly resetPeriod: number;
|
private readonly resetPeriod: number;
|
||||||
private readonly holdPeriod: number;
|
private readonly holdPeriod: number;
|
||||||
private readonly jailPeriod: number;
|
private readonly jailPeriod: number;
|
||||||
private readonly triggers: {
|
private readonly triggers: Record<string, {
|
||||||
[id: string]: {
|
count: number,
|
||||||
count: number,
|
lastTrigger?: number,
|
||||||
lastTrigger?: number,
|
jailed?: number;
|
||||||
jailed?: number;
|
} | undefined> = {};
|
||||||
}
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
constructor(max: number, resetPeriod: number, holdPeriod: number, jailPeriod: number) {
|
public constructor(max: number, resetPeriod: number, holdPeriod: number, jailPeriod: number) {
|
||||||
this.max = max;
|
this.max = max;
|
||||||
this.resetPeriod = resetPeriod;
|
this.resetPeriod = resetPeriod;
|
||||||
this.holdPeriod = holdPeriod;
|
this.holdPeriod = holdPeriod;
|
||||||
@ -51,12 +57,10 @@ class Throttle {
|
|||||||
let trigger = this.triggers[id];
|
let trigger = this.triggers[id];
|
||||||
if (!trigger) trigger = this.triggers[id] = {count: 0};
|
if (!trigger) trigger = this.triggers[id] = {count: 0};
|
||||||
|
|
||||||
let currentDate = new Date().getTime();
|
const currentDate = new Date().getTime();
|
||||||
|
|
||||||
if (trigger.jailed && currentDate - trigger.jailed < this.jailPeriod) {
|
if (trigger.jailed && currentDate - trigger.jailed < this.jailPeriod)
|
||||||
this.throw((trigger.jailed + this.jailPeriod) - currentDate);
|
return this.throw(trigger.jailed + this.jailPeriod - currentDate);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trigger.lastTrigger) {
|
if (trigger.lastTrigger) {
|
||||||
let timeDiff = currentDate - trigger.lastTrigger;
|
let timeDiff = currentDate - trigger.lastTrigger;
|
||||||
@ -71,12 +75,11 @@ class Throttle {
|
|||||||
|
|
||||||
if (trigger.count > this.max) {
|
if (trigger.count > this.max) {
|
||||||
trigger.jailed = currentDate;
|
trigger.jailed = currentDate;
|
||||||
this.throw((trigger.jailed + this.jailPeriod) - currentDate);
|
return this.throw(trigger.jailed + this.jailPeriod - currentDate);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected throw(unjailedIn: number) {
|
protected throw(unjailedIn: number) {
|
||||||
throw new TooManyRequestsHttpError(unjailedIn);
|
throw new TooManyRequestsHttpError(unjailedIn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
22
src/Utils.ts
22
src/Utils.ts
@ -18,7 +18,7 @@ export abstract class WrappingError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get name(): string {
|
public get name(): string {
|
||||||
return this.constructor.name;
|
return this.constructor.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -28,15 +28,15 @@ export function cryptoRandomDictionary(size: number, dictionary: string): string
|
|||||||
const output = new Array(size);
|
const output = new Array(size);
|
||||||
|
|
||||||
for (let i = 0; i < size; i++) {
|
for (let i = 0; i < size; i++) {
|
||||||
output[i] = dictionary[Math.floor((randomBytes[i] / 255) * dictionary.length)];
|
output[i] = dictionary[Math.floor(randomBytes[i] / 255 * dictionary.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
return output.join('');
|
return output.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Type<T> = { new(...args: any[]): T };
|
export type Type<T> = { new(...args: never[]): T };
|
||||||
|
|
||||||
export function bufferToUUID(buffer: Buffer): string {
|
export function bufferToUuid(buffer: Buffer): string {
|
||||||
const chars = buffer.toString('hex');
|
const chars = buffer.toString('hex');
|
||||||
let out = '';
|
let out = '';
|
||||||
let i = 0;
|
let i = 0;
|
||||||
@ -48,12 +48,12 @@ export function bufferToUUID(buffer: Buffer): string {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMethods<T>(obj: T): (string)[] {
|
export function getMethods<T extends { [p: string]: unknown }>(obj: T): string[] {
|
||||||
let properties = new Set()
|
const properties = new Set<string>();
|
||||||
let currentObj = obj
|
let currentObj: T | unknown = obj;
|
||||||
do {
|
do {
|
||||||
Object.getOwnPropertyNames(currentObj).map(item => properties.add(item))
|
Object.getOwnPropertyNames(currentObj).map(item => properties.add(item));
|
||||||
} while ((currentObj = Object.getPrototypeOf(currentObj)))
|
currentObj = Object.getPrototypeOf(currentObj);
|
||||||
// @ts-ignore
|
} while (currentObj);
|
||||||
return [...properties.keys()].filter(item => typeof obj[item] === 'function')
|
return [...properties.keys()].filter(item => typeof obj[item] === 'function');
|
||||||
}
|
}
|
||||||
|
@ -15,5 +15,9 @@ export default abstract class WebSocketListener<T extends Application> {
|
|||||||
|
|
||||||
public abstract path(): string;
|
public abstract path(): string;
|
||||||
|
|
||||||
public abstract async handle(socket: WebSocket, request: IncomingMessage, session: Express.Session | null): Promise<void>;
|
public abstract async handle(
|
||||||
}
|
socket: WebSocket,
|
||||||
|
request: IncomingMessage,
|
||||||
|
session: Express.Session | null,
|
||||||
|
): Promise<void>;
|
||||||
|
}
|
||||||
|
@ -31,7 +31,7 @@ export class AuthMiddleware extends Middleware {
|
|||||||
protected async handle(req: Request, res: Response, next: NextFunction): Promise<void> {
|
protected async handle(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||||
this.authGuard = this.app.as(AuthComponent).getAuthGuard();
|
this.authGuard = this.app.as(AuthComponent).getAuthGuard();
|
||||||
|
|
||||||
const proof = await this.authGuard.isAuthenticated(req.session!);
|
const proof = await this.authGuard.isAuthenticated(req.getSession());
|
||||||
if (proof) {
|
if (proof) {
|
||||||
this.user = await proof.getResource();
|
this.user = await proof.getResource();
|
||||||
res.locals.user = this.user;
|
res.locals.user = this.user;
|
||||||
@ -76,7 +76,7 @@ export class RequireAuthMiddleware extends Middleware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Via session
|
// Via session
|
||||||
if (!await authGuard.isAuthenticated(req.session!)) {
|
if (!await authGuard.isAuthenticated(req.getSession())) {
|
||||||
req.flash('error', `You must be logged in to access ${req.url}.`);
|
req.flash('error', `You must be logged in to access ${req.url}.`);
|
||||||
res.redirect(Controller.route('auth', undefined, {
|
res.redirect(Controller.route('auth', undefined, {
|
||||||
redirect_uri: req.url,
|
redirect_uri: req.url,
|
||||||
@ -90,7 +90,7 @@ export class RequireAuthMiddleware extends Middleware {
|
|||||||
|
|
||||||
export class RequireGuestMiddleware extends Middleware {
|
export class RequireGuestMiddleware extends Middleware {
|
||||||
protected async handle(req: Request, res: Response, next: NextFunction): Promise<void> {
|
protected async handle(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||||
if (await req.as(AuthMiddleware).getAuthGuard().isAuthenticated(req.session!)) {
|
if (await req.as(AuthMiddleware).getAuthGuard().isAuthenticated(req.getSession())) {
|
||||||
res.redirectBack();
|
res.redirectBack();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -7,14 +7,14 @@ export default abstract class AuthController extends Controller {
|
|||||||
return '/auth';
|
return '/auth';
|
||||||
}
|
}
|
||||||
|
|
||||||
public routes() {
|
public routes(): void {
|
||||||
this.get('/', this.getAuth, 'auth', RequireGuestMiddleware);
|
this.get('/', this.getAuth, 'auth', RequireGuestMiddleware);
|
||||||
this.post('/', this.postAuth, 'auth', RequireGuestMiddleware);
|
this.post('/', this.postAuth, 'auth', RequireGuestMiddleware);
|
||||||
this.get('/check', this.getCheckAuth, 'check_auth');
|
this.get('/check', this.getCheckAuth, 'check_auth');
|
||||||
this.post('/logout', this.postLogout, 'logout', RequireAuthMiddleware);
|
this.post('/logout', this.postLogout, 'logout', RequireAuthMiddleware);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getAuth(req: Request, res: Response, next: NextFunction): Promise<void> {
|
protected async getAuth(req: Request, res: Response, _next: NextFunction): Promise<void> {
|
||||||
const registerEmail = req.flash('register_confirm_email');
|
const registerEmail = req.flash('register_confirm_email');
|
||||||
res.render('auth/auth', {
|
res.render('auth/auth', {
|
||||||
register_confirm_email: registerEmail.length > 0 ? registerEmail[0] : null,
|
register_confirm_email: registerEmail.length > 0 ? registerEmail[0] : null,
|
||||||
@ -25,11 +25,11 @@ export default abstract class AuthController extends Controller {
|
|||||||
|
|
||||||
protected abstract async getCheckAuth(req: Request, res: Response, next: NextFunction): Promise<void>;
|
protected abstract async getCheckAuth(req: Request, res: Response, next: NextFunction): Promise<void>;
|
||||||
|
|
||||||
protected async postLogout(req: Request, res: Response, next: NextFunction): Promise<void> {
|
protected async postLogout(req: Request, res: Response, _next: NextFunction): Promise<void> {
|
||||||
const proof = await req.as(AuthMiddleware).getAuthGuard().getProof(req);
|
const proof = await req.as(AuthMiddleware).getAuthGuard().getProof(req);
|
||||||
await proof?.revoke();
|
await proof?.revoke();
|
||||||
req.flash('success', 'Successfully logged out.');
|
req.flash('success', 'Successfully logged out.');
|
||||||
res.redirect(req.query.redirect_uri?.toString() || '/');
|
res.redirect(req.query.redirect_uri?.toString() || '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import config from "config";
|
|||||||
export default abstract class AuthGuard<P extends AuthProof<User>> {
|
export default abstract class AuthGuard<P extends AuthProof<User>> {
|
||||||
protected abstract async getProofForSession(session: Express.Session): Promise<P | null>;
|
protected abstract async getProofForSession(session: Express.Session): Promise<P | null>;
|
||||||
|
|
||||||
protected async getProofForRequest(req: Request): Promise<P | null> {
|
protected async getProofForRequest(_req: Request): Promise<P | null> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ export default abstract class AuthGuard<P extends AuthProof<User>> {
|
|||||||
session: Express.Session,
|
session: Express.Session,
|
||||||
proof: P,
|
proof: P,
|
||||||
onLogin?: (user: User) => Promise<void>,
|
onLogin?: (user: User) => Promise<void>,
|
||||||
onRegister?: (connection: Connection, user: User) => Promise<RegisterCallback[]>
|
onRegister?: (connection: Connection, user: User) => Promise<RegisterCallback[]>,
|
||||||
): Promise<User> {
|
): Promise<User> {
|
||||||
if (!await proof.isValid()) throw new InvalidAuthProofError();
|
if (!await proof.isValid()) throw new InvalidAuthProofError();
|
||||||
if (!await proof.isAuthorized()) throw new UnauthorizedAuthProofError();
|
if (!await proof.isAuthorized()) throw new UnauthorizedAuthProofError();
|
||||||
@ -63,27 +63,24 @@ export default abstract class AuthGuard<P extends AuthProof<User>> {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
const callbacks: RegisterCallback[] = [];
|
const callbacks: RegisterCallback[] = [];
|
||||||
|
|
||||||
await MysqlConnectionManager.wrapTransaction(async connection => {
|
user = await MysqlConnectionManager.wrapTransaction(async connection => {
|
||||||
user = User.create({});
|
const user = User.create({});
|
||||||
await user.save(connection, c => callbacks.push(c));
|
await user.save(connection, c => callbacks.push(c));
|
||||||
if (onRegister) {
|
if (onRegister) {
|
||||||
(await onRegister(connection, user)).forEach(c => callbacks.push(c));
|
(await onRegister(connection, user)).forEach(c => callbacks.push(c));
|
||||||
}
|
}
|
||||||
|
return user;
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const callback of callbacks) {
|
for (const callback of callbacks) {
|
||||||
await callback();
|
await callback();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user) {
|
if (!user.isApproved()) {
|
||||||
if (!user!.isApproved()) {
|
await new Mail(PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE, {
|
||||||
await new Mail(PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE, {
|
username: (await user.mainEmail.get())?.getOrFail('email'),
|
||||||
username: user!.name,
|
link: config.get<string>('base_url') + Controller.route('accounts-approval'),
|
||||||
link: config.get<string>('base_url') + Controller.route('accounts-approval'),
|
}).send(config.get<string>('app.contact_email'));
|
||||||
}).send(config.get<string>('app.contact_email'));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error('Unable to register user.');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,28 +99,25 @@ export default abstract class AuthGuard<P extends AuthProof<User>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class AuthError extends Error {
|
export class AuthError extends Error {
|
||||||
constructor(message: string) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AuthProofError extends AuthError {
|
export class AuthProofError extends AuthError {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InvalidAuthProofError extends AuthProofError {
|
export class InvalidAuthProofError extends AuthProofError {
|
||||||
constructor() {
|
public constructor() {
|
||||||
super('Invalid auth proof.');
|
super('Invalid auth proof.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UnauthorizedAuthProofError extends AuthProofError {
|
export class UnauthorizedAuthProofError extends AuthProofError {
|
||||||
constructor() {
|
public constructor() {
|
||||||
super('Unauthorized auth proof.');
|
super('Unauthorized auth proof.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PendingApprovalAuthError extends AuthError {
|
export class PendingApprovalAuthError extends AuthError {
|
||||||
constructor() {
|
public constructor() {
|
||||||
super(`User is not approved.`);
|
super(`User is not approved.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
* This class is most commonly used for authentication. It can be more generically used to represent a verification
|
* This class is most commonly used for authentication. It can be more generically used to represent a verification
|
||||||
* state of whether a given resource is owned by a session.
|
* state of whether a given resource is owned by a session.
|
||||||
*
|
*
|
||||||
* Any auth system should consider this auth proof valid if and only if both {@code isValid()} and {@code isAuthorized()}
|
* Any auth system should consider this auth proof valid if and only if both {@code isValid()} and
|
||||||
* both return {@code true}.
|
* {@code isAuthorized()} both return {@code true}.
|
||||||
*
|
*
|
||||||
* @type <R> The resource type this AuthProof authorizes.
|
* @type <R> The resource type this AuthProof authorizes.
|
||||||
*/
|
*/
|
||||||
@ -34,7 +34,8 @@ export default interface AuthProof<R> {
|
|||||||
* - {@code isAuthorized} returns {@code false}
|
* - {@code isAuthorized} returns {@code false}
|
||||||
* - There is no way to re-authorize this proof (i.e. {@code isAuthorized} can never return {@code true} again)
|
* - There is no way to re-authorize this proof (i.e. {@code isAuthorized} can never return {@code true} again)
|
||||||
*
|
*
|
||||||
* Additionally, this method should delete any stored data that could lead to restoration of this AuthProof instance.
|
* Additionally, this method should delete any stored data that could lead to restoration of this AuthProof
|
||||||
|
* instance.
|
||||||
*/
|
*/
|
||||||
revoke(): Promise<void>;
|
revoke(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,12 @@ import Controller from "../Controller";
|
|||||||
import Mail from "../Mail";
|
import Mail from "../Mail";
|
||||||
|
|
||||||
export default class MailController extends Controller {
|
export default class MailController extends Controller {
|
||||||
routes(): void {
|
public routes(): void {
|
||||||
this.get("/mail/:template", this.getMail, 'mail');
|
this.get("/mail/:template", this.getMail, 'mail');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getMail(request: Request, response: Response) {
|
protected async getMail(request: Request, response: Response): Promise<void> {
|
||||||
const template = request.params['template'];
|
const template = request.params['template'];
|
||||||
response.send(Mail.parse(`mails/${template}.mjml.njk`, request.query, false));
|
response.send(Mail.parse(`mails/${template}.mjml.njk`, request.query, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,38 +15,40 @@ import User from "../models/User";
|
|||||||
|
|
||||||
export default abstract class MagicLinkAuthController extends AuthController {
|
export default abstract class MagicLinkAuthController extends AuthController {
|
||||||
public static async checkAndAuth(req: Request, res: Response, magicLink: MagicLink): Promise<User | null> {
|
public static async checkAndAuth(req: Request, res: Response, magicLink: MagicLink): Promise<User | null> {
|
||||||
if (magicLink.getSessionID() !== req.sessionID!) throw new BadOwnerMagicLink();
|
const session = req.getSession();
|
||||||
|
if (magicLink.getOrFail('session_id') !== session.id) throw new BadOwnerMagicLink();
|
||||||
if (!await magicLink.isAuthorized()) throw new UnauthorizedMagicLink();
|
if (!await magicLink.isAuthorized()) throw new UnauthorizedMagicLink();
|
||||||
if (!await magicLink.isValid()) throw new InvalidMagicLink();
|
if (!await magicLink.isValid()) throw new InvalidMagicLink();
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
try {
|
try {
|
||||||
return await req.as(AuthMiddleware).getAuthGuard().authenticateOrRegister(req.session!, magicLink, undefined, async (connection, user) => {
|
return await req.as(AuthMiddleware).getAuthGuard().authenticateOrRegister(
|
||||||
const callbacks: RegisterCallback[] = [];
|
session, magicLink, undefined, async (connection, user) => {
|
||||||
|
const callbacks: RegisterCallback[] = [];
|
||||||
|
|
||||||
const userEmail = UserEmail.create({
|
const userEmail = UserEmail.create({
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
email: magicLink.getEmail(),
|
email: magicLink.getOrFail('email'),
|
||||||
|
});
|
||||||
|
await userEmail.save(connection, c => callbacks.push(c));
|
||||||
|
user.main_email_id = userEmail.id;
|
||||||
|
await user.save(connection, c => callbacks.push(c));
|
||||||
|
|
||||||
|
return callbacks;
|
||||||
});
|
});
|
||||||
await userEmail.save(connection, c => callbacks.push(c));
|
|
||||||
user.main_email_id = userEmail.id;
|
|
||||||
await user.save(connection, c => callbacks.push(c));
|
|
||||||
|
|
||||||
return callbacks;
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof PendingApprovalAuthError) {
|
if (e instanceof PendingApprovalAuthError) {
|
||||||
res.format({
|
res.format({
|
||||||
json: () => {
|
json: () => {
|
||||||
res.json({
|
res.json({
|
||||||
'status': 'warning',
|
'status': 'warning',
|
||||||
'message': `Your account is pending review. You'll receive an email once you're approved.`
|
'message': `Your account is pending review. You'll receive an email once you're approved.`,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
html: () => {
|
html: () => {
|
||||||
req.flash('warning', `Your account is pending review. You'll receive an email once you're approved.`);
|
req.flash('warning', `Your account is pending review. You'll receive an email once you're approved.`);
|
||||||
res.redirect('/');
|
res.redirect('/');
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
@ -65,7 +67,8 @@ export default abstract class MagicLinkAuthController extends AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async getAuth(req: Request, res: Response, next: NextFunction): Promise<void> {
|
protected async getAuth(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||||
const link = await MagicLink.bySessionID(req.sessionID!, [this.loginMagicLinkActionType, this.registerMagicLinkActionType]);
|
const link = await MagicLink.bySessionId(req.getSession().id,
|
||||||
|
[this.loginMagicLinkActionType, this.registerMagicLinkActionType]);
|
||||||
if (link && await link.isValid()) {
|
if (link && await link.isValid()) {
|
||||||
res.redirect(Controller.route('magic_link_lobby', undefined, {
|
res.redirect(Controller.route('magic_link_lobby', undefined, {
|
||||||
redirect_uri: req.query.redirect_uri?.toString() || undefined,
|
redirect_uri: req.query.redirect_uri?.toString() || undefined,
|
||||||
@ -76,7 +79,7 @@ export default abstract class MagicLinkAuthController extends AuthController {
|
|||||||
await super.getAuth(req, res, next);
|
await super.getAuth(req, res, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async postAuth(req: Request, res: Response, next: NextFunction): Promise<void> {
|
protected async postAuth(req: Request, res: Response, _next: NextFunction): Promise<void> {
|
||||||
const email = req.body.email;
|
const email = req.body.email;
|
||||||
if (!email) throw new BadRequestError('Email not specified.', 'Please try again.', req.originalUrl);
|
if (!email) throw new BadRequestError('Email not specified.', 'Please try again.', req.originalUrl);
|
||||||
|
|
||||||
@ -96,7 +99,7 @@ export default abstract class MagicLinkAuthController extends AuthController {
|
|||||||
// Register (email link)
|
// Register (email link)
|
||||||
const geo = geoip.lookup(req.ip);
|
const geo = geoip.lookup(req.ip);
|
||||||
await MagicLinkController.sendMagicLink(
|
await MagicLinkController.sendMagicLink(
|
||||||
req.sessionID!,
|
req.getSession().id,
|
||||||
isRegistration ? this.registerMagicLinkActionType : this.loginMagicLinkActionType,
|
isRegistration ? this.registerMagicLinkActionType : this.loginMagicLinkActionType,
|
||||||
Controller.route('auth', undefined, {
|
Controller.route('auth', undefined, {
|
||||||
redirect_uri: req.query.redirect_uri?.toString() || undefined,
|
redirect_uri: req.query.redirect_uri?.toString() || undefined,
|
||||||
@ -107,7 +110,7 @@ export default abstract class MagicLinkAuthController extends AuthController {
|
|||||||
type: isRegistration ? 'register' : 'login',
|
type: isRegistration ? 'register' : 'login',
|
||||||
ip: req.ip,
|
ip: req.ip,
|
||||||
geo: geo ? `${geo.city}, ${geo.country}` : 'Unknown location',
|
geo: geo ? `${geo.city}, ${geo.country}` : 'Unknown location',
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
res.redirect(Controller.route('magic_link_lobby', undefined, {
|
res.redirect(Controller.route('magic_link_lobby', undefined, {
|
||||||
@ -125,13 +128,18 @@ export default abstract class MagicLinkAuthController extends AuthController {
|
|||||||
/**
|
/**
|
||||||
* Check whether a magic link is authorized, and authenticate if yes
|
* Check whether a magic link is authorized, and authenticate if yes
|
||||||
*/
|
*/
|
||||||
protected async getCheckAuth(req: Request, res: Response, next: NextFunction): Promise<void> {
|
protected async getCheckAuth(req: Request, res: Response, _next: NextFunction): Promise<void> {
|
||||||
const magicLink = await MagicLink.bySessionID(req.sessionID!, [this.loginMagicLinkActionType, this.registerMagicLinkActionType]);
|
const magicLink = await MagicLink.bySessionId(req.getSession().id,
|
||||||
|
[this.loginMagicLinkActionType, this.registerMagicLinkActionType]);
|
||||||
|
|
||||||
if (!magicLink) {
|
if (!magicLink) {
|
||||||
res.format({
|
res.format({
|
||||||
json: () => {
|
json: () => {
|
||||||
throw new BadRequestError('No magic link were found linked with that session.', 'Please retry once you have requested a magic link.', req.originalUrl);
|
throw new BadRequestError(
|
||||||
|
'No magic link were found linked with that session.',
|
||||||
|
'Please retry once you have requested a magic link.',
|
||||||
|
req.originalUrl,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
default: () => {
|
default: () => {
|
||||||
req.flash('warning', 'No magic link found. Please try again.');
|
req.flash('warning', 'No magic link found. Please try again.');
|
||||||
@ -160,19 +168,19 @@ export default abstract class MagicLinkAuthController extends AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class BadOwnerMagicLink extends AuthError {
|
export class BadOwnerMagicLink extends AuthError {
|
||||||
constructor() {
|
public constructor() {
|
||||||
super(`This magic link doesn't belong to this session.`);
|
super(`This magic link doesn't belong to this session.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UnauthorizedMagicLink extends AuthError {
|
export class UnauthorizedMagicLink extends AuthError {
|
||||||
constructor() {
|
public constructor() {
|
||||||
super(`This magic link is unauthorized.`);
|
super(`This magic link is unauthorized.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InvalidMagicLink extends AuthError {
|
export class InvalidMagicLink extends AuthError {
|
||||||
constructor() {
|
public constructor() {
|
||||||
super(`This magic link is invalid.`);
|
super(`This magic link is invalid.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,15 +6,24 @@ import Throttler from "../../Throttler";
|
|||||||
import Mail, {MailTemplate} from "../../Mail";
|
import Mail, {MailTemplate} from "../../Mail";
|
||||||
import MagicLink from "../models/MagicLink";
|
import MagicLink from "../models/MagicLink";
|
||||||
import config from "config";
|
import config from "config";
|
||||||
|
import Application from "../../Application";
|
||||||
|
import {ParsedUrlQueryInput} from "querystring";
|
||||||
|
|
||||||
export default abstract class MagicLinkController extends Controller {
|
export default abstract class MagicLinkController<A extends Application> extends Controller {
|
||||||
public static async sendMagicLink(sessionID: string, actionType: string, original_url: string, email: string, mailTemplate: MailTemplate, data: object): Promise<void> {
|
public static async sendMagicLink(
|
||||||
Throttler.throttle('magic_link', 2, MagicLink.validityPeriod(), sessionID, 0, 0);
|
sessionId: string,
|
||||||
|
actionType: string,
|
||||||
|
original_url: string,
|
||||||
|
email: string,
|
||||||
|
mailTemplate: MailTemplate,
|
||||||
|
data: ParsedUrlQueryInput,
|
||||||
|
): Promise<void> {
|
||||||
|
Throttler.throttle('magic_link', 2, MagicLink.validityPeriod(), sessionId, 0, 0);
|
||||||
Throttler.throttle('magic_link', 1, MagicLink.validityPeriod(), email, 0, 0);
|
Throttler.throttle('magic_link', 1, MagicLink.validityPeriod(), email, 0, 0);
|
||||||
|
|
||||||
const link = await MagicLink.bySessionID(sessionID, actionType) ||
|
const link = await MagicLink.bySessionId(sessionId, actionType) ||
|
||||||
MagicLink.create({
|
MagicLink.create({
|
||||||
session_id: sessionID,
|
session_id: sessionId,
|
||||||
action_type: actionType,
|
action_type: actionType,
|
||||||
original_url: original_url,
|
original_url: original_url,
|
||||||
});
|
});
|
||||||
@ -34,7 +43,7 @@ export default abstract class MagicLinkController extends Controller {
|
|||||||
|
|
||||||
protected readonly magicLinkWebsocketPath: string;
|
protected readonly magicLinkWebsocketPath: string;
|
||||||
|
|
||||||
protected constructor(magicLinkWebsocketListener: MagicLinkWebSocketListener) {
|
protected constructor(magicLinkWebsocketListener: MagicLinkWebSocketListener<A>) {
|
||||||
super();
|
super();
|
||||||
this.magicLinkWebsocketPath = magicLinkWebsocketListener.path();
|
this.magicLinkWebsocketPath = magicLinkWebsocketListener.path();
|
||||||
}
|
}
|
||||||
@ -49,14 +58,14 @@ export default abstract class MagicLinkController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async getLobby(req: Request, res: Response): Promise<void> {
|
protected async getLobby(req: Request, res: Response): Promise<void> {
|
||||||
const link = await MagicLink.bySessionID(req.sessionID!);
|
const link = await MagicLink.bySessionId(req.getSession().id);
|
||||||
if (!link) {
|
if (!link) {
|
||||||
throw new NotFoundHttpError('magic link', req.url);
|
throw new NotFoundHttpError('magic link', req.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await link.isValid()) {
|
if (!await link.isValid()) {
|
||||||
req.flash('error', 'This magic link has expired. Please try again.');
|
req.flash('error', 'This magic link has expired. Please try again.');
|
||||||
res.redirect(link.getOriginalURL());
|
res.redirect(link.getOrFail('original_url'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,8 +75,8 @@ export default abstract class MagicLinkController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.render('magic_link_lobby', {
|
res.render('magic_link_lobby', {
|
||||||
email: link.getEmail(),
|
email: link.getOrFail('email'),
|
||||||
type: link.getActionType(),
|
type: link.getOrFail('action_type'),
|
||||||
validUntil: link.getExpirationDate().getTime(),
|
validUntil: link.getExpirationDate().getTime(),
|
||||||
websocketUrl: config.get<string>('public_websocket_url') + this.magicLinkWebsocketPath,
|
websocketUrl: config.get<string>('public_websocket_url') + this.magicLinkWebsocketPath,
|
||||||
});
|
});
|
||||||
@ -76,7 +85,8 @@ export default abstract class MagicLinkController extends Controller {
|
|||||||
protected async getMagicLink(req: Request, res: Response): Promise<void> {
|
protected async getMagicLink(req: Request, res: Response): Promise<void> {
|
||||||
const id = parseInt(<string>req.query.id);
|
const id = parseInt(<string>req.query.id);
|
||||||
const token = <string>req.query.token;
|
const token = <string>req.query.token;
|
||||||
if (!id || !token) throw new BadRequestError('Need parameters id, token.', 'Please try again.', req.originalUrl);
|
if (!id || !token)
|
||||||
|
throw new BadRequestError('Need parameters id, token.', 'Please try again.', req.originalUrl);
|
||||||
|
|
||||||
let success = true;
|
let success = true;
|
||||||
let err;
|
let err;
|
||||||
@ -91,7 +101,8 @@ export default abstract class MagicLinkController extends Controller {
|
|||||||
// Validation success, authenticate the user
|
// Validation success, authenticate the user
|
||||||
magicLink.authorize();
|
magicLink.authorize();
|
||||||
await magicLink.save();
|
await magicLink.save();
|
||||||
MagicLinkWebSocketListener.refreshMagicLink(magicLink.getSessionID());
|
this.getApp().as<MagicLinkWebSocketListener<A>>(MagicLinkWebSocketListener)
|
||||||
|
.refreshMagicLink(magicLink.getOrFail('session_id'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,4 +114,4 @@ export default abstract class MagicLinkController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected abstract async performAction(magicLink: MagicLink, req: Request, res: Response): Promise<void>;
|
protected abstract async performAction(magicLink: MagicLink, req: Request, res: Response): Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -2,18 +2,19 @@ import WebSocket from "ws";
|
|||||||
import {IncomingMessage} from "http";
|
import {IncomingMessage} from "http";
|
||||||
import WebSocketListener from "../../WebSocketListener";
|
import WebSocketListener from "../../WebSocketListener";
|
||||||
import MagicLink from "../models/MagicLink";
|
import MagicLink from "../models/MagicLink";
|
||||||
|
import Application from "../../Application";
|
||||||
|
|
||||||
export default class MagicLinkWebSocketListener extends WebSocketListener<any> {
|
export default class MagicLinkWebSocketListener<A extends Application> extends WebSocketListener<A> {
|
||||||
private static readonly connections: { [p: string]: (() => void)[] } = {};
|
private readonly connections: { [p: string]: (() => void)[] | undefined } = {};
|
||||||
|
|
||||||
public static refreshMagicLink(sessionID: string) {
|
public refreshMagicLink(sessionId: string): void {
|
||||||
const fs = MagicLinkWebSocketListener.connections[sessionID];
|
const fs = this.connections[sessionId];
|
||||||
if (fs) {
|
if (fs) {
|
||||||
fs.forEach(f => f());
|
fs.forEach(f => f());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handle(socket: WebSocket, request: IncomingMessage, session: Express.Session | null): Promise<void> {
|
public async handle(socket: WebSocket, request: IncomingMessage, session: Express.Session | null): Promise<void> {
|
||||||
// Drop if requested without session
|
// Drop if requested without session
|
||||||
if (!session) {
|
if (!session) {
|
||||||
socket.close(1002, 'Session is required for this request.');
|
socket.close(1002, 'Session is required for this request.');
|
||||||
@ -26,7 +27,7 @@ export default class MagicLinkWebSocketListener extends WebSocketListener<any> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Get magic link
|
// Get magic link
|
||||||
const magicLink = await MagicLink.bySessionID(session.id);
|
const magicLink = await MagicLink.bySessionId(session.id);
|
||||||
|
|
||||||
// Refresh if immediately applicable
|
// Refresh if immediately applicable
|
||||||
if (!magicLink || !await magicLink.isValid() || await magicLink.isAuthorized()) {
|
if (!magicLink || !await magicLink.isValid() || await magicLink.isAuthorized()) {
|
||||||
@ -47,16 +48,19 @@ export default class MagicLinkWebSocketListener extends WebSocketListener<any> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
socket.on('close', () => {
|
socket.on('close', () => {
|
||||||
MagicLinkWebSocketListener.connections[session.id] = MagicLinkWebSocketListener.connections[session.id].filter(f => f !== f);
|
const connections = this.connections[session.id];
|
||||||
if (MagicLinkWebSocketListener.connections[session.id].length === 0) delete MagicLinkWebSocketListener.connections[session.id];
|
if (connections) {
|
||||||
|
this.connections[session.id] = connections.filter(f => f !== f);
|
||||||
|
if (connections.length === 0) delete this.connections[session.id];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!MagicLinkWebSocketListener.connections[session.id]) MagicLinkWebSocketListener.connections[session.id] = [];
|
let connections = this.connections[session.id];
|
||||||
|
if (!connections) connections = this.connections[session.id] = [];
|
||||||
MagicLinkWebSocketListener.connections[session.id].push(f);
|
connections.push(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
path(): string {
|
public path(): string {
|
||||||
return '/magic-link';
|
return '/magic-link';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,4 +16,4 @@ export default class AddApprovedFieldToUsersTable extends Migration {
|
|||||||
public registerModels(): void {
|
public registerModels(): void {
|
||||||
ModelFactory.get(User).addComponent(UserApprovedComponent);
|
ModelFactory.get(User).addComponent(UserApprovedComponent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,4 +27,4 @@ export default class CreateMagicLinksTable extends Migration {
|
|||||||
ModelFactory.register(MagicLink);
|
ModelFactory.register(MagicLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,4 @@ export default class DropNameFromUsers extends Migration {
|
|||||||
await this.query('ALTER TABLE users ADD COLUMN name VARCHAR(64)', connection);
|
await this.query('ALTER TABLE users ADD COLUMN name VARCHAR(64)', connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
public registerModels(): void {
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -17,13 +17,9 @@ export default class FixUserMainEmailRelation extends Migration {
|
|||||||
await this.query(`ALTER TABLE user_emails
|
await this.query(`ALTER TABLE user_emails
|
||||||
ADD COLUMN main BOOLEAN DEFAULT false`, connection);
|
ADD COLUMN main BOOLEAN DEFAULT false`, connection);
|
||||||
await this.query(`UPDATE user_emails ue LEFT JOIN users u ON ue.id = u.main_email_id
|
await this.query(`UPDATE user_emails ue LEFT JOIN users u ON ue.id = u.main_email_id
|
||||||
SET ue.main = true`, connection)
|
SET ue.main = true`, connection);
|
||||||
await this.query(`ALTER TABLE users
|
await this.query(`ALTER TABLE users
|
||||||
DROP FOREIGN KEY main_user_email_fk,
|
DROP FOREIGN KEY main_user_email_fk,
|
||||||
DROP COLUMN main_email_id`, connection);
|
DROP COLUMN main_email_id`, connection);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
public registerModels(): void {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -9,8 +9,8 @@ import UserEmail from "./UserEmail";
|
|||||||
import {EMAIL_REGEX} from "../../db/Validator";
|
import {EMAIL_REGEX} from "../../db/Validator";
|
||||||
|
|
||||||
export default class MagicLink extends Model implements AuthProof<User> {
|
export default class MagicLink extends Model implements AuthProof<User> {
|
||||||
public static async bySessionID(sessionID: string, actionType?: string | string[]): Promise<MagicLink | null> {
|
public static async bySessionId(sessionId: string, actionType?: string | string[]): Promise<MagicLink | null> {
|
||||||
let query = this.select().where('session_id', sessionID);
|
let query = this.select().where('session_id', sessionId);
|
||||||
if (actionType !== undefined) {
|
if (actionType !== undefined) {
|
||||||
if (typeof actionType === 'string') {
|
if (typeof actionType === 'string') {
|
||||||
query = query.where('action_type', actionType);
|
query = query.where('action_type', actionType);
|
||||||
@ -26,10 +26,10 @@ export default class MagicLink extends Model implements AuthProof<User> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public readonly id?: number = undefined;
|
public readonly id?: number = undefined;
|
||||||
private session_id?: string = undefined;
|
public readonly session_id?: string = undefined;
|
||||||
private email?: string = undefined;
|
private email?: string = undefined;
|
||||||
private token?: string = undefined;
|
private token?: string = undefined;
|
||||||
private action_type?: string = undefined;
|
public readonly action_type?: string = undefined;
|
||||||
private original_url?: string = undefined;
|
private original_url?: string = undefined;
|
||||||
private generated_at?: Date = undefined;
|
private generated_at?: Date = undefined;
|
||||||
private authorized: boolean = false;
|
private authorized: boolean = false;
|
||||||
@ -46,9 +46,9 @@ export default class MagicLink extends Model implements AuthProof<User> {
|
|||||||
public async getResource(): Promise<User | null> {
|
public async getResource(): Promise<User | null> {
|
||||||
const email = await UserEmail.select()
|
const email = await UserEmail.select()
|
||||||
.with('user')
|
.with('user')
|
||||||
.where('email', await this.getEmail())
|
.where('email', await this.getOrFail('email'))
|
||||||
.first();
|
.first();
|
||||||
return email ? email.user.get() : null;
|
return email ? await email.user.get() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async revoke(): Promise<void> {
|
public async revoke(): Promise<void> {
|
||||||
@ -61,10 +61,10 @@ export default class MagicLink extends Model implements AuthProof<User> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async isAuthorized(): Promise<boolean> {
|
public async isAuthorized(): Promise<boolean> {
|
||||||
return this.authorized!;
|
return this.authorized;
|
||||||
}
|
}
|
||||||
|
|
||||||
public authorize() {
|
public authorize(): void {
|
||||||
this.authorized = true;
|
this.authorized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,28 +85,21 @@ export default class MagicLink extends Model implements AuthProof<User> {
|
|||||||
* @returns {@code null} if the token is valid, an error {@code string} otherwise.
|
* @returns {@code null} if the token is valid, an error {@code string} otherwise.
|
||||||
*/
|
*/
|
||||||
public async verifyToken(tokenGuess: string): Promise<string | null> {
|
public async verifyToken(tokenGuess: string): Promise<string | null> {
|
||||||
if (this.token === undefined || this.generated_at === undefined) return 'This token was not generated.'; // There is no token
|
// There is no token
|
||||||
if (new Date().getTime() - this.generated_at.getTime() > MagicLink.validityPeriod()) return 'This token has expired.'; // Token has expired
|
if (this.token === undefined || this.generated_at === undefined)
|
||||||
if (!await argon2.verify(this.token, tokenGuess)) return 'This token is invalid.';
|
return 'This token was not generated.';
|
||||||
|
|
||||||
|
// Token has expired
|
||||||
|
if (new Date().getTime() - this.generated_at.getTime() > MagicLink.validityPeriod())
|
||||||
|
return 'This token has expired.';
|
||||||
|
|
||||||
|
// Token is invalid
|
||||||
|
if (!await argon2.verify(this.token, tokenGuess))
|
||||||
|
return 'This token is invalid.';
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSessionID(): string {
|
|
||||||
return this.session_id!;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getEmail(): string {
|
|
||||||
return this.email!;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getActionType(): string {
|
|
||||||
return this.action_type!;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getOriginalURL(): string {
|
|
||||||
return this.original_url!;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getExpirationDate(): Date {
|
public getExpirationDate(): Date {
|
||||||
if (!this.generated_at) return new Date();
|
if (!this.generated_at) return new Date();
|
||||||
|
@ -8,7 +8,8 @@ import UserApprovedComponent from "./UserApprovedComponent";
|
|||||||
|
|
||||||
export default class User extends Model {
|
export default class User extends Model {
|
||||||
public static isApprovalMode(): boolean {
|
public static isApprovalMode(): boolean {
|
||||||
return config.get<boolean>('approval_mode') && MysqlConnectionManager.hasMigration(AddApprovedFieldToUsersTable);
|
return config.get<boolean>('approval_mode') &&
|
||||||
|
MysqlConnectionManager.hasMigration(AddApprovedFieldToUsersTable);
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly id?: number = undefined;
|
public readonly id?: number = undefined;
|
||||||
@ -19,7 +20,7 @@ export default class User extends Model {
|
|||||||
|
|
||||||
public readonly emails = new ManyModelRelation(this, UserEmail, {
|
public readonly emails = new ManyModelRelation(this, UserEmail, {
|
||||||
localKey: 'id',
|
localKey: 'id',
|
||||||
foreignKey: 'user_id'
|
foreignKey: 'user_id',
|
||||||
});
|
});
|
||||||
|
|
||||||
public readonly mainEmail = this.emails.cloneReduceToOne().constraint(q => q.where('id', this.main_email_id));
|
public readonly mainEmail = this.emails.cloneReduceToOne().constraint(q => q.where('id', this.main_email_id));
|
||||||
|
@ -3,7 +3,4 @@ import User from "./User";
|
|||||||
|
|
||||||
export default class UserApprovedComponent extends ModelComponent<User> {
|
export default class UserApprovedComponent extends ModelComponent<User> {
|
||||||
public approved: boolean = false;
|
public approved: boolean = false;
|
||||||
|
}
|
||||||
protected init(): void {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -11,7 +11,7 @@ export default class UserEmail extends Model {
|
|||||||
|
|
||||||
public readonly user = new OneModelRelation(this, User, {
|
public readonly user = new OneModelRelation(this, User, {
|
||||||
localKey: 'user_id',
|
localKey: 'user_id',
|
||||||
foreignKey: 'id'
|
foreignKey: 'id',
|
||||||
});
|
});
|
||||||
|
|
||||||
protected init(): void {
|
protected init(): void {
|
||||||
|
@ -6,6 +6,11 @@ import {ForbiddenHttpError} from "../HttpError";
|
|||||||
import Logger from "../Logger";
|
import Logger from "../Logger";
|
||||||
|
|
||||||
export default class AutoUpdateComponent extends ApplicationComponent {
|
export default class AutoUpdateComponent extends ApplicationComponent {
|
||||||
|
private static async runCommand(command: string): Promise<void> {
|
||||||
|
Logger.info(`> ${command}`);
|
||||||
|
Logger.info(child_process.execSync(command).toString());
|
||||||
|
}
|
||||||
|
|
||||||
public async checkSecuritySettings(): Promise<void> {
|
public async checkSecuritySettings(): Promise<void> {
|
||||||
this.checkSecurityConfigField('gitlab_webhook_token');
|
this.checkSecurityConfigField('gitlab_webhook_token');
|
||||||
}
|
}
|
||||||
@ -13,7 +18,8 @@ export default class AutoUpdateComponent extends ApplicationComponent {
|
|||||||
public async init(router: Router): Promise<void> {
|
public async init(router: Router): Promise<void> {
|
||||||
router.post('/update/push.json', (req, res) => {
|
router.post('/update/push.json', (req, res) => {
|
||||||
const token = req.header('X-Gitlab-Token');
|
const token = req.header('X-Gitlab-Token');
|
||||||
if (!token || token !== config.get<string>('gitlab_webhook_token')) throw new ForbiddenHttpError('Invalid token', req.url);
|
if (!token || token !== config.get<string>('gitlab_webhook_token'))
|
||||||
|
throw new ForbiddenHttpError('Invalid token', req.url);
|
||||||
|
|
||||||
this.update(req.body).catch(Logger.error);
|
this.update(req.body).catch(Logger.error);
|
||||||
|
|
||||||
@ -23,20 +29,20 @@ export default class AutoUpdateComponent extends ApplicationComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async update(params: any) {
|
private async update(params: { [p: string]: unknown }) {
|
||||||
Logger.info('Update params:', params);
|
Logger.info('Update params:', params);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Logger.info('Starting auto update...');
|
Logger.info('Starting auto update...');
|
||||||
|
|
||||||
// Fetch
|
// Fetch
|
||||||
await this.runCommand(`git pull`);
|
await AutoUpdateComponent.runCommand(`git pull`);
|
||||||
|
|
||||||
// Install new dependencies
|
// Install new dependencies
|
||||||
await this.runCommand(`yarn install --production=false`);
|
await AutoUpdateComponent.runCommand(`yarn install --production=false`);
|
||||||
|
|
||||||
// Process assets
|
// Process assets
|
||||||
await this.runCommand(`yarn dist`);
|
await AutoUpdateComponent.runCommand(`yarn dist`);
|
||||||
|
|
||||||
// Stop app
|
// Stop app
|
||||||
await this.getApp().stop();
|
await this.getApp().stop();
|
||||||
@ -46,9 +52,4 @@ export default class AutoUpdateComponent extends ApplicationComponent {
|
|||||||
Logger.error(e, 'An error occurred while running the auto update.');
|
Logger.error(e, 'An error occurred while running the auto update.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private async runCommand(command: string): Promise<void> {
|
|
||||||
Logger.info(`> ${command}`);
|
|
||||||
Logger.info(child_process.execSync(command).toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -7,14 +7,14 @@ import {AuthMiddleware} from "../auth/AuthComponent";
|
|||||||
export default class CsrfProtectionComponent extends ApplicationComponent {
|
export default class CsrfProtectionComponent extends ApplicationComponent {
|
||||||
private static readonly excluders: ((req: Request) => boolean)[] = [];
|
private static readonly excluders: ((req: Request) => boolean)[] = [];
|
||||||
|
|
||||||
public static getCSRFToken(session: Express.Session): string {
|
public static getCsrfToken(session: Express.Session): string {
|
||||||
if (typeof session.csrf !== 'string') {
|
if (typeof session.csrf !== 'string') {
|
||||||
session.csrf = crypto.randomBytes(64).toString('base64');
|
session.csrf = crypto.randomBytes(64).toString('base64');
|
||||||
}
|
}
|
||||||
return session.csrf;
|
return session.csrf;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static addExcluder(excluder: (req: Request) => boolean) {
|
public static addExcluder(excluder: (req: Request) => boolean): void {
|
||||||
this.excluders.push(excluder);
|
this.excluders.push(excluder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,22 +24,19 @@ export default class CsrfProtectionComponent extends ApplicationComponent {
|
|||||||
if (excluder(req)) return next();
|
if (excluder(req)) return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!req.session) {
|
const session = req.getSession();
|
||||||
throw new Error('Session is unavailable.');
|
res.locals.getCsrfToken = () => {
|
||||||
}
|
return CsrfProtectionComponent.getCsrfToken(session);
|
||||||
|
|
||||||
res.locals.getCSRFToken = () => {
|
|
||||||
return CsrfProtectionComponent.getCSRFToken(req.session!);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!['GET', 'HEAD', 'OPTIONS'].find(s => s === req.method)) {
|
if (!['GET', 'HEAD', 'OPTIONS'].find(s => s === req.method)) {
|
||||||
try {
|
try {
|
||||||
if (!await req.as(AuthMiddleware).getAuthGuard().isAuthenticatedViaRequest(req)) {
|
if (!await req.as(AuthMiddleware).getAuthGuard().isAuthenticatedViaRequest(req)) {
|
||||||
if (req.session.csrf === undefined) {
|
if (session.csrf === undefined) {
|
||||||
return next(new InvalidCsrfTokenError(req.baseUrl, `You weren't assigned any CSRF token.`));
|
return next(new InvalidCsrfTokenError(req.baseUrl, `You weren't assigned any CSRF token.`));
|
||||||
} else if (req.body.csrf === undefined) {
|
} else if (req.body.csrf === undefined) {
|
||||||
return next(new InvalidCsrfTokenError(req.baseUrl, `You didn't provide any CSRF token.`));
|
return next(new InvalidCsrfTokenError(req.baseUrl, `You didn't provide any CSRF token.`));
|
||||||
} else if (req.session.csrf !== req.body.csrf) {
|
} else if (session.csrf !== req.body.csrf) {
|
||||||
return next(new InvalidCsrfTokenError(req.baseUrl, `Tokens don't match.`));
|
return next(new InvalidCsrfTokenError(req.baseUrl, `Tokens don't match.`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -53,20 +50,20 @@ export default class CsrfProtectionComponent extends ApplicationComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class InvalidCsrfTokenError extends BadRequestError {
|
class InvalidCsrfTokenError extends BadRequestError {
|
||||||
constructor(url: string, details: string, cause?: Error) {
|
public constructor(url: string, details: string, cause?: Error) {
|
||||||
super(
|
super(
|
||||||
`Invalid CSRF token`,
|
`Invalid CSRF token`,
|
||||||
`${details} We can't process this request. Please try again.`,
|
`${details} We can't process this request. Please try again.`,
|
||||||
url,
|
url,
|
||||||
cause
|
cause,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get name(): string {
|
public get name(): string {
|
||||||
return 'Invalid CSRF Token';
|
return 'Invalid CSRF Token';
|
||||||
}
|
}
|
||||||
|
|
||||||
get errorCode(): number {
|
public get errorCode(): number {
|
||||||
return 401;
|
return 401;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ export default class ExpressAppComponent extends ApplicationComponent {
|
|||||||
|
|
||||||
public async init(router: Router): Promise<void> {
|
public async init(router: Router): Promise<void> {
|
||||||
router.use(express.json({
|
router.use(express.json({
|
||||||
type: req => req.headers['content-type']?.match(/^application\/(.+\+)?json$/)
|
type: req => req.headers['content-type']?.match(/^application\/(.+\+)?json$/),
|
||||||
}));
|
}));
|
||||||
router.use(express.urlencoded({
|
router.use(express.urlencoded({
|
||||||
extended: true,
|
extended: true,
|
||||||
@ -42,7 +42,7 @@ export default class ExpressAppComponent extends ApplicationComponent {
|
|||||||
|
|
||||||
router.use((req, res, next) => {
|
router.use((req, res, next) => {
|
||||||
req.middlewares = [];
|
req.middlewares = [];
|
||||||
req.as = <M extends Middleware>(type: Type<M>) => {
|
req.as = <M extends Middleware>(type: Type<M>): M => {
|
||||||
const middleware = req.middlewares.find(m => m.constructor === type);
|
const middleware = req.middlewares.find(m => m.constructor === type);
|
||||||
if (!middleware) throw new Error('Middleware ' + type.name + ' not present in this request.');
|
if (!middleware) throw new Error('Middleware ' + type.name + ' not present in this request.');
|
||||||
return middleware as M;
|
return middleware as M;
|
||||||
@ -52,8 +52,9 @@ export default class ExpressAppComponent extends ApplicationComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async stop(): Promise<void> {
|
public async stop(): Promise<void> {
|
||||||
if (this.server) {
|
const server = this.server;
|
||||||
await this.close('Webserver', this.server, this.server.close);
|
if (server) {
|
||||||
|
await this.close('Webserver', callback => server.close(callback));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,6 +64,7 @@ export default class ExpressAppComponent extends ApplicationComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getExpressApp(): Express {
|
public getExpressApp(): Express {
|
||||||
return this.expressApp!;
|
if (!this.expressApp) throw new Error('Express app not initialized.');
|
||||||
|
return this.expressApp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,7 @@ import {Router} from "express";
|
|||||||
export default class FormHelperComponent extends ApplicationComponent {
|
export default class FormHelperComponent extends ApplicationComponent {
|
||||||
public async init(router: Router): Promise<void> {
|
public async init(router: Router): Promise<void> {
|
||||||
router.use((req, res, next) => {
|
router.use((req, res, next) => {
|
||||||
if (!req.session) {
|
let _validation: unknown | null;
|
||||||
throw new Error('Session is unavailable.');
|
|
||||||
}
|
|
||||||
|
|
||||||
let _validation: any = null;
|
|
||||||
res.locals.validation = () => {
|
res.locals.validation = () => {
|
||||||
if (!_validation) {
|
if (!_validation) {
|
||||||
const v = req.flash('validation');
|
const v = req.flash('validation');
|
||||||
@ -16,9 +12,9 @@ export default class FormHelperComponent extends ApplicationComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return _validation;
|
return _validation;
|
||||||
}
|
};
|
||||||
|
|
||||||
let _previousFormData: any = null;
|
let _previousFormData: unknown | null = null;
|
||||||
res.locals.previousFormData = () => {
|
res.locals.previousFormData = () => {
|
||||||
if (!_previousFormData) {
|
if (!_previousFormData) {
|
||||||
const v = req.flash('previousFormData');
|
const v = req.flash('previousFormData');
|
||||||
@ -39,4 +35,4 @@ export default class FormHelperComponent extends ApplicationComponent {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,29 +2,24 @@ import ApplicationComponent from "../ApplicationComponent";
|
|||||||
import onFinished from "on-finished";
|
import onFinished from "on-finished";
|
||||||
import Logger from "../Logger";
|
import Logger from "../Logger";
|
||||||
import {Request, Response, Router} from "express";
|
import {Request, Response, Router} from "express";
|
||||||
|
import {HttpError} from "../HttpError";
|
||||||
|
|
||||||
export default class LogRequestsComponent extends ApplicationComponent {
|
export default class LogRequestsComponent extends ApplicationComponent {
|
||||||
private static fullRequests: boolean = false;
|
private static fullRequests: boolean = false;
|
||||||
|
|
||||||
public static logFullHttpRequests() {
|
public static logFullHttpRequests(): void {
|
||||||
this.fullRequests = true;
|
this.fullRequests = true;
|
||||||
Logger.info('Http requests will be logged with more details.');
|
Logger.info('Http requests will be logged with more details.');
|
||||||
}
|
}
|
||||||
|
|
||||||
public static logRequest(req: Request, res: Response, err: any = null, additionalStr: string = '', silent: boolean = false): string {
|
public static logRequest(
|
||||||
if (!LogRequestsComponent.fullRequests) {
|
req: Request,
|
||||||
let logStr = `${req.method} ${req.originalUrl} - ${res.statusCode}`;
|
res: Response,
|
||||||
if (err) {
|
err?: unknown,
|
||||||
if (silent) {
|
additionalStr: string = '',
|
||||||
logStr += ` ${err.errorCode} ${err.name}`;
|
silent: boolean = false,
|
||||||
return Logger.silentError(err, logStr);
|
): string {
|
||||||
} else {
|
if (LogRequestsComponent.fullRequests) {
|
||||||
return Logger.error(err, logStr, additionalStr, err);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Logger.info(logStr);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const requestObj = JSON.stringify({
|
const requestObj = JSON.stringify({
|
||||||
ip: req.ip,
|
ip: req.ip,
|
||||||
host: req.hostname,
|
host: req.hostname,
|
||||||
@ -36,16 +31,37 @@ export default class LogRequestsComponent extends ApplicationComponent {
|
|||||||
body: req.body,
|
body: req.body,
|
||||||
files: req.files,
|
files: req.files,
|
||||||
cookies: req.cookies,
|
cookies: req.cookies,
|
||||||
sessionId: req.sessionID,
|
sessionId: req.session?.id,
|
||||||
result: {
|
result: {
|
||||||
code: res.statusCode
|
code: res.statusCode,
|
||||||
}
|
},
|
||||||
}, null, 4);
|
}, null, 4);
|
||||||
if (err) {
|
if (err) {
|
||||||
return Logger.error(err, requestObj, err);
|
if (err instanceof Error) {
|
||||||
|
return Logger.error(err, requestObj, err);
|
||||||
|
} else {
|
||||||
|
return Logger.error(new Error(String(err)), requestObj);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Logger.info(requestObj);
|
Logger.info(requestObj);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
let logStr = `${req.method} ${req.originalUrl} - ${res.statusCode}`;
|
||||||
|
if (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
if (silent) {
|
||||||
|
if (err instanceof HttpError) logStr += ` ${err.errorCode}`;
|
||||||
|
logStr += ` ${err.name}`;
|
||||||
|
return Logger.silentError(err, logStr);
|
||||||
|
} else {
|
||||||
|
return Logger.error(err, logStr, additionalStr, err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Logger.error(new Error(String(err)), logStr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger.info(logStr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
@ -61,4 +77,4 @@ export default class LogRequestsComponent extends ApplicationComponent {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ export default class MailComponent extends ApplicationComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start(app: Express): Promise<void> {
|
public async start(_app: Express): Promise<void> {
|
||||||
await this.prepare('Mail connection', () => Mail.prepare());
|
await this.prepare('Mail connection', () => Mail.prepare());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,4 +23,4 @@ export default class MailComponent extends ApplicationComponent {
|
|||||||
Mail.end();
|
Mail.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ export default class MaintenanceComponent extends ApplicationComponent {
|
|||||||
private readonly application: Application;
|
private readonly application: Application;
|
||||||
private readonly canServe: () => boolean;
|
private readonly canServe: () => boolean;
|
||||||
|
|
||||||
constructor(application: Application, canServe: () => boolean) {
|
public constructor(application: Application, canServe: () => boolean) {
|
||||||
super();
|
super();
|
||||||
this.application = application;
|
this.application = application;
|
||||||
this.canServe = canServe;
|
this.canServe = canServe;
|
||||||
@ -34,4 +34,4 @@ export default class MaintenanceComponent extends ApplicationComponent {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import {Express} from "express";
|
|||||||
import MysqlConnectionManager from "../db/MysqlConnectionManager";
|
import MysqlConnectionManager from "../db/MysqlConnectionManager";
|
||||||
|
|
||||||
export default class MysqlComponent extends ApplicationComponent {
|
export default class MysqlComponent extends ApplicationComponent {
|
||||||
public async start(app: Express): Promise<void> {
|
public async start(_app: Express): Promise<void> {
|
||||||
await this.prepare('Mysql connection', () => MysqlConnectionManager.prepare());
|
await this.prepare('Mysql connection', () => MysqlConnectionManager.prepare());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -15,4 +15,4 @@ export default class MysqlComponent extends ApplicationComponent {
|
|||||||
return MysqlConnectionManager.isReady();
|
return MysqlConnectionManager.isReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,7 @@ import config from "config";
|
|||||||
import {Express, NextFunction, Request, Response, Router} from "express";
|
import {Express, NextFunction, Request, Response, Router} from "express";
|
||||||
import ApplicationComponent from "../ApplicationComponent";
|
import ApplicationComponent from "../ApplicationComponent";
|
||||||
import Controller from "../Controller";
|
import Controller from "../Controller";
|
||||||
import {ServerError} from "../HttpError";
|
|
||||||
import * as querystring from "querystring";
|
import * as querystring from "querystring";
|
||||||
import {ParsedUrlQueryInput} from "querystring";
|
|
||||||
import * as util from "util";
|
import * as util from "util";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
@ -42,11 +40,7 @@ export default class NunjucksComponent extends ApplicationComponent {
|
|||||||
noCache: !config.get('view.cache'),
|
noCache: !config.get('view.cache'),
|
||||||
throwOnUndefined: true,
|
throwOnUndefined: true,
|
||||||
})
|
})
|
||||||
.addGlobal('route', (route: string, params?: { [p: string]: string } | [], query?: ParsedUrlQueryInput, absolute?: boolean) => {
|
.addGlobal('route', Controller.route)
|
||||||
const path = Controller.route(route, params, query, absolute);
|
|
||||||
if (path === null) throw new ServerError(`Route ${route} not found.`);
|
|
||||||
return path;
|
|
||||||
})
|
|
||||||
.addGlobal('app_version', this.getApp().getVersion())
|
.addGlobal('app_version', this.getApp().getVersion())
|
||||||
.addGlobal('core_version', coreVersion)
|
.addGlobal('core_version', coreVersion)
|
||||||
.addGlobal('querystring', querystring)
|
.addGlobal('querystring', querystring)
|
||||||
@ -62,7 +56,7 @@ export default class NunjucksComponent extends ApplicationComponent {
|
|||||||
app.set('view engine', 'njk');
|
app.set('view engine', 'njk');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async init(router: Router): Promise<void> {
|
public async init(_router: Router): Promise<void> {
|
||||||
this.use(NunjucksMiddleware);
|
this.use(NunjucksMiddleware);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ export default class RedirectBackComponent extends ApplicationComponent {
|
|||||||
|
|
||||||
public async handle(router: Router): Promise<void> {
|
public async handle(router: Router): Promise<void> {
|
||||||
router.use((req, res, next) => {
|
router.use((req, res, next) => {
|
||||||
res.redirectBack = (defaultUrl?: string) => {
|
res.redirectBack = (defaultUrl?: string): void => {
|
||||||
const previousUrl = RedirectBackComponent.getPreviousURL(req, defaultUrl);
|
const previousUrl = RedirectBackComponent.getPreviousURL(req, defaultUrl);
|
||||||
if (!previousUrl) throw new ServerError(`Couldn't redirect you back.`);
|
if (!previousUrl) throw new ServerError(`Couldn't redirect you back.`);
|
||||||
res.redirect(previousUrl);
|
res.redirect(previousUrl);
|
||||||
@ -22,12 +22,15 @@ export default class RedirectBackComponent extends ApplicationComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onFinished(res, (err) => {
|
onFinished(res, (err) => {
|
||||||
if (req.session) {
|
const session = req.session;
|
||||||
const contentType = (res.getHeaders())['content-type'];
|
if (session) {
|
||||||
if (!err && res.statusCode === 200 && (contentType && typeof contentType !== 'number' && contentType.indexOf('text/html') >= 0)) {
|
const contentType = res.getHeader('content-type');
|
||||||
req.session!.previousUrl = req.originalUrl;
|
if (!err && res.statusCode === 200 && (
|
||||||
Logger.debug('Prev url set to', req.session!.previousUrl);
|
contentType && typeof contentType !== 'number' && contentType.indexOf('text/html') >= 0
|
||||||
req.session!.save((err) => {
|
)) {
|
||||||
|
session.previousUrl = req.originalUrl;
|
||||||
|
Logger.debug('Prev url set to', session.previousUrl);
|
||||||
|
session.save((err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
Logger.error(err, 'Error while saving session');
|
Logger.error(err, 'Error while saving session');
|
||||||
}
|
}
|
||||||
@ -39,4 +42,4 @@ export default class RedirectBackComponent extends ApplicationComponent {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,11 +13,11 @@ export default class RedisComponent extends ApplicationComponent implements Cach
|
|||||||
private redisClient?: RedisClient;
|
private redisClient?: RedisClient;
|
||||||
private store?: Store;
|
private store?: Store;
|
||||||
|
|
||||||
public async start(app: Express): Promise<void> {
|
public async start(_app: Express): Promise<void> {
|
||||||
this.redisClient = redis.createClient(config.get('redis.port'), config.get('redis.host'), {
|
this.redisClient = redis.createClient(config.get('redis.port'), config.get('redis.host'), {
|
||||||
password: config.has('redis.password') ? config.get<string>('redis.password') : undefined,
|
password: config.has('redis.password') ? config.get<string>('redis.password') : undefined,
|
||||||
});
|
});
|
||||||
this.redisClient.on('error', (err: any) => {
|
this.redisClient.on('error', (err: Error) => {
|
||||||
Logger.error(err, 'An error occurred with redis.');
|
Logger.error(err, 'An error occurred with redis.');
|
||||||
});
|
});
|
||||||
this.store = new RedisStore({
|
this.store = new RedisStore({
|
||||||
@ -27,8 +27,9 @@ export default class RedisComponent extends ApplicationComponent implements Cach
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async stop(): Promise<void> {
|
public async stop(): Promise<void> {
|
||||||
if (this.redisClient) {
|
const redisClient = this.redisClient;
|
||||||
await this.close('Redis connection', this.redisClient, this.redisClient.quit);
|
if (redisClient) {
|
||||||
|
await this.close('Redis connection', callback => redisClient.quit(callback));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,7 +43,7 @@ export default class RedisComponent extends ApplicationComponent implements Cach
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async get(key: string, defaultValue?: string): Promise<string | null> {
|
public async get(key: string, defaultValue?: string): Promise<string | null> {
|
||||||
return new Promise<string | null>((resolve, reject) => {
|
return await new Promise<string | null>((resolve, reject) => {
|
||||||
if (!this.redisClient) {
|
if (!this.redisClient) {
|
||||||
reject(`Redis store was not initialized.`);
|
reject(`Redis store was not initialized.`);
|
||||||
return;
|
return;
|
||||||
@ -53,7 +54,7 @@ export default class RedisComponent extends ApplicationComponent implements Cach
|
|||||||
reject(err);
|
reject(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resolve(val !== null ? val : (defaultValue !== undefined ? defaultValue : null));
|
resolve(val || defaultValue || null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -63,7 +64,7 @@ export default class RedisComponent extends ApplicationComponent implements Cach
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async forget(key: string): Promise<void> {
|
public async forget(key: string): Promise<void> {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return await new Promise<void>((resolve, reject) => {
|
||||||
if (!this.redisClient) {
|
if (!this.redisClient) {
|
||||||
reject(`Redis store was not initialized.`);
|
reject(`Redis store was not initialized.`);
|
||||||
return;
|
return;
|
||||||
@ -81,7 +82,7 @@ export default class RedisComponent extends ApplicationComponent implements Cach
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async remember(key: string, value: string, ttl: number): Promise<void> {
|
public async remember(key: string, value: string, ttl: number): Promise<void> {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return await new Promise<void>((resolve, reject) => {
|
||||||
if (!this.redisClient) {
|
if (!this.redisClient) {
|
||||||
reject(`Redis store was not initialized.`);
|
reject(`Redis store was not initialized.`);
|
||||||
return;
|
return;
|
||||||
@ -96,4 +97,4 @@ export default class RedisComponent extends ApplicationComponent implements Cach
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,21 +7,18 @@ export default class ServeStaticDirectoryComponent extends ApplicationComponent
|
|||||||
private readonly root: string;
|
private readonly root: string;
|
||||||
private readonly path?: PathParams;
|
private readonly path?: PathParams;
|
||||||
|
|
||||||
constructor(root: string, routePath?: PathParams) {
|
public constructor(root: string, routePath?: PathParams) {
|
||||||
super();
|
super();
|
||||||
this.root = path.join(__dirname, '../../../', root);
|
this.root = path.join(__dirname, '../../../', root);
|
||||||
this.path = routePath;
|
this.path = routePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async init(router: Router): Promise<void> {
|
public async init(router: Router): Promise<void> {
|
||||||
if (typeof this.path !== 'undefined') {
|
if (this.path) {
|
||||||
router.use(this.path, express.static(this.root, {maxAge: 1000 * 3600 * 72}));
|
router.use(this.path, express.static(this.root, {maxAge: 1000 * 3600 * 72}));
|
||||||
} else {
|
} else {
|
||||||
router.use(express.static(this.root, {maxAge: 1000 * 3600 * 72}));
|
router.use(express.static(this.root, {maxAge: 1000 * 3600 * 72}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stop(): Promise<void> {
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -14,7 +14,6 @@ export default class SessionComponent extends ApplicationComponent {
|
|||||||
this.storeComponent = storeComponent;
|
this.storeComponent = storeComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async checkSecuritySettings(): Promise<void> {
|
public async checkSecuritySettings(): Promise<void> {
|
||||||
this.checkSecurityConfigField('session.secret');
|
this.checkSecurityConfigField('session.secret');
|
||||||
if (!config.get<boolean>('session.cookie.secure')) {
|
if (!config.get<boolean>('session.cookie.secure')) {
|
||||||
@ -39,17 +38,18 @@ export default class SessionComponent extends ApplicationComponent {
|
|||||||
router.use(flash());
|
router.use(flash());
|
||||||
|
|
||||||
router.use((req, res, next) => {
|
router.use((req, res, next) => {
|
||||||
if (!req.session) {
|
req.getSession = () => {
|
||||||
throw new Error('Session is unavailable.');
|
if (!req.session) throw new Error('Session not initialized.');
|
||||||
}
|
return req.session;
|
||||||
|
};
|
||||||
|
|
||||||
res.locals.session = req.session;
|
res.locals.session = req.getSession();
|
||||||
|
|
||||||
let _flash: any = {};
|
const _flash: FlashStorage = {};
|
||||||
res.locals.flash = (key?: string) => {
|
res.locals.flash = (key?: string): FlashMessages | unknown[] => {
|
||||||
if (key !== undefined) {
|
if (key !== undefined) {
|
||||||
if (_flash[key] === undefined) _flash[key] = req.flash(key) || null;
|
if (_flash[key] === undefined) _flash[key] = req.flash(key);
|
||||||
return _flash[key];
|
return _flash[key] || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_flash._messages === undefined) {
|
if (_flash._messages === undefined) {
|
||||||
@ -65,4 +65,19 @@ export default class SessionComponent extends ApplicationComponent {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FlashMessages = {
|
||||||
|
[k: string]: unknown[] | undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DefaultFlashMessages = FlashMessages & {
|
||||||
|
info?: unknown[] | undefined;
|
||||||
|
success?: unknown[] | undefined;
|
||||||
|
warning?: unknown[] | undefined;
|
||||||
|
error?: unknown[] | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FlashStorage = FlashMessages & {
|
||||||
|
_messages?: DefaultFlashMessages,
|
||||||
|
};
|
||||||
|
@ -14,7 +14,7 @@ import NunjucksComponent from "./NunjucksComponent";
|
|||||||
export default class WebSocketServerComponent extends ApplicationComponent {
|
export default class WebSocketServerComponent extends ApplicationComponent {
|
||||||
private wss?: WebSocket.Server;
|
private wss?: WebSocket.Server;
|
||||||
|
|
||||||
constructor(
|
public constructor(
|
||||||
private readonly application: Application,
|
private readonly application: Application,
|
||||||
private readonly expressAppComponent: ExpressAppComponent,
|
private readonly expressAppComponent: ExpressAppComponent,
|
||||||
private readonly storeComponent: RedisComponent,
|
private readonly storeComponent: RedisComponent,
|
||||||
@ -23,8 +23,8 @@ export default class WebSocketServerComponent extends ApplicationComponent {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start(app: Express): Promise<void> {
|
public async start(_app: Express): Promise<void> {
|
||||||
const listeners: { [p: string]: WebSocketListener<any> } = this.application.getWebSocketListeners();
|
const listeners: { [p: string]: WebSocketListener<Application> } = this.application.getWebSocketListeners();
|
||||||
this.wss = new WebSocketServer({
|
this.wss = new WebSocketServer({
|
||||||
server: this.expressAppComponent.getServer(),
|
server: this.expressAppComponent.getServer(),
|
||||||
}, () => {
|
}, () => {
|
||||||
@ -78,8 +78,9 @@ export default class WebSocketServerComponent extends ApplicationComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async stop(): Promise<void> {
|
public async stop(): Promise<void> {
|
||||||
if (this.wss) {
|
const wss = this.wss;
|
||||||
await this.close('WebSocket server', this.wss, this.wss.close);
|
if (wss) {
|
||||||
|
await this.close('WebSocket server', callback => wss.close(callback));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import {Connection} from "mysql";
|
import {Connection} from "mysql";
|
||||||
import MysqlConnectionManager from "./MysqlConnectionManager";
|
import MysqlConnectionManager from "./MysqlConnectionManager";
|
||||||
|
import {Type} from "../Utils";
|
||||||
|
|
||||||
export default abstract class Migration {
|
export default abstract class Migration {
|
||||||
public readonly version: number;
|
public readonly version: number;
|
||||||
@ -16,9 +17,13 @@ export default abstract class Migration {
|
|||||||
|
|
||||||
public abstract async rollback(connection: Connection): Promise<void>;
|
public abstract async rollback(connection: Connection): Promise<void>;
|
||||||
|
|
||||||
public abstract registerModels(): void;
|
public registerModels?(): void;
|
||||||
|
|
||||||
protected async query(queryString: string, connection: Connection): Promise<void> {
|
protected async query(queryString: string, connection: Connection): Promise<void> {
|
||||||
await MysqlConnectionManager.query(queryString, undefined, connection);
|
await MysqlConnectionManager.query(queryString, undefined, connection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MigrationType<M extends Migration> extends Type<M> {
|
||||||
|
new(version: number): M;
|
||||||
|
}
|
||||||
|
114
src/db/Model.ts
114
src/db/Model.ts
@ -1,69 +1,75 @@
|
|||||||
import MysqlConnectionManager, {query} from "./MysqlConnectionManager";
|
import MysqlConnectionManager, {isQueryVariable, query, QueryVariable} from "./MysqlConnectionManager";
|
||||||
import Validator from "./Validator";
|
import Validator from "./Validator";
|
||||||
import {Connection} from "mysql";
|
import {Connection} from "mysql";
|
||||||
import ModelComponent from "./ModelComponent";
|
import ModelComponent from "./ModelComponent";
|
||||||
import {Type} from "../Utils";
|
import {Type} from "../Utils";
|
||||||
import ModelFactory from "./ModelFactory";
|
import ModelFactory, {PrimaryKeyValue} from "./ModelFactory";
|
||||||
import ModelRelation from "./ModelRelation";
|
import ModelRelation from "./ModelRelation";
|
||||||
import ModelQuery, {ModelQueryResult, SelectFields} from "./ModelQuery";
|
import ModelQuery, {ModelFieldData, ModelQueryResult, SelectFields} from "./ModelQuery";
|
||||||
import {Request} from "express";
|
import {Request} from "express";
|
||||||
import Extendable from "../Extendable";
|
import Extendable from "../Extendable";
|
||||||
|
|
||||||
export default abstract class Model implements Extendable<ModelComponent<Model>> {
|
export default abstract class Model implements Extendable<ModelComponent<Model>> {
|
||||||
public static get table(): string {
|
public static get table(): string {
|
||||||
return this.name
|
const single = this.name
|
||||||
.replace(/(?:^|\.?)([A-Z])/g, (x, y) => '_' + y.toLowerCase())
|
.replace(/(?:^|\.?)([A-Z])/g, (x, y) => '_' + y.toLowerCase())
|
||||||
.replace(/^_/, '')
|
.replace(/^_/, '');
|
||||||
+ 's';
|
return single + 's';
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getPrimaryKeyFields(): string[] {
|
public static getPrimaryKeyFields(): string[] {
|
||||||
return ['id'];
|
return ['id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static create<T extends Model>(this: ModelType<T>, data: any): T {
|
public static create<M extends Model>(this: ModelType<M>, data: Pick<M, keyof M>): M {
|
||||||
return ModelFactory.get(this).create(data, true);
|
return ModelFactory.get(this).create(data, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static select<T extends Model>(this: ModelType<T>, ...fields: SelectFields): ModelQuery<T> {
|
public static select<M extends Model>(this: ModelType<M>, ...fields: SelectFields): ModelQuery<M> {
|
||||||
return ModelFactory.get(this).select(...fields);
|
return ModelFactory.get(this).select(...fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static update<T extends Model>(this: ModelType<T>, data: { [key: string]: any }): ModelQuery<T> {
|
public static update<M extends Model>(this: ModelType<M>, data: Pick<M, keyof M>): ModelQuery<M> {
|
||||||
return ModelFactory.get(this).update(data);
|
return ModelFactory.get(this).update(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static delete<T extends Model>(this: ModelType<T>): ModelQuery<T> {
|
public static delete<M extends Model>(this: ModelType<M>): ModelQuery<M> {
|
||||||
return ModelFactory.get(this).delete();
|
return ModelFactory.get(this).delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async getById<T extends Model>(this: ModelType<T>, ...id: any): Promise<T | null> {
|
public static async getById<M extends Model>(this: ModelType<M>, ...id: PrimaryKeyValue[]): Promise<M | null> {
|
||||||
return ModelFactory.get(this).getById(...id);
|
return await ModelFactory.get(this).getById(...id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async paginate<T extends Model>(this: ModelType<T>, request: Request, perPage: number = 20, query?: ModelQuery<T>): Promise<ModelQueryResult<T>> {
|
public static async paginate<M extends Model>(
|
||||||
return ModelFactory.get(this).paginate(request, perPage, query);
|
this: ModelType<M>,
|
||||||
|
request: Request,
|
||||||
|
perPage: number = 20,
|
||||||
|
query?: ModelQuery<M>,
|
||||||
|
): Promise<ModelQueryResult<M>> {
|
||||||
|
return await ModelFactory.get(this).paginate(request, perPage, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected readonly _factory: ModelFactory<Model>;
|
protected readonly _factory: ModelFactory<Model>;
|
||||||
private readonly _components: ModelComponent<this>[] = [];
|
private readonly _components: ModelComponent<this>[] = [];
|
||||||
private readonly _validators: { [key: string]: Validator<any> | undefined } = {};
|
private readonly _validators: { [K in keyof this]?: Validator<this[K]> | undefined } = {};
|
||||||
private _exists: boolean;
|
private _exists: boolean;
|
||||||
|
|
||||||
[key: string]: any;
|
[key: string]: ModelFieldData;
|
||||||
|
|
||||||
public constructor(factory: ModelFactory<Model>, isNew: boolean) {
|
public constructor(factory: ModelFactory<never>, isNew: boolean) {
|
||||||
if (!factory || !(factory instanceof ModelFactory)) throw new Error('Cannot instantiate model directly.');
|
if (!(factory instanceof ModelFactory)) throw new Error('Cannot instantiate model directly.');
|
||||||
this._factory = factory;
|
this._factory = factory;
|
||||||
this.init();
|
this.init?.();
|
||||||
this._exists = !isNew;
|
this._exists = !isNew;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract init(): void;
|
protected init?(): void;
|
||||||
|
|
||||||
protected setValidation<T>(propertyName: keyof this): Validator<T> {
|
protected setValidation<K extends keyof this>(propertyName: K): Validator<this[K]> {
|
||||||
const validator = new Validator<T>();
|
const validator = new Validator<this[K]>();
|
||||||
this._validators[propertyName as string] = validator;
|
this._validators[propertyName] = validator;
|
||||||
return validator;
|
return validator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,10 +98,10 @@ export default abstract class Model implements Extendable<ModelComponent<Model>>
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateWithData(data: any) {
|
public updateWithData(data: Pick<this, keyof this> | Record<string, unknown>): void {
|
||||||
for (const property of this._properties) {
|
for (const property of this._properties) {
|
||||||
if (data[property] !== undefined) {
|
if (data[property] !== undefined) {
|
||||||
this[property] = data[property];
|
this[property] = data[property] as this[keyof this & string];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -118,22 +124,24 @@ export default abstract class Model implements Extendable<ModelComponent<Model>>
|
|||||||
|
|
||||||
const needs_full_update = connection ?
|
const needs_full_update = connection ?
|
||||||
await this.saveTransaction(connection) :
|
await this.saveTransaction(connection) :
|
||||||
await MysqlConnectionManager.wrapTransaction(async connection => this.saveTransaction(connection));
|
await MysqlConnectionManager.wrapTransaction(async connection => await this.saveTransaction(connection));
|
||||||
|
|
||||||
const callback = async () => {
|
const callback = async () => {
|
||||||
if (needs_full_update) {
|
if (needs_full_update) {
|
||||||
const result = await this._factory.select()
|
const query = this._factory.select();
|
||||||
.where('id', this.id!)
|
for (const field of this._factory.getPrimaryKeyFields()) {
|
||||||
.limit(1)
|
query.where(field, this[field]);
|
||||||
.execute(connection);
|
}
|
||||||
|
query.limit(1);
|
||||||
|
const result = await query.execute(connection);
|
||||||
this.updateWithData(result.results[0]);
|
this.updateWithData(result.results[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.afterSave?.();
|
await this.afterSave?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (connection) {
|
if (postHook) {
|
||||||
postHook!(callback);
|
postHook(callback);
|
||||||
} else {
|
} else {
|
||||||
await callback();
|
await callback();
|
||||||
}
|
}
|
||||||
@ -142,19 +150,19 @@ export default abstract class Model implements Extendable<ModelComponent<Model>>
|
|||||||
private async saveTransaction(connection: Connection): Promise<boolean> {
|
private async saveTransaction(connection: Connection): Promise<boolean> {
|
||||||
// Before save
|
// Before save
|
||||||
await this.beforeSave?.(connection);
|
await this.beforeSave?.(connection);
|
||||||
if (!this.exists() && this.hasOwnProperty('created_at')) {
|
if (!this.exists() && this.hasProperty('created_at')) {
|
||||||
this.created_at = new Date();
|
this.created_at = new Date();
|
||||||
}
|
}
|
||||||
if (this.exists() && this.hasOwnProperty('updated_at')) {
|
if (this.exists() && this.hasProperty('updated_at')) {
|
||||||
this.updated_at = new Date();
|
this.updated_at = new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
const properties = [];
|
const properties = [];
|
||||||
const values = [];
|
const values: QueryVariable[] = [];
|
||||||
let needsFullUpdate = false;
|
let needsFullUpdate = false;
|
||||||
|
|
||||||
if (this.exists()) {
|
if (this.exists()) {
|
||||||
const data: any = {};
|
const data: { [K in keyof this]?: this[K] } = {};
|
||||||
for (const property of this._properties) {
|
for (const property of this._properties) {
|
||||||
const value = this[property];
|
const value = this[property];
|
||||||
|
|
||||||
@ -170,21 +178,24 @@ export default abstract class Model implements Extendable<ModelComponent<Model>>
|
|||||||
} else {
|
} else {
|
||||||
const props_holders = [];
|
const props_holders = [];
|
||||||
for (const property of this._properties) {
|
for (const property of this._properties) {
|
||||||
const value = this[property];
|
let value: ModelFieldData = this[property];
|
||||||
|
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
needsFullUpdate = true;
|
needsFullUpdate = true;
|
||||||
} else {
|
} else {
|
||||||
|
if (!isQueryVariable(value)) {
|
||||||
|
value = value.toString();
|
||||||
|
}
|
||||||
properties.push(property);
|
properties.push(property);
|
||||||
props_holders.push('?');
|
props_holders.push('?');
|
||||||
values.push(value);
|
values.push(value as QueryVariable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldNames = properties.map(f => `\`${f}\``).join(', ');
|
const fieldNames = properties.map(f => `\`${f}\``).join(', ');
|
||||||
const result = await query(`INSERT INTO ${this.table} (${fieldNames}) VALUES(${props_holders.join(', ')})`, values, connection);
|
const result = await query(`INSERT INTO ${this.table} (${fieldNames}) VALUES(${props_holders.join(', ')})`, values, connection);
|
||||||
|
|
||||||
if (this.hasOwnProperty('id')) this.id = result.other.insertId;
|
if (this.hasProperty('id')) this.id = Number(result.other?.insertId);
|
||||||
this._exists = true;
|
this._exists = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,7 +203,7 @@ export default abstract class Model implements Extendable<ModelComponent<Model>>
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async delete(): Promise<void> {
|
public async delete(): Promise<void> {
|
||||||
if (!(await this.exists())) throw new Error('This model instance doesn\'t exist in DB.');
|
if (!await this.exists()) throw new Error('This model instance doesn\'t exist in DB.');
|
||||||
|
|
||||||
const query = this._factory.delete();
|
const query = this._factory.delete();
|
||||||
for (const indexField of this._factory.getPrimaryKeyFields()) {
|
for (const indexField of this._factory.getPrimaryKeyFields()) {
|
||||||
@ -204,7 +215,7 @@ export default abstract class Model implements Extendable<ModelComponent<Model>>
|
|||||||
|
|
||||||
public async validate(onlyFormat: boolean = false, connection?: Connection): Promise<void[]> {
|
public async validate(onlyFormat: boolean = false, connection?: Connection): Promise<void[]> {
|
||||||
return await Promise.all(this._properties.map(
|
return await Promise.all(this._properties.map(
|
||||||
prop => this._validators[prop]?.execute(prop, this[prop], onlyFormat, connection)
|
prop => this._validators[prop]?.execute(prop, this[prop], onlyFormat, connection),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,19 +234,30 @@ export default abstract class Model implements Extendable<ModelComponent<Model>>
|
|||||||
return this._factory.table;
|
return this._factory.table;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _properties(): string[] {
|
private get _properties(): (keyof this & string)[] {
|
||||||
return Object.getOwnPropertyNames(this).filter(p => {
|
return Object.getOwnPropertyNames(this).filter(p => {
|
||||||
return !p.startsWith('_') &&
|
return !p.startsWith('_') &&
|
||||||
typeof this[p] !== 'function' &&
|
typeof this[p] !== 'function' &&
|
||||||
!(this[p] instanceof ModelRelation);
|
!(this[p] instanceof ModelRelation);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private hasProperty(key: string | number | symbol): key is keyof this {
|
||||||
|
return typeof key === 'string' && this._properties.indexOf(key) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getOrFail<K extends keyof this & string>(k: K): NonNullable<this[K]> {
|
||||||
|
if (!this[k]) throw new Error(k + ' not initialized.');
|
||||||
|
return this[k] as NonNullable<this[K]>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModelType<T extends Model> extends Type<T> {
|
export interface ModelType<M extends Model> extends Type<M> {
|
||||||
table: string;
|
table: string;
|
||||||
|
|
||||||
new(factory: ModelFactory<any>, isNew: boolean): T;
|
new(factory: ModelFactory<never>, isNew: boolean): M;
|
||||||
|
|
||||||
getPrimaryKeyFields(): string[];
|
getPrimaryKeyFields(): (keyof M & string)[];
|
||||||
|
|
||||||
|
select<M extends Model>(this: ModelType<M>, ...fields: SelectFields): ModelQuery<M>;
|
||||||
}
|
}
|
||||||
|
@ -1,42 +1,43 @@
|
|||||||
import Model from "./Model";
|
import Model from "./Model";
|
||||||
import Validator from "./Validator";
|
import Validator from "./Validator";
|
||||||
import {getMethods} from "../Utils";
|
import {getMethods} from "../Utils";
|
||||||
|
import {ModelFieldData} from "./ModelQuery";
|
||||||
|
|
||||||
export default abstract class ModelComponent<T extends Model> {
|
export default abstract class ModelComponent<M extends Model> {
|
||||||
protected readonly _model: T;
|
protected readonly _model: M;
|
||||||
private readonly _validators: { [key: string]: Validator<any> } = {};
|
private readonly _validators: { [K in keyof this]?: Validator<this[K]> } = {};
|
||||||
|
|
||||||
[key: string]: any;
|
[key: string]: ModelFieldData;
|
||||||
|
|
||||||
public constructor(model: T) {
|
public constructor(model: M) {
|
||||||
this._model = model;
|
this._model = model;
|
||||||
}
|
}
|
||||||
|
|
||||||
public applyToModel(): void {
|
public applyToModel(): void {
|
||||||
this.init?.();
|
this.init?.();
|
||||||
|
|
||||||
|
const model = this._model as Model;
|
||||||
|
|
||||||
for (const property of this._properties) {
|
for (const property of this._properties) {
|
||||||
if (!property.startsWith('_')) {
|
if (!property.startsWith('_')) {
|
||||||
(this._model as Model)[property] = this[property];
|
model[property] = this[property];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const method of getMethods(this)) {
|
for (const method of getMethods(this)) {
|
||||||
// @ts-ignore
|
|
||||||
if (!method.startsWith('_') &&
|
if (!method.startsWith('_') &&
|
||||||
['init', 'setValidation'].indexOf(method) < 0 &&
|
['init', 'setValidation'].indexOf(method) < 0 &&
|
||||||
this._model[method] === undefined) {
|
model[method] === undefined) {
|
||||||
// @ts-ignore
|
model[method] = this[method];
|
||||||
this._model[method] = this[method];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected init?(): void;
|
protected init?(): void;
|
||||||
|
|
||||||
protected setValidation<V>(propertyName: keyof this): Validator<V> {
|
protected setValidation<K extends keyof this>(propertyName: K): Validator<this[K]> {
|
||||||
const validator = new Validator<V>();
|
const validator = new Validator<this[K]>();
|
||||||
this._validators[propertyName as string] = validator;
|
this._validators[propertyName] = validator;
|
||||||
return validator;
|
return validator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,33 +3,37 @@ import Model, {ModelType} from "./Model";
|
|||||||
import ModelQuery, {ModelQueryResult, SelectFields} from "./ModelQuery";
|
import ModelQuery, {ModelQueryResult, SelectFields} from "./ModelQuery";
|
||||||
import {Request} from "express";
|
import {Request} from "express";
|
||||||
|
|
||||||
export default class ModelFactory<T extends Model> {
|
export default class ModelFactory<M extends Model> {
|
||||||
private static readonly factories: { [modelType: string]: ModelFactory<any> } = {};
|
private static readonly factories: { [modelType: string]: ModelFactory<Model> | undefined } = {};
|
||||||
|
|
||||||
public static register<M extends Model>(modelType: ModelType<M>) {
|
public static register<M extends Model>(modelType: ModelType<M>): void {
|
||||||
if (this.factories[modelType.name]) throw new Error(`Factory for type ${modelType.name} already defined.`);
|
if (this.factories[modelType.name]) throw new Error(`Factory for type ${modelType.name} already defined.`);
|
||||||
this.factories[modelType.name] = new ModelFactory<M>(modelType);
|
this.factories[modelType.name] = new ModelFactory<M>(modelType) as unknown as ModelFactory<Model>;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static get<M extends Model>(modelType: ModelType<M>): ModelFactory<M> {
|
public static get<M extends Model>(modelType: ModelType<M>): ModelFactory<M> {
|
||||||
const factory = this.factories[modelType.name];
|
const factory = this.factories[modelType.name];
|
||||||
if (!factory) throw new Error(`No factory registered for ${modelType.name}.`);
|
if (!factory) throw new Error(`No factory registered for ${modelType.name}.`);
|
||||||
return factory;
|
return factory as unknown as ModelFactory<M>;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly modelType: ModelType<T>;
|
public static has<M extends Model>(modelType: ModelType<M>): boolean {
|
||||||
private readonly components: ModelComponentFactory<T>[] = [];
|
return !!this.factories[modelType.name];
|
||||||
|
}
|
||||||
|
|
||||||
protected constructor(modelType: ModelType<T>) {
|
private readonly modelType: ModelType<M>;
|
||||||
|
private readonly components: ModelComponentFactory<M>[] = [];
|
||||||
|
|
||||||
|
protected constructor(modelType: ModelType<M>) {
|
||||||
this.modelType = modelType;
|
this.modelType = modelType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public addComponent(modelComponentFactory: ModelComponentFactory<T>) {
|
public addComponent(modelComponentFactory: ModelComponentFactory<M>): void {
|
||||||
this.components.push(modelComponentFactory);
|
this.components.push(modelComponentFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
public create(data: any, isNewModel: boolean): T {
|
public create(data: Pick<M, keyof M>, isNewModel: boolean): M {
|
||||||
const model = new this.modelType(this, isNewModel);
|
const model = new this.modelType(this as unknown as ModelFactory<never>, isNewModel);
|
||||||
for (const component of this.components) {
|
for (const component of this.components) {
|
||||||
model.addComponent(new component(model));
|
model.addComponent(new component(model));
|
||||||
}
|
}
|
||||||
@ -41,41 +45,41 @@ export default class ModelFactory<T extends Model> {
|
|||||||
return this.modelType.table;
|
return this.modelType.table;
|
||||||
}
|
}
|
||||||
|
|
||||||
public select(...fields: SelectFields): ModelQuery<T> {
|
public select(...fields: SelectFields): ModelQuery<M> {
|
||||||
return ModelQuery.select(this, ...fields);
|
return ModelQuery.select(this, ...fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
public update(data: { [key: string]: any }): ModelQuery<T> {
|
public update(data: Pick<M, keyof M>): ModelQuery<M> {
|
||||||
return ModelQuery.update(this, data);
|
return ModelQuery.update(this, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public delete(): ModelQuery<T> {
|
public delete(): ModelQuery<M> {
|
||||||
return ModelQuery.delete(this);
|
return ModelQuery.delete(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPrimaryKeyFields(): string[] {
|
public getPrimaryKeyFields(): (keyof M & string)[] {
|
||||||
return this.modelType.getPrimaryKeyFields();
|
return this.modelType.getPrimaryKeyFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPrimaryKey(modelData: any): any[] {
|
public getPrimaryKey(modelData: Pick<M, keyof M>): Pick<M, keyof M>[keyof M & string][] {
|
||||||
return this.getPrimaryKeyFields().map(f => modelData[f]);
|
return this.getPrimaryKeyFields().map(f => modelData[f]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPrimaryKeyString(modelData: any): string {
|
public getPrimaryKeyString(modelData: Pick<M, keyof M>): string {
|
||||||
return this.getPrimaryKey(modelData).join(',');
|
return this.getPrimaryKey(modelData).join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getById(...id: any): Promise<T | null> {
|
public async getById(...id: PrimaryKeyValue[]): Promise<M | null> {
|
||||||
let query = this.select();
|
let query = this.select();
|
||||||
const primaryKeyFields = this.getPrimaryKeyFields();
|
const primaryKeyFields = this.getPrimaryKeyFields();
|
||||||
for (let i = 0; i < primaryKeyFields.length; i++) {
|
for (let i = 0; i < primaryKeyFields.length; i++) {
|
||||||
query = query.where(primaryKeyFields[i], id[i]);
|
query = query.where(primaryKeyFields[i], id[i]);
|
||||||
}
|
}
|
||||||
return query.first();
|
return await query.first();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async paginate(request: Request, perPage: number = 20, query?: ModelQuery<T>): Promise<ModelQueryResult<T>> {
|
public async paginate(request: Request, perPage: number = 20, query?: ModelQuery<M>): Promise<ModelQueryResult<M>> {
|
||||||
let page = request.params.page ? parseInt(request.params.page) : 1;
|
const page = request.params.page ? parseInt(request.params.page) : 1;
|
||||||
if (!query) query = this.select();
|
if (!query) query = this.select();
|
||||||
if (request.params.sortBy) {
|
if (request.params.sortBy) {
|
||||||
const dir = request.params.sortDirection;
|
const dir = request.params.sortDirection;
|
||||||
@ -87,4 +91,6 @@ export default class ModelFactory<T extends Model> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ModelComponentFactory<T extends Model> = new (model: T) => ModelComponent<T>;
|
export type ModelComponentFactory<M extends Model> = new (model: M) => ModelComponent<M>;
|
||||||
|
|
||||||
|
export type PrimaryKeyValue = string | number | boolean | null | undefined;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {query, QueryResult} from "./MysqlConnectionManager";
|
import {isQueryVariable, query, QueryResult, QueryVariable} from "./MysqlConnectionManager";
|
||||||
import {Connection} from "mysql";
|
import {Connection} from "mysql";
|
||||||
import Model from "./Model";
|
import Model from "./Model";
|
||||||
import Pagination from "../Pagination";
|
import Pagination from "../Pagination";
|
||||||
@ -12,14 +12,10 @@ export default class ModelQuery<M extends Model> implements WhereFieldConsumer<M
|
|||||||
return new ModelQuery(QueryType.SELECT, factory, fields.length > 0 ? fields : ['*']);
|
return new ModelQuery(QueryType.SELECT, factory, fields.length > 0 ? fields : ['*']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static update<M extends Model>(factory: ModelFactory<M>, data: {
|
public static update<M extends Model>(factory: ModelFactory<M>, data: { [K in keyof M]?: M[K] }): ModelQuery<M> {
|
||||||
[key: string]: any
|
|
||||||
}): ModelQuery<M> {
|
|
||||||
const fields = [];
|
const fields = [];
|
||||||
for (let key in data) {
|
for (const key of Object.keys(data)) {
|
||||||
if (data.hasOwnProperty(key)) {
|
fields.push(new UpdateFieldValue(inputToFieldOrValue(key, factory.table), data[key], false));
|
||||||
fields.push(new UpdateFieldValue(inputToFieldOrValue(key, factory.table), data[key], false));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return new ModelQuery(QueryType.UPDATE, factory, fields);
|
return new ModelQuery(QueryType.UPDATE, factory, fields);
|
||||||
}
|
}
|
||||||
@ -41,7 +37,7 @@ export default class ModelQuery<M extends Model> implements WhereFieldConsumer<M
|
|||||||
private _sortBy?: string;
|
private _sortBy?: string;
|
||||||
private _sortDirection?: 'ASC' | 'DESC';
|
private _sortDirection?: 'ASC' | 'DESC';
|
||||||
private readonly relations: string[] = [];
|
private readonly relations: string[] = [];
|
||||||
private readonly subRelations: { [relation: string]: string[] } = {};
|
private readonly subRelations: { [relation: string]: string[] | undefined } = {};
|
||||||
private _pivot?: string[];
|
private _pivot?: string[];
|
||||||
private _union?: ModelQueryUnion;
|
private _union?: ModelQueryUnion;
|
||||||
private _recursiveRelation?: RelationDatabaseProperties;
|
private _recursiveRelation?: RelationDatabaseProperties;
|
||||||
@ -60,33 +56,57 @@ export default class ModelQuery<M extends Model> implements WhereFieldConsumer<M
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public on(field1: string, field2: string, test: WhereTest = WhereTest.EQ, operator: WhereOperator = WhereOperator.AND): this {
|
public on(
|
||||||
this._leftJoinOn.push(new WhereFieldValue(inputToFieldOrValue(field1), inputToFieldOrValue(field2), true, test, operator));
|
field1: string,
|
||||||
|
field2: string,
|
||||||
|
test: WhereTest = WhereTest.EQ,
|
||||||
|
operator: WhereOperator = WhereOperator.AND,
|
||||||
|
): this {
|
||||||
|
this._leftJoinOn.push(new WhereFieldValue(
|
||||||
|
inputToFieldOrValue(field1), inputToFieldOrValue(field2), true, test, operator,
|
||||||
|
));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public where(field: string, value: string | Date | ModelQuery<any> | any, test: WhereTest = WhereTest.EQ, operator: WhereOperator = WhereOperator.AND): this {
|
public where(
|
||||||
|
field: string,
|
||||||
|
value: ModelFieldData,
|
||||||
|
test: WhereTest = WhereTest.EQ,
|
||||||
|
operator: WhereOperator = WhereOperator.AND,
|
||||||
|
): this {
|
||||||
this._where.push(new WhereFieldValue(field, value, false, test, operator));
|
this._where.push(new WhereFieldValue(field, value, false, test, operator));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public groupWhere(setter: (query: WhereFieldConsumer<M>) => void, operator: WhereOperator = WhereOperator.AND): this {
|
public groupWhere(
|
||||||
|
setter: (query: WhereFieldConsumer<M>) => void,
|
||||||
|
operator: WhereOperator = WhereOperator.AND,
|
||||||
|
): this {
|
||||||
this._where.push(new WhereFieldValueGroup(this.collectWheres(setter), operator));
|
this._where.push(new WhereFieldValueGroup(this.collectWheres(setter), operator));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private collectWheres(setter: (query: WhereFieldConsumer<M>) => void): (WhereFieldValue | WhereFieldValueGroup)[] {
|
private collectWheres(setter: (query: WhereFieldConsumer<M>) => void): (WhereFieldValue | WhereFieldValueGroup)[] {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
const query = this;
|
const query = this;
|
||||||
const wheres: (WhereFieldValue | WhereFieldValueGroup)[] = [];
|
const wheres: (WhereFieldValue | WhereFieldValueGroup)[] = [];
|
||||||
setter({
|
setter({
|
||||||
where(field: string, value: string | Date | ModelQuery<any> | any, test: WhereTest = WhereTest.EQ, operator: WhereOperator = WhereOperator.AND) {
|
where(
|
||||||
|
field: string,
|
||||||
|
value: ModelFieldData,
|
||||||
|
test: WhereTest = WhereTest.EQ,
|
||||||
|
operator: WhereOperator = WhereOperator.AND,
|
||||||
|
) {
|
||||||
wheres.push(new WhereFieldValue(field, value, false, test, operator));
|
wheres.push(new WhereFieldValue(field, value, false, test, operator));
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
groupWhere(setter: (query: WhereFieldConsumer<M>) => void, operator: WhereOperator = WhereOperator.AND) {
|
groupWhere(
|
||||||
wheres.push(new WhereFieldValueGroup(query.collectWheres(setter), operator))
|
setter: (query: WhereFieldConsumer<M>) => void,
|
||||||
|
operator: WhereOperator = WhereOperator.AND,
|
||||||
|
) {
|
||||||
|
wheres.push(new WhereFieldValueGroup(query.collectWheres(setter), operator));
|
||||||
return this;
|
return this;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
return wheres;
|
return wheres;
|
||||||
}
|
}
|
||||||
@ -113,7 +133,7 @@ export default class ModelQuery<M extends Model> implements WhereFieldConsumer<M
|
|||||||
if (this.relations.indexOf(parts[0]) < 0) this.relations.push(parts[0]);
|
if (this.relations.indexOf(parts[0]) < 0) this.relations.push(parts[0]);
|
||||||
if (parts.length > 1) {
|
if (parts.length > 1) {
|
||||||
if (!this.subRelations[parts[0]]) this.subRelations[parts[0]] = [];
|
if (!this.subRelations[parts[0]]) this.subRelations[parts[0]] = [];
|
||||||
this.subRelations[parts[0]].push(parts.slice(1).join('.'));
|
this.subRelations[parts[0]]?.push(parts.slice(1).join('.'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return this;
|
return this;
|
||||||
@ -124,7 +144,13 @@ export default class ModelQuery<M extends Model> implements WhereFieldConsumer<M
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public union(query: ModelQuery<any>, sortBy: string, direction: SortDirection = 'ASC', raw: boolean = false, limit?: number, offset?: number): this {
|
public union(
|
||||||
|
query: ModelQuery<Model>,
|
||||||
|
sortBy: string, direction: SortDirection = 'ASC',
|
||||||
|
raw: boolean = false,
|
||||||
|
limit?: number,
|
||||||
|
offset?: number,
|
||||||
|
): this {
|
||||||
if (this.type !== QueryType.SELECT) throw new Error('Union queries are only implemented with SELECT.');
|
if (this.type !== QueryType.SELECT) throw new Error('Union queries are only implemented with SELECT.');
|
||||||
|
|
||||||
this._union = {
|
this._union = {
|
||||||
@ -150,7 +176,7 @@ export default class ModelQuery<M extends Model> implements WhereFieldConsumer<M
|
|||||||
if (this._pivot) this.fields.push(...this._pivot);
|
if (this._pivot) this.fields.push(...this._pivot);
|
||||||
|
|
||||||
// Prevent wildcard and fields from conflicting
|
// Prevent wildcard and fields from conflicting
|
||||||
let fields = this.fields.map(f => {
|
const fields = this.fields.map(f => {
|
||||||
const field = f.toString();
|
const field = f.toString();
|
||||||
if (field.startsWith('(')) return f; // Skip sub-queries
|
if (field.startsWith('(')) return f; // Skip sub-queries
|
||||||
return inputToFieldOrValue(field, this.table);
|
return inputToFieldOrValue(field, this.table);
|
||||||
@ -184,7 +210,7 @@ export default class ModelQuery<M extends Model> implements WhereFieldConsumer<M
|
|||||||
|
|
||||||
let orderBy = '';
|
let orderBy = '';
|
||||||
if (typeof this._sortBy === 'string') {
|
if (typeof this._sortBy === 'string') {
|
||||||
orderBy = ` ORDER BY ${this._sortBy} ${this._sortDirection!}`;
|
orderBy = ` ORDER BY ${this._sortBy} ${this._sortDirection}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const table = `\`${this.table}\``;
|
const table = `\`${this.table}\``;
|
||||||
@ -193,7 +219,9 @@ export default class ModelQuery<M extends Model> implements WhereFieldConsumer<M
|
|||||||
case QueryType.SELECT:
|
case QueryType.SELECT:
|
||||||
if (this._recursiveRelation) {
|
if (this._recursiveRelation) {
|
||||||
const cteFields = fields.replace(RegExp(`${table}`, 'g'), 'o');
|
const cteFields = fields.replace(RegExp(`${table}`, 'g'), 'o');
|
||||||
const idKey = this._reverseRecursiveRelation ? this._recursiveRelation.foreignKey : this._recursiveRelation.localKey;
|
const idKey = this._reverseRecursiveRelation ?
|
||||||
|
this._recursiveRelation.foreignKey :
|
||||||
|
this._recursiveRelation.localKey;
|
||||||
const sortOrder = this._reverseRecursiveRelation ? 'DESC' : 'ASC';
|
const sortOrder = this._reverseRecursiveRelation ? 'DESC' : 'ASC';
|
||||||
|
|
||||||
query = `WITH RECURSIVE cte AS (`
|
query = `WITH RECURSIVE cte AS (`
|
||||||
@ -228,10 +256,10 @@ export default class ModelQuery<M extends Model> implements WhereFieldConsumer<M
|
|||||||
return this.toString(true);
|
return this.toString(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get variables(): any[] {
|
public get variables(): QueryVariable[] {
|
||||||
const variables: any[] = [];
|
const variables: QueryVariable[] = [];
|
||||||
this.fields?.filter(v => v instanceof FieldValue)
|
this.fields.filter(v => v instanceof FieldValue)
|
||||||
.flatMap(v => (<FieldValue>v).variables)
|
.flatMap(v => (v as FieldValue).variables)
|
||||||
.forEach(v => variables.push(v));
|
.forEach(v => variables.push(v));
|
||||||
this._where.flatMap(v => this.getVariables(v))
|
this._where.flatMap(v => this.getVariables(v))
|
||||||
.forEach(v => variables.push(v));
|
.forEach(v => variables.push(v));
|
||||||
@ -239,7 +267,7 @@ export default class ModelQuery<M extends Model> implements WhereFieldConsumer<M
|
|||||||
return variables;
|
return variables;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getVariables(where: WhereFieldValue | WhereFieldValueGroup): any[] {
|
private getVariables(where: WhereFieldValue | WhereFieldValueGroup): QueryVariable[] {
|
||||||
return where instanceof WhereFieldValueGroup ?
|
return where instanceof WhereFieldValueGroup ?
|
||||||
where.fields.flatMap(v => this.getVariables(v)) :
|
where.fields.flatMap(v => this.getVariables(v)) :
|
||||||
where.variables;
|
where.variables;
|
||||||
@ -257,33 +285,34 @@ export default class ModelQuery<M extends Model> implements WhereFieldConsumer<M
|
|||||||
if (this._pivot) models.pivot = [];
|
if (this._pivot) models.pivot = [];
|
||||||
|
|
||||||
// Eager loading init
|
// Eager loading init
|
||||||
const relationMap: { [p: string]: ModelRelation<any, any, any>[] } = {};
|
const relationMap: { [p: string]: ModelRelation<Model, Model, Model | Model[] | null>[] } = {};
|
||||||
for (const relation of this.relations) {
|
for (const relation of this.relations) {
|
||||||
relationMap[relation] = [];
|
relationMap[relation] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const result of queryResult.results) {
|
for (const result of queryResult.results) {
|
||||||
const modelData: any = {};
|
const modelData: Record<string, unknown> = {};
|
||||||
for (const field of Object.keys(result)) {
|
for (const field of Object.keys(result)) {
|
||||||
modelData[field.split('.')[1] || field] = result[field];
|
modelData[field.split('.')[1] || field] = result[field];
|
||||||
}
|
}
|
||||||
|
|
||||||
const model = this.factory.create(modelData, false);
|
const model = this.factory.create(modelData as Pick<M, keyof M>, false);
|
||||||
models.push(model);
|
models.push(model);
|
||||||
models.originalData.push(modelData);
|
models.originalData.push(modelData);
|
||||||
|
|
||||||
if (this._pivot) {
|
if (this._pivot && models.pivot) {
|
||||||
const pivotData: any = {};
|
const pivotData: Record<string, unknown> = {};
|
||||||
for (const field of this._pivot) {
|
for (const field of this._pivot) {
|
||||||
pivotData[field] = result[field.split('.')[1]];
|
pivotData[field] = result[field.split('.')[1]];
|
||||||
}
|
}
|
||||||
models.pivot!.push(pivotData);
|
models.pivot.push(pivotData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eager loading init map
|
// Eager loading init map
|
||||||
for (const relation of this.relations) {
|
for (const relation of this.relations) {
|
||||||
if (model[relation] === undefined) throw new Error(`Relation ${relation} doesn't exist on ${model.constructor.name}.`);
|
if (model[relation] === undefined) throw new Error(`Relation ${relation} doesn't exist on ${model.constructor.name}.`);
|
||||||
relationMap[relation].push(model[relation]);
|
if (!(model[relation] instanceof ModelRelation)) throw new Error(`Field ${relation} is not a relation on ${model.constructor.name}.`);
|
||||||
|
relationMap[relation].push(model[relation] as ModelRelation<Model, Model, Model | Model[] | null>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,8 +350,9 @@ export default class ModelQuery<M extends Model> implements WhereFieldConsumer<M
|
|||||||
|
|
||||||
this.fields.splice(0, this.fields.length);
|
this.fields.splice(0, this.fields.length);
|
||||||
this.fields.push(new SelectFieldValue('_count', 'COUNT(*)', true));
|
this.fields.push(new SelectFieldValue('_count', 'COUNT(*)', true));
|
||||||
let queryResult = await this.execute(connection);
|
|
||||||
return queryResult.results[0]['_count'];
|
const queryResult = await this.execute(connection);
|
||||||
|
return Number(queryResult.results[0]['_count']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -338,9 +368,9 @@ function inputToFieldOrValue(input: string, addTable?: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ModelQueryResult<M extends Model> extends Array<M> {
|
export interface ModelQueryResult<M extends Model> extends Array<M> {
|
||||||
originalData?: any[];
|
originalData?: Record<string, unknown>[];
|
||||||
pagination?: Pagination<M>;
|
pagination?: Pagination<M>;
|
||||||
pivot?: { [p: string]: any }[];
|
pivot?: Record<string, unknown>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum QueryType {
|
export enum QueryType {
|
||||||
@ -366,19 +396,30 @@ export enum WhereTest {
|
|||||||
|
|
||||||
class FieldValue {
|
class FieldValue {
|
||||||
protected readonly field: string;
|
protected readonly field: string;
|
||||||
protected value: any;
|
protected value: ModelFieldData;
|
||||||
protected raw: boolean;
|
protected raw: boolean;
|
||||||
|
|
||||||
constructor(field: string, value: any, raw: boolean) {
|
public constructor(field: string, value: ModelFieldData, raw: boolean) {
|
||||||
this.field = field;
|
this.field = field;
|
||||||
this.value = value;
|
this.value = value;
|
||||||
this.raw = raw;
|
this.raw = raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
public toString(first: boolean = true): string {
|
public toString(first: boolean = true): string {
|
||||||
const valueStr = (this.raw || this.value === null || this.value instanceof ModelQuery) ? this.value :
|
let valueStr: string;
|
||||||
(Array.isArray(this.value) ? `(${'?'.repeat(this.value.length).split('').join(',')})` : '?');
|
if (this.value instanceof ModelQuery) {
|
||||||
let field = inputToFieldOrValue(this.field);
|
valueStr = this.value.toString(false);
|
||||||
|
} else if (this.value === null || this.value === undefined) {
|
||||||
|
valueStr = 'null';
|
||||||
|
} else if (this.raw) {
|
||||||
|
valueStr = this.value.toString();
|
||||||
|
} else {
|
||||||
|
valueStr = Array.isArray(this.value) ?
|
||||||
|
`(${'?'.repeat(this.value.length).split('').join(',')})` :
|
||||||
|
'?';
|
||||||
|
}
|
||||||
|
|
||||||
|
const field = inputToFieldOrValue(this.field);
|
||||||
return `${first ? '' : ','}${field}${this.test}${valueStr}`;
|
return `${first ? '' : ','}${field}${this.test}${valueStr}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -386,17 +427,33 @@ class FieldValue {
|
|||||||
return '=';
|
return '=';
|
||||||
}
|
}
|
||||||
|
|
||||||
public get variables(): any[] {
|
public get variables(): QueryVariable[] {
|
||||||
if (this.value instanceof ModelQuery) return this.value.variables;
|
if (this.value instanceof ModelQuery) return this.value.variables;
|
||||||
if (this.raw || this.value === null) return [];
|
if (this.raw || this.value === null || this.value === undefined) return [];
|
||||||
if (Array.isArray(this.value)) return this.value;
|
if (Array.isArray(this.value)) return this.value.map(value => {
|
||||||
return [this.value];
|
if (!isQueryVariable(value)) value = value.toString();
|
||||||
|
return value;
|
||||||
|
}) as QueryVariable[];
|
||||||
|
|
||||||
|
let value = this.value;
|
||||||
|
if (!isQueryVariable(value)) value = value.toString();
|
||||||
|
return [value as QueryVariable];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SelectFieldValue extends FieldValue {
|
export class SelectFieldValue extends FieldValue {
|
||||||
public toString(first: boolean = true): string {
|
public toString(): string {
|
||||||
return `(${this.value instanceof ModelQuery ? this.value : (this.raw ? this.value : '?')}) AS \`${this.field}\``;
|
let value: string;
|
||||||
|
if (this.value instanceof ModelQuery) {
|
||||||
|
value = this.value.toString(true);
|
||||||
|
} else if (this.value === null || this.value === undefined) {
|
||||||
|
value = 'null';
|
||||||
|
} else {
|
||||||
|
value = this.raw ?
|
||||||
|
this.value.toString() :
|
||||||
|
'?';
|
||||||
|
}
|
||||||
|
return `(${value}) AS \`${this.field}\``;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -407,7 +464,7 @@ class WhereFieldValue extends FieldValue {
|
|||||||
private readonly _test: WhereTest;
|
private readonly _test: WhereTest;
|
||||||
private readonly operator: WhereOperator;
|
private readonly operator: WhereOperator;
|
||||||
|
|
||||||
constructor(field: string, value: any, raw: boolean, test: WhereTest, operator: WhereOperator) {
|
public constructor(field: string, value: ModelFieldData, raw: boolean, test: WhereTest, operator: WhereOperator) {
|
||||||
super(field, value, raw);
|
super(field, value, raw);
|
||||||
this._test = test;
|
this._test = test;
|
||||||
this.operator = operator;
|
this.operator = operator;
|
||||||
@ -451,7 +508,7 @@ class WhereFieldValueGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface WhereFieldConsumer<M extends Model> {
|
export interface WhereFieldConsumer<M extends Model> {
|
||||||
where(field: string, value: string | Date | ModelQuery<any> | any, test?: WhereTest, operator?: WhereOperator): this;
|
where(field: string, value: ModelFieldData, test?: WhereTest, operator?: WhereOperator): this;
|
||||||
|
|
||||||
groupWhere(setter: (query: WhereFieldConsumer<M>) => void, operator?: WhereOperator): this;
|
groupWhere(setter: (query: WhereFieldConsumer<M>) => void, operator?: WhereOperator): this;
|
||||||
}
|
}
|
||||||
@ -461,9 +518,15 @@ export type SelectFields = (string | SelectFieldValue | UpdateFieldValue)[];
|
|||||||
export type SortDirection = 'ASC' | 'DESC';
|
export type SortDirection = 'ASC' | 'DESC';
|
||||||
|
|
||||||
type ModelQueryUnion = {
|
type ModelQueryUnion = {
|
||||||
query: ModelQuery<any>,
|
query: ModelQuery<Model>,
|
||||||
sortBy: string,
|
sortBy: string,
|
||||||
direction: SortDirection,
|
direction: SortDirection,
|
||||||
limit?: number,
|
limit?: number,
|
||||||
offset?: number,
|
offset?: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ModelFieldData =
|
||||||
|
| QueryVariable
|
||||||
|
| ModelQuery<Model>
|
||||||
|
| { toString(): string }
|
||||||
|
| (QueryVariable | { toString(): string })[];
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import ModelQuery, {ModelQueryResult, WhereTest} from "./ModelQuery";
|
import ModelQuery, {ModelFieldData, ModelQueryResult, WhereTest} from "./ModelQuery";
|
||||||
import Model, {ModelType} from "./Model";
|
import Model, {ModelType} from "./Model";
|
||||||
import ModelFactory from "./ModelFactory";
|
import ModelFactory from "./ModelFactory";
|
||||||
|
|
||||||
@ -34,12 +34,12 @@ export default abstract class ModelRelation<S extends Model, O extends Model, R
|
|||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getModelID(): any {
|
public getModelId(): ModelFieldData {
|
||||||
return this.model[this.dbProperties.localKey];
|
return this.model[this.dbProperties.localKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected applyRegularConstraints(query: ModelQuery<O>): void {
|
protected applyRegularConstraints(query: ModelQuery<O>): void {
|
||||||
query.where(this.dbProperties.foreignKey, this.getModelID());
|
query.where(this.dbProperties.foreignKey, this.getModelId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get(): Promise<R> {
|
public async get(): Promise<R> {
|
||||||
@ -69,10 +69,13 @@ export default abstract class ModelRelation<S extends Model, O extends Model, R
|
|||||||
|
|
||||||
protected abstract collectionToOutput(models: O[]): R;
|
protected abstract collectionToOutput(models: O[]): R;
|
||||||
|
|
||||||
public async eagerLoad(relations: ModelRelation<S, O, R>[], subRelations: string[] = []): Promise<ModelQueryResult<O>> {
|
public async eagerLoad(
|
||||||
const ids = relations.map(r => r.getModelID())
|
relations: ModelRelation<S, O, R>[],
|
||||||
|
subRelations: string[] = [],
|
||||||
|
): Promise<ModelQueryResult<O>> {
|
||||||
|
const ids = relations.map(r => r.getModelId())
|
||||||
.filter(id => id !== null && id !== undefined)
|
.filter(id => id !== null && id !== undefined)
|
||||||
.reduce((array: O[], val) => array.indexOf(val) >= 0 ? array : [...array, val], []);
|
.reduce((array: ModelFieldData[], val) => array.indexOf(val) >= 0 ? array : [...array, val], []);
|
||||||
if (ids.length === 0) return [];
|
if (ids.length === 0) return [];
|
||||||
|
|
||||||
const query = this.makeQuery();
|
const query = this.makeQuery();
|
||||||
@ -82,7 +85,7 @@ export default abstract class ModelRelation<S extends Model, O extends Model, R
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async populate(models: ModelQueryResult<O>): Promise<void> {
|
public async populate(models: ModelQueryResult<O>): Promise<void> {
|
||||||
this.cachedModels = models.filter(m => m[this.dbProperties.foreignKey] === this.getModelID())
|
this.cachedModels = models.filter(m => m[this.dbProperties.foreignKey] === this.getModelId())
|
||||||
.reduce((array: O[], val) => array.find(v => v.equals(val)) ? array : [...array, val], []);
|
.reduce((array: O[], val) => array.find(v => v.equals(val)) ? array : [...array, val], []);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,9 +125,13 @@ export class OneModelRelation<S extends Model, O extends Model> extends ModelRel
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ManyModelRelation<S extends Model, O extends Model> extends ModelRelation<S, O, O[]> {
|
export class ManyModelRelation<S extends Model, O extends Model> extends ModelRelation<S, O, O[]> {
|
||||||
protected readonly paginatedCache: { [perPage: number]: { [pageNumber: number]: ModelQueryResult<O> } } = {};
|
protected readonly paginatedCache: {
|
||||||
|
[perPage: number]: {
|
||||||
|
[pageNumber: number]: ModelQueryResult<O> | undefined
|
||||||
|
} | undefined
|
||||||
|
} = {};
|
||||||
|
|
||||||
constructor(model: S, foreignModelType: ModelType<O>, dbProperties: RelationDatabaseProperties) {
|
public constructor(model: S, foreignModelType: ModelType<O>, dbProperties: RelationDatabaseProperties) {
|
||||||
super(model, foreignModelType, dbProperties);
|
super(model, foreignModelType, dbProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,29 +148,29 @@ export class ManyModelRelation<S extends Model, O extends Model> extends ModelRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async paginate(page: number, perPage: number): Promise<ModelQueryResult<O>> {
|
public async paginate(page: number, perPage: number): Promise<ModelQueryResult<O>> {
|
||||||
if (!this.paginatedCache[perPage]) this.paginatedCache[perPage] = {};
|
let cache = this.paginatedCache[perPage];
|
||||||
|
if (!cache) cache = this.paginatedCache[perPage] = {};
|
||||||
|
|
||||||
const cache = this.paginatedCache[perPage];
|
let cachePage = cache[page];
|
||||||
|
if (!cachePage) {
|
||||||
if (!cache[page]) {
|
|
||||||
const query = this.makeQuery();
|
const query = this.makeQuery();
|
||||||
this.applyRegularConstraints(query);
|
this.applyRegularConstraints(query);
|
||||||
cache[page] = await query.paginate(page, perPage);
|
cachePage = cache[page] = await query.paginate(page, perPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cache[page];
|
return cachePage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ManyThroughModelRelation<S extends Model, O extends Model> extends ManyModelRelation<S, O> {
|
export class ManyThroughModelRelation<S extends Model, O extends Model> extends ManyModelRelation<S, O> {
|
||||||
protected readonly dbProperties: PivotRelationDatabaseProperties;
|
protected readonly dbProperties: PivotRelationDatabaseProperties;
|
||||||
|
|
||||||
constructor(model: S, foreignModelType: ModelType<O>, dbProperties: PivotRelationDatabaseProperties) {
|
public constructor(model: S, foreignModelType: ModelType<O>, dbProperties: PivotRelationDatabaseProperties) {
|
||||||
super(model, foreignModelType, dbProperties);
|
super(model, foreignModelType, dbProperties);
|
||||||
this.dbProperties = dbProperties;
|
this.dbProperties = dbProperties;
|
||||||
this.constraint(query => query
|
this.constraint(query => query
|
||||||
.leftJoin(this.dbProperties.pivotTable, 'pivot')
|
.leftJoin(this.dbProperties.pivotTable, 'pivot')
|
||||||
.on(`pivot.${this.dbProperties.foreignPivotKey}`, `${this.foreignModelType.table}.${this.dbProperties.foreignKey}`)
|
.on(`pivot.${this.dbProperties.foreignPivotKey}`, `${this.foreignModelType.table}.${this.dbProperties.foreignKey}`),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,12 +183,15 @@ export class ManyThroughModelRelation<S extends Model, O extends Model> extends
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected applyRegularConstraints(query: ModelQuery<O>): void {
|
protected applyRegularConstraints(query: ModelQuery<O>): void {
|
||||||
query.where(`pivot.${this.dbProperties.localPivotKey}`, this.getModelID());
|
query.where(`pivot.${this.dbProperties.localPivotKey}`, this.getModelId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async eagerLoad(relations: ModelRelation<S, O, O[]>[], subRelations: string[] = []): Promise<ModelQueryResult<O>> {
|
public async eagerLoad(
|
||||||
const ids = relations.map(r => r.getModelID())
|
relations: ModelRelation<S, O, O[]>[],
|
||||||
.reduce((array: O[], val) => array.indexOf(val) >= 0 ? array : [...array, val], []);
|
subRelations: string[] = [],
|
||||||
|
): Promise<ModelQueryResult<O>> {
|
||||||
|
const ids = relations.map(r => r.getModelId())
|
||||||
|
.reduce((array: ModelFieldData[], val) => array.indexOf(val) >= 0 ? array : [...array, val], []);
|
||||||
if (ids.length === 0) return [];
|
if (ids.length === 0) return [];
|
||||||
|
|
||||||
const query = this.makeQuery();
|
const query = this.makeQuery();
|
||||||
@ -192,8 +202,9 @@ export class ManyThroughModelRelation<S extends Model, O extends Model> extends
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async populate(models: ModelQueryResult<O>): Promise<void> {
|
public async populate(models: ModelQueryResult<O>): Promise<void> {
|
||||||
const ids = models.pivot!
|
if (!models.pivot) throw new Error('ModelQueryResult.pivot not loaded.');
|
||||||
.filter(p => p[`pivot.${this.dbProperties.localPivotKey}`] === this.getModelID())
|
const ids = models.pivot
|
||||||
|
.filter(p => p[`pivot.${this.dbProperties.localPivotKey}`] === this.getModelId())
|
||||||
.map(p => p[`pivot.${this.dbProperties.foreignPivotKey}`]);
|
.map(p => p[`pivot.${this.dbProperties.foreignPivotKey}`]);
|
||||||
this.cachedModels = models.filter(m => ids.indexOf(m[this.dbProperties.foreignKey]) >= 0)
|
this.cachedModels = models.filter(m => ids.indexOf(m[this.dbProperties.foreignKey]) >= 0)
|
||||||
.reduce((array: O[], val) => array.find(v => v.equals(val)) ? array : [...array, val], []);
|
.reduce((array: O[], val) => array.find(v => v.equals(val)) ? array : [...array, val], []);
|
||||||
@ -203,7 +214,12 @@ export class ManyThroughModelRelation<S extends Model, O extends Model> extends
|
|||||||
export class RecursiveModelRelation<M extends Model> extends ManyModelRelation<M, M> {
|
export class RecursiveModelRelation<M extends Model> extends ManyModelRelation<M, M> {
|
||||||
private readonly reverse: boolean;
|
private readonly reverse: boolean;
|
||||||
|
|
||||||
public constructor(model: M, foreignModelType: ModelType<M>, dbProperties: RelationDatabaseProperties, reverse: boolean = false) {
|
public constructor(
|
||||||
|
model: M,
|
||||||
|
foreignModelType: ModelType<M>,
|
||||||
|
dbProperties: RelationDatabaseProperties,
|
||||||
|
reverse: boolean = false,
|
||||||
|
) {
|
||||||
super(model, foreignModelType, dbProperties);
|
super(model, foreignModelType, dbProperties);
|
||||||
this.constraint(query => query.recursive(this.dbProperties, reverse));
|
this.constraint(query => query.recursive(this.dbProperties, reverse));
|
||||||
this.reverse = reverse;
|
this.reverse = reverse;
|
||||||
@ -215,17 +231,19 @@ export class RecursiveModelRelation<M extends Model> extends ManyModelRelation<M
|
|||||||
|
|
||||||
public async populate(models: ModelQueryResult<M>): Promise<void> {
|
public async populate(models: ModelQueryResult<M>): Promise<void> {
|
||||||
await super.populate(models);
|
await super.populate(models);
|
||||||
if (this.cachedModels) {
|
const cachedModels = this.cachedModels;
|
||||||
|
if (cachedModels) {
|
||||||
let count;
|
let count;
|
||||||
do {
|
do {
|
||||||
count = this.cachedModels.length;
|
count = cachedModels.length;
|
||||||
this.cachedModels.push(...models.filter(model =>
|
cachedModels.push(...models.filter(model =>
|
||||||
!this.cachedModels!.find(cached => cached.equals(model)) &&
|
!cachedModels.find(cached => cached.equals(model)) && cachedModels.find(cached => {
|
||||||
this.cachedModels!.find(cached => cached[this.dbProperties.localKey] === model[this.dbProperties.foreignKey])
|
return cached[this.dbProperties.localKey] === model[this.dbProperties.foreignKey];
|
||||||
|
}),
|
||||||
).reduce((array: M[], val) => array.find(v => v.equals(val)) ? array : [...array, val], []));
|
).reduce((array: M[], val) => array.find(v => v.equals(val)) ? array : [...array, val], []));
|
||||||
} while (count !== this.cachedModels.length);
|
} while (count !== cachedModels.length);
|
||||||
|
|
||||||
if (this.reverse) this.cachedModels!.reverse();
|
if (this.reverse) cachedModels.reverse();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,17 +1,21 @@
|
|||||||
import mysql, {Connection, FieldInfo, Pool} from 'mysql';
|
import mysql, {Connection, FieldInfo, MysqlError, Pool, PoolConnection} from 'mysql';
|
||||||
import config from 'config';
|
import config from 'config';
|
||||||
import Migration from "./Migration";
|
import Migration, {MigrationType} from "./Migration";
|
||||||
import Logger from "../Logger";
|
import Logger from "../Logger";
|
||||||
import {Type} from "../Utils";
|
import {Type} from "../Utils";
|
||||||
|
|
||||||
export interface QueryResult {
|
export interface QueryResult {
|
||||||
readonly results: any[];
|
readonly results: Record<string, unknown>[];
|
||||||
readonly fields: FieldInfo[];
|
readonly fields: FieldInfo[];
|
||||||
readonly other?: any;
|
readonly other?: Record<string, unknown>;
|
||||||
foundRows?: number;
|
foundRows?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function query(queryString: string, values?: any, connection?: Connection): Promise<QueryResult> {
|
export async function query(
|
||||||
|
queryString: string,
|
||||||
|
values?: QueryVariable[],
|
||||||
|
connection?: Connection,
|
||||||
|
): Promise<QueryResult> {
|
||||||
return await MysqlConnectionManager.query(queryString, values, connection);
|
return await MysqlConnectionManager.query(queryString, values, connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,7 +29,7 @@ export default class MysqlConnectionManager {
|
|||||||
return this.databaseReady && this.currentPool !== undefined;
|
return this.databaseReady && this.currentPool !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static registerMigrations(migrations: Type<Migration>[]) {
|
public static registerMigrations(migrations: MigrationType<Migration>[]): void {
|
||||||
if (!this.migrationsRegistered) {
|
if (!this.migrationsRegistered) {
|
||||||
this.migrationsRegistered = true;
|
this.migrationsRegistered = true;
|
||||||
migrations.forEach(m => this.registerMigration(v => new m(v)));
|
migrations.forEach(m => this.registerMigration(v => new m(v)));
|
||||||
@ -36,14 +40,14 @@ export default class MysqlConnectionManager {
|
|||||||
this.migrations.push(migration(this.migrations.length + 1));
|
this.migrations.push(migration(this.migrations.length + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static hasMigration(migration: Type<Migration>) {
|
public static hasMigration(migration: Type<Migration>): boolean {
|
||||||
for (const m of this.migrations) {
|
for (const m of this.migrations) {
|
||||||
if (m.constructor === migration) return true;
|
if (m.constructor === migration) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async prepare(runMigrations: boolean = true) {
|
public static async prepare(runMigrations: boolean = true): Promise<void> {
|
||||||
if (config.get('mysql.create_database_automatically') === true) {
|
if (config.get('mysql.create_database_automatically') === true) {
|
||||||
const dbName = config.get('mysql.database');
|
const dbName = config.get('mysql.database');
|
||||||
Logger.info(`Creating database ${dbName}...`);
|
Logger.info(`Creating database ${dbName}...`);
|
||||||
@ -55,11 +59,9 @@ export default class MysqlConnectionManager {
|
|||||||
});
|
});
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
connection.query(`CREATE DATABASE IF NOT EXISTS ${dbName}`, (error) => {
|
connection.query(`CREATE DATABASE IF NOT EXISTS ${dbName}`, (error) => {
|
||||||
if (error !== null) {
|
return error !== null ?
|
||||||
reject(error);
|
reject(error) :
|
||||||
} else {
|
|
||||||
resolve();
|
resolve();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
connection.end();
|
connection.end();
|
||||||
@ -89,20 +91,24 @@ export default class MysqlConnectionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static async endPool(): Promise<void> {
|
public static async endPool(): Promise<void> {
|
||||||
return new Promise(resolve => {
|
return await new Promise(resolve => {
|
||||||
if (this.currentPool !== undefined) {
|
if (this.currentPool === undefined) {
|
||||||
this.currentPool.end(() => {
|
return resolve();
|
||||||
Logger.info('Mysql connection pool ended.');
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
this.currentPool = undefined;
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.currentPool.end(() => {
|
||||||
|
Logger.info('Mysql connection pool ended.');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
this.currentPool = undefined;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async query(queryString: string, values?: any, connection?: Connection): Promise<QueryResult> {
|
public static async query(
|
||||||
|
queryString: string,
|
||||||
|
values: QueryVariable[] = [],
|
||||||
|
connection?: Connection,
|
||||||
|
): Promise<QueryResult> {
|
||||||
return await new Promise<QueryResult>((resolve, reject) => {
|
return await new Promise<QueryResult>((resolve, reject) => {
|
||||||
Logger.dev('SQL:', Logger.isVerboseMode() ? mysql.format(queryString, values) : queryString);
|
Logger.dev('SQL:', Logger.isVerboseMode() ? mysql.format(queryString, values) : queryString);
|
||||||
|
|
||||||
@ -115,7 +121,7 @@ export default class MysqlConnectionManager {
|
|||||||
resolve({
|
resolve({
|
||||||
results: Array.isArray(results) ? results : [],
|
results: Array.isArray(results) ? results : [],
|
||||||
fields: fields !== undefined ? fields : [],
|
fields: fields !== undefined ? fields : [],
|
||||||
other: Array.isArray(results) ? null : results
|
other: Array.isArray(results) ? null : results,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -123,13 +129,13 @@ export default class MysqlConnectionManager {
|
|||||||
|
|
||||||
public static async wrapTransaction<T>(transaction: (connection: Connection) => Promise<T>): Promise<T> {
|
public static async wrapTransaction<T>(transaction: (connection: Connection) => Promise<T>): Promise<T> {
|
||||||
return await new Promise<T>((resolve, reject) => {
|
return await new Promise<T>((resolve, reject) => {
|
||||||
this.pool.getConnection((err, connection) => {
|
this.pool.getConnection((err: MysqlError | undefined, connection: PoolConnection) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
connection.beginTransaction((err) => {
|
connection.beginTransaction((err?: MysqlError) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
this.pool.releaseConnection(connection);
|
this.pool.releaseConnection(connection);
|
||||||
@ -137,7 +143,7 @@ export default class MysqlConnectionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
transaction(connection).then(val => {
|
transaction(connection).then(val => {
|
||||||
connection.commit((err) => {
|
connection.commit((err?: MysqlError) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.rejectAndRollback(connection, err, reject);
|
this.rejectAndRollback(connection, err, reject);
|
||||||
this.pool.releaseConnection(connection);
|
this.pool.releaseConnection(connection);
|
||||||
@ -156,13 +162,15 @@ export default class MysqlConnectionManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static rejectAndRollback(connection: Connection, err: any, reject: (err: any) => void) {
|
private static rejectAndRollback(
|
||||||
connection.rollback((rollbackErr) => {
|
connection: Connection,
|
||||||
if (rollbackErr) {
|
err: MysqlError | undefined,
|
||||||
reject(err + '\n' + rollbackErr);
|
reject: (err: unknown) => void,
|
||||||
} else {
|
) {
|
||||||
|
connection.rollback((rollbackErr?: MysqlError) => {
|
||||||
|
return rollbackErr ?
|
||||||
|
reject(err + '\n' + rollbackErr) :
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,7 +179,7 @@ export default class MysqlConnectionManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await query('SELECT id FROM migrations ORDER BY id DESC LIMIT 1');
|
const result = await query('SELECT id FROM migrations ORDER BY id DESC LIMIT 1');
|
||||||
currentVersion = result.results[0].id;
|
currentVersion = Number(result.results[0]?.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.code === 'ECONNREFUSED' || e.code !== 'ER_NO_SUCH_TABLE') {
|
if (e.code === 'ECONNREFUSED' || e.code !== 'ER_NO_SUCH_TABLE') {
|
||||||
throw new Error('Cannot run migrations: ' + e.code);
|
throw new Error('Cannot run migrations: ' + e.code);
|
||||||
@ -197,16 +205,16 @@ export default class MysqlConnectionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const migration of this.migrations) {
|
for (const migration of this.migrations) {
|
||||||
migration.registerModels();
|
migration.registerModels?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param migrationID what migration to rollback. Use with caution. default=0 is for last registered migration.
|
* @param migrationId what migration to rollback. Use with caution. default=0 is for last registered migration.
|
||||||
*/
|
*/
|
||||||
public static async rollbackMigration(migrationID: number = 0): Promise<void> {
|
public static async rollbackMigration(migrationId: number = 0): Promise<void> {
|
||||||
migrationID--;
|
migrationId--;
|
||||||
const migration = this.migrations[migrationID];
|
const migration = this.migrations[migrationId];
|
||||||
Logger.info('Rolling back migration ', migration.version, migration.constructor.name);
|
Logger.info('Rolling back migration ', migration.version, migration.constructor.name);
|
||||||
await MysqlConnectionManager.wrapTransaction<void>(async c => {
|
await MysqlConnectionManager.wrapTransaction<void>(async c => {
|
||||||
await migration.rollback(c);
|
await migration.rollback(c);
|
||||||
@ -220,12 +228,12 @@ export default class MysqlConnectionManager {
|
|||||||
|
|
||||||
for (let i = 0; i < args.length; i++) {
|
for (let i = 0; i < args.length; i++) {
|
||||||
if (args[i] === 'rollback') {
|
if (args[i] === 'rollback') {
|
||||||
let migrationID = 0;
|
let migrationId = 0;
|
||||||
if (args.length > i + 1) {
|
if (args.length > i + 1) {
|
||||||
migrationID = parseInt(args[i + 1]);
|
migrationId = parseInt(args[i + 1]);
|
||||||
}
|
}
|
||||||
await this.prepare(false);
|
await this.prepare(false);
|
||||||
await this.rollbackMigration(migrationID);
|
await this.rollbackMigration(migrationId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -233,4 +241,21 @@ export default class MysqlConnectionManager {
|
|||||||
await MysqlConnectionManager.endPool();
|
await MysqlConnectionManager.endPool();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type QueryVariable =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| Date
|
||||||
|
| Buffer
|
||||||
|
| null
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
export function isQueryVariable(value: unknown): value is QueryVariable {
|
||||||
|
return typeof value === "string" ||
|
||||||
|
typeof value === 'number' ||
|
||||||
|
value instanceof Date ||
|
||||||
|
value instanceof Buffer ||
|
||||||
|
value === null ||
|
||||||
|
value === undefined;
|
||||||
|
}
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
import Model from "./Model";
|
import Model, {ModelType} from "./Model";
|
||||||
import ModelQuery, {WhereTest} from "./ModelQuery";
|
import ModelQuery, {WhereTest} from "./ModelQuery";
|
||||||
import {Connection} from "mysql";
|
import {Connection} from "mysql";
|
||||||
import {Type} from "../Utils";
|
|
||||||
import {ServerError} from "../HttpError";
|
import {ServerError} from "../HttpError";
|
||||||
|
|
||||||
export const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;
|
export const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;
|
||||||
|
|
||||||
export default class Validator<T> {
|
export default class Validator<V> {
|
||||||
private readonly steps: ValidationStep<T>[] = [];
|
private readonly steps: ValidationStep<V>[] = [];
|
||||||
private readonly validationAttributes: string[] = [];
|
private readonly validationAttributes: string[] = [];
|
||||||
|
|
||||||
private readonly rawValueToHuman?: (val: T) => string;
|
private readonly rawValueToHuman?: (val: V) => string;
|
||||||
private _min?: number;
|
private _min?: number;
|
||||||
private _max?: number;
|
private _max?: number;
|
||||||
|
|
||||||
public constructor(rawValueToHuman?: (val: T) => string) {
|
public constructor(rawValueToHuman?: (val: V) => string) {
|
||||||
this.rawValueToHuman = rawValueToHuman;
|
this.rawValueToHuman = rawValueToHuman;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,8 +23,13 @@ export default class Validator<T> {
|
|||||||
* @param onlyFormat {@code true} to only validate format properties, {@code false} otherwise.
|
* @param onlyFormat {@code true} to only validate format properties, {@code false} otherwise.
|
||||||
* @param connection A connection to use in case of wrapped transactions.
|
* @param connection A connection to use in case of wrapped transactions.
|
||||||
*/
|
*/
|
||||||
async execute(thingName: string, value: T | undefined, onlyFormat: boolean, connection?: Connection): Promise<void> {
|
public async execute(
|
||||||
const bag = new ValidationBag();
|
thingName: string,
|
||||||
|
value: V | undefined,
|
||||||
|
onlyFormat: boolean,
|
||||||
|
connection?: Connection,
|
||||||
|
): Promise<void> {
|
||||||
|
const bag = new ValidationBag<V>();
|
||||||
|
|
||||||
for (const step of this.steps) {
|
for (const step of this.steps) {
|
||||||
if (onlyFormat && !step.isFormat) continue;
|
if (onlyFormat && !step.isFormat) continue;
|
||||||
@ -41,7 +45,7 @@ export default class Validator<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result === false && step.throw) {
|
if (result === false && step.throw) {
|
||||||
const error: ValidationError = step.throw();
|
const error: ValidationError<V> = step.throw();
|
||||||
error.rawValueToHuman = this.rawValueToHuman;
|
error.rawValueToHuman = this.rawValueToHuman;
|
||||||
error.thingName = thingName;
|
error.thingName = thingName;
|
||||||
error.value = value;
|
error.value = value;
|
||||||
@ -57,7 +61,7 @@ export default class Validator<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public defined(): Validator<T> {
|
public defined(): Validator<V> {
|
||||||
this.validationAttributes.push('required');
|
this.validationAttributes.push('required');
|
||||||
|
|
||||||
this.addStep({
|
this.addStep({
|
||||||
@ -68,17 +72,21 @@ export default class Validator<T> {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public acceptUndefined(alsoAcceptEmptyString: boolean = false): Validator<T> {
|
public acceptUndefined(alsoAcceptEmptyString: boolean = false): Validator<V> {
|
||||||
this.addStep({
|
this.addStep({
|
||||||
verifyStep: () => true,
|
verifyStep: () => true,
|
||||||
throw: null,
|
throw: null,
|
||||||
interrupt: val => val === undefined || val === null || (alsoAcceptEmptyString && typeof val === 'string' && val.length === 0),
|
interrupt: val => {
|
||||||
|
return val === undefined ||
|
||||||
|
val === null ||
|
||||||
|
alsoAcceptEmptyString && typeof val === 'string' && val.length === 0;
|
||||||
|
},
|
||||||
isFormat: true,
|
isFormat: true,
|
||||||
});
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public equals(other?: T): Validator<T> {
|
public equals(other?: V): Validator<V> {
|
||||||
this.addStep({
|
this.addStep({
|
||||||
verifyStep: val => val === other,
|
verifyStep: val => val === other,
|
||||||
throw: () => new BadValueValidationError(other),
|
throw: () => new BadValueValidationError(other),
|
||||||
@ -87,7 +95,7 @@ export default class Validator<T> {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public sameAs(otherName?: string, other?: T): Validator<T> {
|
public sameAs(otherName?: string, other?: V): Validator<V> {
|
||||||
this.addStep({
|
this.addStep({
|
||||||
verifyStep: val => val === other,
|
verifyStep: val => val === other,
|
||||||
throw: () => new DifferentThanError(otherName),
|
throw: () => new DifferentThanError(otherName),
|
||||||
@ -96,7 +104,7 @@ export default class Validator<T> {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public regexp(regexp: RegExp): Validator<T> {
|
public regexp(regexp: RegExp): Validator<V> {
|
||||||
this.validationAttributes.push(`pattern="${regexp}"`);
|
this.validationAttributes.push(`pattern="${regexp}"`);
|
||||||
this.addStep({
|
this.addStep({
|
||||||
verifyStep: val => regexp.test(<string><unknown>val),
|
verifyStep: val => regexp.test(<string><unknown>val),
|
||||||
@ -106,9 +114,9 @@ export default class Validator<T> {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public length(length: number): Validator<T> {
|
public length(length: number): Validator<V> {
|
||||||
this.addStep({
|
this.addStep({
|
||||||
verifyStep: val => (<any>val).length === length,
|
verifyStep: val => isLenghtable(val) && val.length === length,
|
||||||
throw: () => new BadLengthValidationError(length),
|
throw: () => new BadLengthValidationError(length),
|
||||||
isFormat: true,
|
isFormat: true,
|
||||||
});
|
});
|
||||||
@ -118,9 +126,9 @@ export default class Validator<T> {
|
|||||||
/**
|
/**
|
||||||
* @param minLength included
|
* @param minLength included
|
||||||
*/
|
*/
|
||||||
public minLength(minLength: number): Validator<T> {
|
public minLength(minLength: number): Validator<V> {
|
||||||
this.addStep({
|
this.addStep({
|
||||||
verifyStep: val => (<any>val).length >= minLength,
|
verifyStep: val => isLenghtable(val) && val.length >= minLength,
|
||||||
throw: () => new TooShortError(minLength),
|
throw: () => new TooShortError(minLength),
|
||||||
isFormat: true,
|
isFormat: true,
|
||||||
});
|
});
|
||||||
@ -130,9 +138,9 @@ export default class Validator<T> {
|
|||||||
/**
|
/**
|
||||||
* @param maxLength included
|
* @param maxLength included
|
||||||
*/
|
*/
|
||||||
public maxLength(maxLength: number): Validator<T> {
|
public maxLength(maxLength: number): Validator<V> {
|
||||||
this.addStep({
|
this.addStep({
|
||||||
verifyStep: val => (<any>val).length <= maxLength,
|
verifyStep: val => isLenghtable(val) && val.length <= maxLength,
|
||||||
throw: () => new TooLongError(maxLength),
|
throw: () => new TooLongError(maxLength),
|
||||||
isFormat: true,
|
isFormat: true,
|
||||||
});
|
});
|
||||||
@ -143,11 +151,11 @@ export default class Validator<T> {
|
|||||||
* @param minLength included
|
* @param minLength included
|
||||||
* @param maxLength included
|
* @param maxLength included
|
||||||
*/
|
*/
|
||||||
public between(minLength: number, maxLength: number): Validator<T> {
|
public between(minLength: number, maxLength: number): Validator<V> {
|
||||||
this.addStep({
|
this.addStep({
|
||||||
verifyStep: val => {
|
verifyStep: val => {
|
||||||
const length = (<any>val).length;
|
return isLenghtable(val) &&
|
||||||
return length >= minLength && length <= maxLength;
|
val.length >= minLength && val.length <= maxLength;
|
||||||
},
|
},
|
||||||
throw: () => new BadLengthValidationError(minLength, maxLength),
|
throw: () => new BadLengthValidationError(minLength, maxLength),
|
||||||
isFormat: true,
|
isFormat: true,
|
||||||
@ -158,12 +166,12 @@ export default class Validator<T> {
|
|||||||
/**
|
/**
|
||||||
* @param min included
|
* @param min included
|
||||||
*/
|
*/
|
||||||
public min(min: number): Validator<T> {
|
public min(min: number): Validator<V> {
|
||||||
this.validationAttributes.push(`min="${min}"`);
|
this.validationAttributes.push(`min="${min}"`);
|
||||||
this._min = min;
|
this._min = min;
|
||||||
this.addStep({
|
this.addStep({
|
||||||
verifyStep: val => {
|
verifyStep: val => {
|
||||||
return (<any>val) >= min;
|
return typeof val === 'number' && val >= min;
|
||||||
},
|
},
|
||||||
throw: () => new OutOfRangeValidationError(this._min, this._max),
|
throw: () => new OutOfRangeValidationError(this._min, this._max),
|
||||||
isFormat: true,
|
isFormat: true,
|
||||||
@ -174,12 +182,12 @@ export default class Validator<T> {
|
|||||||
/**
|
/**
|
||||||
* @param max included
|
* @param max included
|
||||||
*/
|
*/
|
||||||
public max(max: number): Validator<T> {
|
public max(max: number): Validator<V> {
|
||||||
this.validationAttributes.push(`max="${max}"`);
|
this.validationAttributes.push(`max="${max}"`);
|
||||||
this._max = max;
|
this._max = max;
|
||||||
this.addStep({
|
this.addStep({
|
||||||
verifyStep: val => {
|
verifyStep: val => {
|
||||||
return (<any>val) <= max;
|
return typeof val === 'number' && val <= max;
|
||||||
},
|
},
|
||||||
throw: () => new OutOfRangeValidationError(this._min, this._max),
|
throw: () => new OutOfRangeValidationError(this._min, this._max),
|
||||||
isFormat: true,
|
isFormat: true,
|
||||||
@ -187,35 +195,46 @@ export default class Validator<T> {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public unique<M extends Model>(model: M | Type<M>, foreignKey?: string, querySupplier?: () => ModelQuery<M>): Validator<T> {
|
public unique<M extends Model>(
|
||||||
|
model: M | ModelType<M>,
|
||||||
|
foreignKey?: string,
|
||||||
|
querySupplier?: () => ModelQuery<M>,
|
||||||
|
): Validator<V> {
|
||||||
this.addStep({
|
this.addStep({
|
||||||
verifyStep: async (val, thingName, c) => {
|
verifyStep: async (val, thingName, c) => {
|
||||||
if (!foreignKey) foreignKey = thingName;
|
if (!foreignKey) foreignKey = thingName;
|
||||||
let query: ModelQuery<M>;
|
let query: ModelQuery<M>;
|
||||||
if (querySupplier) {
|
if (querySupplier) {
|
||||||
query = querySupplier().where(foreignKey, val);
|
query = querySupplier();
|
||||||
} else {
|
} else {
|
||||||
query = (model instanceof Model ? <any>model.constructor : model).select('').where(foreignKey, val);
|
query = (model instanceof Model ? <ModelType<M>>model.constructor : model).select('');
|
||||||
}
|
}
|
||||||
if (model instanceof Model && typeof model.id === 'number') query = query.where('id', model.id, WhereTest.NE);
|
query.where(foreignKey, val);
|
||||||
|
if (model instanceof Model && typeof model.id === 'number')
|
||||||
|
query = query.where('id', model.id, WhereTest.NE);
|
||||||
return (await query.execute(c)).results.length === 0;
|
return (await query.execute(c)).results.length === 0;
|
||||||
},
|
},
|
||||||
throw: () => new AlreadyExistsValidationError((<any>model).table),
|
throw: () => new AlreadyExistsValidationError(model.table),
|
||||||
isFormat: false,
|
isFormat: false,
|
||||||
});
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public exists(modelClass: Function, foreignKey?: string): Validator<T> {
|
public exists(modelType: ModelType<Model>, foreignKey?: string): Validator<V> {
|
||||||
this.addStep({
|
this.addStep({
|
||||||
verifyStep: async (val, thingName, c) => (await (<any>modelClass).select('').where(foreignKey !== undefined ? foreignKey : thingName, val).execute(c)).results.length >= 1,
|
verifyStep: async (val, thingName, c) => {
|
||||||
throw: () => new UnknownRelationValidationError((<any>modelClass).table, foreignKey),
|
const results = await modelType.select('')
|
||||||
|
.where(foreignKey !== undefined ? foreignKey : thingName, val)
|
||||||
|
.execute(c);
|
||||||
|
return results.results.length >= 1;
|
||||||
|
},
|
||||||
|
throw: () => new UnknownRelationValidationError(modelType.table, foreignKey),
|
||||||
isFormat: false,
|
isFormat: false,
|
||||||
});
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private addStep(step: ValidationStep<T>) {
|
private addStep(step: ValidationStep<V>) {
|
||||||
this.steps.push(step);
|
this.steps.push(step);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,31 +242,31 @@ export default class Validator<T> {
|
|||||||
return this.validationAttributes;
|
return this.validationAttributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public step(step: number): Validator<T> {
|
public step(step: number): Validator<V> {
|
||||||
this.validationAttributes.push(`step="${step}"`);
|
this.validationAttributes.push(`step="${step}"`);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ValidationStep<T> {
|
interface ValidationStep<V> {
|
||||||
interrupt?: (val?: T) => boolean;
|
interrupt?: (val?: V) => boolean;
|
||||||
|
|
||||||
verifyStep(val: T | undefined, thingName: string, connection?: Connection): boolean | Promise<boolean>;
|
verifyStep(val: V | undefined, thingName: string, connection?: Connection): boolean | Promise<boolean>;
|
||||||
|
|
||||||
throw: ((val?: T) => ValidationError) | null;
|
throw: ((val?: V) => ValidationError<V>) | null;
|
||||||
|
|
||||||
readonly isFormat: boolean;
|
readonly isFormat: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ValidationBag extends Error {
|
export class ValidationBag<V> extends Error {
|
||||||
private readonly errors: ValidationError[] = [];
|
private readonly errors: ValidationError<V>[] = [];
|
||||||
|
|
||||||
public addMessage(err: ValidationError) {
|
public addMessage(err: ValidationError<V>): void {
|
||||||
if (!err.thingName) throw new Error('Null thing name');
|
if (!err.thingName) throw new Error('Null thing name');
|
||||||
this.errors.push(err);
|
this.errors.push(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addBag(otherBag: ValidationBag) {
|
public addBag(otherBag: ValidationBag<V>): void {
|
||||||
for (const error of otherBag.errors) {
|
for (const error of otherBag.errors) {
|
||||||
this.errors.push(error);
|
this.errors.push(error);
|
||||||
}
|
}
|
||||||
@ -257,10 +276,10 @@ export class ValidationBag extends Error {
|
|||||||
return this.errors.length > 0;
|
return this.errors.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getMessages(): { [p: string]: ValidationError } {
|
public getMessages(): { [p: string]: ValidationError<V> } {
|
||||||
const messages: { [p: string]: ValidationError } = {};
|
const messages: { [p: string]: ValidationError<V> } = {};
|
||||||
for (const err of this.errors) {
|
for (const err of this.errors) {
|
||||||
messages[err.thingName!] = {
|
messages[err.thingName || 'unknown'] = {
|
||||||
name: err.name,
|
name: err.name,
|
||||||
message: err.message,
|
message: err.message,
|
||||||
value: err.value,
|
value: err.value,
|
||||||
@ -269,26 +288,26 @@ export class ValidationBag extends Error {
|
|||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getErrors(): ValidationError[] {
|
public getErrors(): ValidationError<V>[] {
|
||||||
return this.errors;
|
return this.errors;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class ValidationError extends Error {
|
export abstract class ValidationError<V> extends Error {
|
||||||
public rawValueToHuman?: (val: any) => string;
|
public rawValueToHuman?: (val: V) => string;
|
||||||
public thingName?: string;
|
public thingName?: string;
|
||||||
public value?: any;
|
public value?: V;
|
||||||
|
|
||||||
public get name(): string {
|
public get name(): string {
|
||||||
return this.constructor.name;
|
return this.constructor.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BadLengthValidationError extends ValidationError {
|
export class BadLengthValidationError<V> extends ValidationError<V> {
|
||||||
private readonly expectedLength: number;
|
private readonly expectedLength: number;
|
||||||
private readonly maxLength?: number;
|
private readonly maxLength?: number;
|
||||||
|
|
||||||
constructor(expectedLength: number, maxLength?: number) {
|
public constructor(expectedLength: number, maxLength?: number) {
|
||||||
super();
|
super();
|
||||||
this.expectedLength = expectedLength;
|
this.expectedLength = expectedLength;
|
||||||
this.maxLength = maxLength;
|
this.maxLength = maxLength;
|
||||||
@ -296,14 +315,14 @@ export class BadLengthValidationError extends ValidationError {
|
|||||||
|
|
||||||
public get message(): string {
|
public get message(): string {
|
||||||
return `${this.thingName} expected length: ${this.expectedLength}${this.maxLength !== undefined ? ` to ${this.maxLength}` : ''}; ` +
|
return `${this.thingName} expected length: ${this.expectedLength}${this.maxLength !== undefined ? ` to ${this.maxLength}` : ''}; ` +
|
||||||
`actual length: ${this.value.length}.`;
|
`actual length: ${isLenghtable(this.value) && this.value.length}.`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TooShortError extends ValidationError {
|
export class TooShortError<V> extends ValidationError<V> {
|
||||||
private readonly minLength: number;
|
private readonly minLength: number;
|
||||||
|
|
||||||
constructor(minLength: number) {
|
public constructor(minLength: number) {
|
||||||
super();
|
super();
|
||||||
this.minLength = minLength;
|
this.minLength = minLength;
|
||||||
}
|
}
|
||||||
@ -313,10 +332,10 @@ export class TooShortError extends ValidationError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TooLongError extends ValidationError {
|
export class TooLongError<V> extends ValidationError<V> {
|
||||||
private readonly maxLength: number;
|
private readonly maxLength: number;
|
||||||
|
|
||||||
constructor(maxLength: number) {
|
public constructor(maxLength: number) {
|
||||||
super();
|
super();
|
||||||
this.maxLength = maxLength;
|
this.maxLength = maxLength;
|
||||||
}
|
}
|
||||||
@ -326,43 +345,43 @@ export class TooLongError extends ValidationError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BadValueValidationError extends ValidationError {
|
export class BadValueValidationError<V> extends ValidationError<V> {
|
||||||
private readonly expectedValue: any;
|
private readonly expectedValue: V;
|
||||||
|
|
||||||
constructor(expectedValue: any) {
|
public constructor(expectedValue: V) {
|
||||||
super();
|
super();
|
||||||
this.expectedValue = expectedValue;
|
this.expectedValue = expectedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get message(): string {
|
public get message(): string {
|
||||||
let expectedValue = this.expectedValue;
|
let expectedValue: string = String(this.expectedValue);
|
||||||
let actualValue = this.value;
|
let actualValue: string = String(this.value);
|
||||||
if (this.rawValueToHuman) {
|
if (this.rawValueToHuman && this.value) {
|
||||||
expectedValue = this.rawValueToHuman(expectedValue);
|
expectedValue = this.rawValueToHuman(this.expectedValue);
|
||||||
actualValue = this.rawValueToHuman(actualValue);
|
actualValue = this.rawValueToHuman(this.value);
|
||||||
}
|
}
|
||||||
return `Expected: ${expectedValue}; got: ${actualValue}.`
|
return `Expected: ${expectedValue}; got: ${actualValue}.`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DifferentThanError extends ValidationError {
|
export class DifferentThanError<V> extends ValidationError<V> {
|
||||||
private readonly otherName: any;
|
private readonly otherName?: string;
|
||||||
|
|
||||||
constructor(otherName: any) {
|
public constructor(otherName?: string) {
|
||||||
super();
|
super();
|
||||||
this.otherName = otherName;
|
this.otherName = otherName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get message(): string {
|
public get message(): string {
|
||||||
return `This should be the same as ${this.otherName}.`
|
return `This should be the same as ${this.otherName}.`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class OutOfRangeValidationError extends ValidationError {
|
export class OutOfRangeValidationError<V> extends ValidationError<V> {
|
||||||
private readonly min?: number;
|
private readonly min?: number;
|
||||||
private readonly max?: number;
|
private readonly max?: number;
|
||||||
|
|
||||||
constructor(min?: number, max?: number) {
|
public constructor(min?: number, max?: number) {
|
||||||
super();
|
super();
|
||||||
this.min = min;
|
this.min = min;
|
||||||
this.max = max;
|
this.max = max;
|
||||||
@ -374,32 +393,32 @@ export class OutOfRangeValidationError extends ValidationError {
|
|||||||
} else if (this.max === undefined) {
|
} else if (this.max === undefined) {
|
||||||
return `${this.thingName} must be at least ${this.min}`;
|
return `${this.thingName} must be at least ${this.min}`;
|
||||||
}
|
}
|
||||||
let min: any = this.min;
|
let min: string = String(this.min);
|
||||||
let max: any = this.max;
|
let max: string = String(this.max);
|
||||||
if (this.rawValueToHuman) {
|
if (this.rawValueToHuman) {
|
||||||
min = this.rawValueToHuman(min);
|
min = this.rawValueToHuman(this.min as unknown as V);
|
||||||
max = this.rawValueToHuman(max);
|
max = this.rawValueToHuman(this.max as unknown as V);
|
||||||
}
|
}
|
||||||
return `${this.thingName} must be between ${min} and ${max}.`;
|
return `${this.thingName} must be between ${min} and ${max}.`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InvalidFormatValidationError extends ValidationError {
|
export class InvalidFormatValidationError<V> extends ValidationError<V> {
|
||||||
public get message(): string {
|
public get message(): string {
|
||||||
return `"${this.value}" is not a valid ${this.thingName}.`;
|
return `"${this.value}" is not a valid ${this.thingName}.`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UndefinedValueValidationError extends ValidationError {
|
export class UndefinedValueValidationError<V> extends ValidationError<V> {
|
||||||
public get message(): string {
|
public get message(): string {
|
||||||
return `${this.thingName} is required.`;
|
return `${this.thingName} is required.`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AlreadyExistsValidationError extends ValidationError {
|
export class AlreadyExistsValidationError<V> extends ValidationError<V> {
|
||||||
private readonly table: string;
|
private readonly table: string;
|
||||||
|
|
||||||
constructor(table: string) {
|
public constructor(table: string) {
|
||||||
super();
|
super();
|
||||||
this.table = table;
|
this.table = table;
|
||||||
}
|
}
|
||||||
@ -409,11 +428,11 @@ export class AlreadyExistsValidationError extends ValidationError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UnknownRelationValidationError extends ValidationError {
|
export class UnknownRelationValidationError<V> extends ValidationError<V> {
|
||||||
private readonly table: string;
|
private readonly table: string;
|
||||||
private readonly foreignKey?: string;
|
private readonly foreignKey?: string;
|
||||||
|
|
||||||
constructor(table: string, foreignKey?: string) {
|
public constructor(table: string, foreignKey?: string) {
|
||||||
super();
|
super();
|
||||||
this.table = table;
|
this.table = table;
|
||||||
this.foreignKey = foreignKey;
|
this.foreignKey = foreignKey;
|
||||||
@ -424,15 +443,24 @@ export class UnknownRelationValidationError extends ValidationError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FileError extends ValidationError {
|
export class FileError<V> extends ValidationError<V> {
|
||||||
private readonly m: string;
|
private readonly _message: string;
|
||||||
|
|
||||||
constructor(message: string) {
|
public constructor(message: string) {
|
||||||
super();
|
super();
|
||||||
this.m = message;
|
this._message = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get message(): string {
|
public get message(): string {
|
||||||
return `${this.m}`;
|
return `${this._message}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Lengthable = {
|
||||||
|
length: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isLenghtable(value: unknown): value is Lengthable {
|
||||||
|
return value !== undefined && value !== null &&
|
||||||
|
typeof (value as Lengthable).length === 'number';
|
||||||
|
}
|
||||||
|
@ -39,9 +39,12 @@ export default class BackendController extends Controller {
|
|||||||
public routes(): void {
|
public routes(): void {
|
||||||
this.get('/', this.getIndex, 'backend', RequireAuthMiddleware, RequireAdminMiddleware);
|
this.get('/', this.getIndex, 'backend', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||||
if (User.isApprovalMode()) {
|
if (User.isApprovalMode()) {
|
||||||
this.get('/accounts-approval', this.getAccountApproval, 'accounts-approval', RequireAuthMiddleware, RequireAdminMiddleware);
|
this.get('/accounts-approval', this.getAccountApproval, 'accounts-approval',
|
||||||
this.post('/accounts-approval/approve', this.postApproveAccount, 'approve-account', RequireAuthMiddleware, RequireAdminMiddleware);
|
RequireAuthMiddleware, RequireAdminMiddleware);
|
||||||
this.post('/accounts-approval/reject', this.postRejectAccount, 'reject-account', RequireAuthMiddleware, RequireAdminMiddleware);
|
this.post('/accounts-approval/approve', this.postApproveAccount, 'approve-account',
|
||||||
|
RequireAuthMiddleware, RequireAdminMiddleware);
|
||||||
|
this.post('/accounts-approval/reject', this.postRejectAccount, 'reject-account',
|
||||||
|
RequireAuthMiddleware, RequireAdminMiddleware);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,11 +74,11 @@ export default class BackendController extends Controller {
|
|||||||
account.as(UserApprovedComponent).approved = true;
|
account.as(UserApprovedComponent).approved = true;
|
||||||
await account.save();
|
await account.save();
|
||||||
|
|
||||||
if (email) {
|
if (email && email.email) {
|
||||||
await new Mail(ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE, {
|
await new Mail(ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE, {
|
||||||
approved: true,
|
approved: true,
|
||||||
link: config.get<string>('base_url') + Controller.route('auth'),
|
link: config.get<string>('base_url') + Controller.route('auth'),
|
||||||
}).send(email.email!);
|
}).send(email.email);
|
||||||
}
|
}
|
||||||
|
|
||||||
req.flash('success', `Account successfully approved.`);
|
req.flash('success', `Account successfully approved.`);
|
||||||
@ -87,10 +90,10 @@ export default class BackendController extends Controller {
|
|||||||
|
|
||||||
await account.delete();
|
await account.delete();
|
||||||
|
|
||||||
if (email) {
|
if (email && email.email) {
|
||||||
await new Mail(ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE, {
|
await new Mail(ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE, {
|
||||||
approved: false,
|
approved: false,
|
||||||
}).send(email.email!);
|
}).send(email.email);
|
||||||
}
|
}
|
||||||
|
|
||||||
req.flash('success', `Account successfully deleted.`);
|
req.flash('success', `Account successfully deleted.`);
|
||||||
|
@ -29,4 +29,4 @@ export default class CreateLogsTable extends Migration {
|
|||||||
public registerModels(): void {
|
public registerModels(): void {
|
||||||
ModelFactory.register(Log);
|
ModelFactory.register(Log);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,4 @@ export default class CreateMigrationsTable extends Migration {
|
|||||||
public async rollback(connection: Connection): Promise<void> {
|
public async rollback(connection: Connection): Promise<void> {
|
||||||
await this.query('DROP TABLE migrations', connection);
|
await this.query('DROP TABLE migrations', connection);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
public registerModels(): void {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import Model from "../db/Model";
|
import Model from "../db/Model";
|
||||||
import {LogLevel, LogLevelKeys} from "../Logger";
|
import {LogLevel, LogLevelKeys} from "../Logger";
|
||||||
import {bufferToUUID} from "../Utils";
|
import {bufferToUuid} from "../Utils";
|
||||||
|
|
||||||
export default class Log extends Model {
|
export default class Log extends Model {
|
||||||
public readonly id?: number = undefined;
|
public readonly id?: number = undefined;
|
||||||
@ -26,15 +26,15 @@ export default class Log extends Model {
|
|||||||
return <LogLevelKeys>LogLevel[this.level];
|
return <LogLevelKeys>LogLevel[this.level];
|
||||||
}
|
}
|
||||||
|
|
||||||
public setLevel(level: LogLevelKeys) {
|
public setLevel(level: LogLevel): void {
|
||||||
this.level = LogLevel[level];
|
this.level = level;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getLogID(): string | null {
|
public getLogId(): string | null {
|
||||||
return this.log_id ? bufferToUUID(this.log_id!) : null;
|
return this.log_id ? bufferToUuid(this.log_id) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setLogID(buffer: Buffer) {
|
public setLogId(buffer: Buffer): void {
|
||||||
this.log_id = buffer;
|
this.log_id = buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ export default class Log extends Model {
|
|||||||
return this.error_stack || '';
|
return this.error_stack || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public setError(error?: Error) {
|
public setError(error?: Error): void {
|
||||||
if (!error) return;
|
if (!error) return;
|
||||||
|
|
||||||
this.error_name = error.name;
|
this.error_name = error.name;
|
||||||
|
14
src/types/Express.d.ts
vendored
14
src/types/Express.d.ts
vendored
@ -1,10 +1,14 @@
|
|||||||
import {Files} from "formidable";
|
import {Files} from "formidable";
|
||||||
import {Type} from "../Utils";
|
import {Type} from "../Utils";
|
||||||
import Middleware from "../Middleware";
|
import Middleware from "../Middleware";
|
||||||
|
import {FlashMessages} from "../components/SessionComponent";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace Express {
|
namespace Express {
|
||||||
export interface Request {
|
export interface Request {
|
||||||
|
getSession(): Session;
|
||||||
|
|
||||||
|
|
||||||
files: Files;
|
files: Files;
|
||||||
|
|
||||||
|
|
||||||
@ -13,15 +17,15 @@ declare global {
|
|||||||
as<M extends Middleware>(type: Type<M>): M;
|
as<M extends Middleware>(type: Type<M>): M;
|
||||||
|
|
||||||
|
|
||||||
flash(): { [key: string]: string[] };
|
flash(): FlashMessages;
|
||||||
|
|
||||||
flash(message: string): any;
|
flash(message: string): unknown[];
|
||||||
|
|
||||||
flash(event: string, message: any): any;
|
flash(event: string, message: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Response {
|
export interface Response {
|
||||||
redirectBack(defaultUrl?: string): any;
|
redirectBack(defaultUrl?: string): void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,12 +8,12 @@ useApp((addr, port) => {
|
|||||||
return app = new class extends TestApp {
|
return app = new class extends TestApp {
|
||||||
protected async init(): Promise<void> {
|
protected async init(): Promise<void> {
|
||||||
this.use(new class extends Controller {
|
this.use(new class extends Controller {
|
||||||
routes(): void {
|
public routes(): void {
|
||||||
this.get('/', (req, res, next) => {
|
this.get('/', (req, res) => {
|
||||||
res.render('test/csrf.njk');
|
res.render('test/csrf.njk');
|
||||||
}, 'csrf_test');
|
}, 'csrf_test');
|
||||||
|
|
||||||
this.post('/', (req, res, next) => {
|
this.post('/', (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
});
|
});
|
||||||
@ -97,4 +97,4 @@ describe('Test CSRF protection', () => {
|
|||||||
.send({csrf: csrf})
|
.send({csrf: csrf})
|
||||||
.expect(200, done);
|
.expect(200, done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -43,9 +43,6 @@ class Author extends Model {
|
|||||||
localPivotKey: 'author_id',
|
localPivotKey: 'author_id',
|
||||||
foreignPivotKey: 'role_id',
|
foreignPivotKey: 'role_id',
|
||||||
});
|
});
|
||||||
|
|
||||||
protected init(): void {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Role extends Model {
|
class Role extends Model {
|
||||||
@ -59,17 +56,11 @@ class Role extends Model {
|
|||||||
localPivotKey: 'role_id',
|
localPivotKey: 'role_id',
|
||||||
foreignPivotKey: 'permission_id',
|
foreignPivotKey: 'permission_id',
|
||||||
});
|
});
|
||||||
|
|
||||||
protected init(): void {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Permission extends Model {
|
class Permission extends Model {
|
||||||
public id?: number = undefined;
|
public id?: number = undefined;
|
||||||
public name?: string = undefined;
|
public name?: string = undefined;
|
||||||
|
|
||||||
protected init(): void {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthorRole extends Model {
|
class AuthorRole extends Model {
|
||||||
@ -133,12 +124,12 @@ beforeAll(async () => {
|
|||||||
postFactory,
|
postFactory,
|
||||||
authorFactory,
|
authorFactory,
|
||||||
roleFactory,
|
roleFactory,
|
||||||
permissionFactory
|
permissionFactory,
|
||||||
]) {
|
]) {
|
||||||
await MysqlConnectionManager.query(`DROP TABLE IF EXISTS ${(factory.table)}`);
|
await MysqlConnectionManager.query(`DROP TABLE IF EXISTS ${factory.table}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await MysqlConnectionManager.query(`CREATE TABLE ${(fakeDummyModelModelFactory.table)}(
|
await MysqlConnectionManager.query(`CREATE TABLE ${fakeDummyModelModelFactory.table}(
|
||||||
id INT NOT NULL AUTO_INCREMENT,
|
id INT NOT NULL AUTO_INCREMENT,
|
||||||
name VARCHAR(256),
|
name VARCHAR(256),
|
||||||
date DATETIME,
|
date DATETIME,
|
||||||
@ -146,24 +137,24 @@ beforeAll(async () => {
|
|||||||
PRIMARY KEY(id)
|
PRIMARY KEY(id)
|
||||||
)`);
|
)`);
|
||||||
|
|
||||||
await MysqlConnectionManager.query(`CREATE TABLE ${(authorFactory.table)}(
|
await MysqlConnectionManager.query(`CREATE TABLE ${authorFactory.table}(
|
||||||
id INT NOT NULL AUTO_INCREMENT,
|
id INT NOT NULL AUTO_INCREMENT,
|
||||||
name VARCHAR(64),
|
name VARCHAR(64),
|
||||||
PRIMARY KEY(id)
|
PRIMARY KEY(id)
|
||||||
)`);
|
)`);
|
||||||
await MysqlConnectionManager.query(`CREATE TABLE ${(postFactory.table)}(
|
await MysqlConnectionManager.query(`CREATE TABLE ${postFactory.table}(
|
||||||
id INT NOT NULL AUTO_INCREMENT,
|
id INT NOT NULL AUTO_INCREMENT,
|
||||||
author_id INT NOT NULL,
|
author_id INT NOT NULL,
|
||||||
content VARCHAR(512),
|
content VARCHAR(512),
|
||||||
PRIMARY KEY(id),
|
PRIMARY KEY(id),
|
||||||
FOREIGN KEY post_author_fk (author_id) REFERENCES ${(authorFactory.table)} (id)
|
FOREIGN KEY post_author_fk (author_id) REFERENCES ${authorFactory.table} (id)
|
||||||
)`);
|
)`);
|
||||||
await MysqlConnectionManager.query(`CREATE TABLE ${(roleFactory.table)}(
|
await MysqlConnectionManager.query(`CREATE TABLE ${roleFactory.table}(
|
||||||
id INT NOT NULL AUTO_INCREMENT,
|
id INT NOT NULL AUTO_INCREMENT,
|
||||||
name VARCHAR(64),
|
name VARCHAR(64),
|
||||||
PRIMARY KEY(id)
|
PRIMARY KEY(id)
|
||||||
)`);
|
)`);
|
||||||
await MysqlConnectionManager.query(`CREATE TABLE ${(permissionFactory.table)}(
|
await MysqlConnectionManager.query(`CREATE TABLE ${permissionFactory.table}(
|
||||||
id INT NOT NULL AUTO_INCREMENT,
|
id INT NOT NULL AUTO_INCREMENT,
|
||||||
name VARCHAR(64),
|
name VARCHAR(64),
|
||||||
PRIMARY KEY(id)
|
PRIMARY KEY(id)
|
||||||
@ -174,16 +165,16 @@ beforeAll(async () => {
|
|||||||
author_id INT NOT NULL,
|
author_id INT NOT NULL,
|
||||||
role_id INT NOT NULL,
|
role_id INT NOT NULL,
|
||||||
PRIMARY KEY(id),
|
PRIMARY KEY(id),
|
||||||
FOREIGN KEY author_role_author_fk (author_id) REFERENCES ${(authorFactory.table)} (id),
|
FOREIGN KEY author_role_author_fk (author_id) REFERENCES ${authorFactory.table} (id),
|
||||||
FOREIGN KEY author_role_role_fk (role_id) REFERENCES ${(roleFactory.table)} (id)
|
FOREIGN KEY author_role_role_fk (role_id) REFERENCES ${roleFactory.table} (id)
|
||||||
)`);
|
)`);
|
||||||
await MysqlConnectionManager.query(`CREATE TABLE role_permission(
|
await MysqlConnectionManager.query(`CREATE TABLE role_permission(
|
||||||
id INT NOT NULL AUTO_INCREMENT,
|
id INT NOT NULL AUTO_INCREMENT,
|
||||||
role_id INT NOT NULL,
|
role_id INT NOT NULL,
|
||||||
permission_id INT NOT NULL,
|
permission_id INT NOT NULL,
|
||||||
PRIMARY KEY(id),
|
PRIMARY KEY(id),
|
||||||
FOREIGN KEY role_permission_role_fk (role_id) REFERENCES ${(roleFactory.table)} (id),
|
FOREIGN KEY role_permission_role_fk (role_id) REFERENCES ${roleFactory.table} (id),
|
||||||
FOREIGN KEY role_permission_permission_fk (permission_id) REFERENCES ${(permissionFactory.table)} (id)
|
FOREIGN KEY role_permission_permission_fk (permission_id) REFERENCES ${permissionFactory.table} (id)
|
||||||
)`);
|
)`);
|
||||||
|
|
||||||
|
|
||||||
@ -289,10 +280,10 @@ describe('Model', () => {
|
|||||||
// Check that row exists in DB
|
// Check that row exists in DB
|
||||||
const retrievedInstance = await FakeDummyModel.getById(1);
|
const retrievedInstance = await FakeDummyModel.getById(1);
|
||||||
expect(retrievedInstance).toBeDefined();
|
expect(retrievedInstance).toBeDefined();
|
||||||
expect(retrievedInstance!.id).toBe(1);
|
expect(retrievedInstance?.id).toBe(1);
|
||||||
expect(retrievedInstance!.name).toBe('name1');
|
expect(retrievedInstance?.name).toBe('name1');
|
||||||
expect(retrievedInstance!.date?.getTime()).toBeCloseTo(date.getTime(), -4);
|
expect(retrievedInstance?.date?.getTime()).toBeCloseTo(date.getTime(), -4);
|
||||||
expect(retrievedInstance!.date_default).toBeDefined();
|
expect(retrievedInstance?.date_default).toBeDefined();
|
||||||
|
|
||||||
const failingInsertModel = fakeDummyModelModelFactory.create({
|
const failingInsertModel = fakeDummyModelModelFactory.create({
|
||||||
name: 'a',
|
name: 'a',
|
||||||
@ -308,17 +299,19 @@ describe('Model', () => {
|
|||||||
|
|
||||||
const preUpdatedModel = await FakeDummyModel.getById(insertModel.id);
|
const preUpdatedModel = await FakeDummyModel.getById(insertModel.id);
|
||||||
expect(preUpdatedModel).not.toBeNull();
|
expect(preUpdatedModel).not.toBeNull();
|
||||||
expect(preUpdatedModel!.name).toBe(insertModel.name);
|
expect(preUpdatedModel?.name).toBe(insertModel.name);
|
||||||
|
|
||||||
// Update model
|
// Update model
|
||||||
preUpdatedModel!.name = 'updated_name';
|
if (preUpdatedModel) {
|
||||||
await preUpdatedModel!.save();
|
preUpdatedModel.name = 'updated_name';
|
||||||
|
await preUpdatedModel.save();
|
||||||
|
}
|
||||||
|
|
||||||
const postUpdatedModel = await FakeDummyModel.getById(insertModel.id);
|
const postUpdatedModel = await FakeDummyModel.getById(insertModel.id);
|
||||||
expect(postUpdatedModel).not.toBeNull();
|
expect(postUpdatedModel).not.toBeNull();
|
||||||
expect(postUpdatedModel!.id).toBe(insertModel.id);
|
expect(postUpdatedModel?.id).toBe(insertModel.id);
|
||||||
expect(postUpdatedModel!.name).not.toBe(insertModel.name);
|
expect(postUpdatedModel?.name).not.toBe(insertModel.name);
|
||||||
expect(postUpdatedModel!.name).toBe(preUpdatedModel!.name);
|
expect(postUpdatedModel?.name).toBe(preUpdatedModel?.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete properly', async () => {
|
it('should delete properly', async () => {
|
||||||
@ -330,7 +323,7 @@ describe('Model', () => {
|
|||||||
const preDeleteModel = await FakeDummyModel.getById(insertModel.id);
|
const preDeleteModel = await FakeDummyModel.getById(insertModel.id);
|
||||||
expect(preDeleteModel).not.toBeNull();
|
expect(preDeleteModel).not.toBeNull();
|
||||||
|
|
||||||
await preDeleteModel!.delete();
|
await preDeleteModel?.delete();
|
||||||
|
|
||||||
const postDeleteModel = await FakeDummyModel.getById(insertModel.id);
|
const postDeleteModel = await FakeDummyModel.getById(insertModel.id);
|
||||||
expect(postDeleteModel).toBeNull();
|
expect(postDeleteModel).toBeNull();
|
||||||
@ -359,21 +352,28 @@ describe('ModelRelation', () => {
|
|||||||
|
|
||||||
expect(posts.length).toBe(3);
|
expect(posts.length).toBe(3);
|
||||||
|
|
||||||
async function testPost(post: Post, originalPost: Post, expectedAuthor: Author, expectedRoles: Role[], expectedPermissions: Permission[]) {
|
async function testPost(
|
||||||
console.log('Testing post', post)
|
post: Post,
|
||||||
|
originalPost: Post,
|
||||||
|
expectedAuthor: Author,
|
||||||
|
expectedRoles: Role[],
|
||||||
|
expectedPermissions: Permission[],
|
||||||
|
) {
|
||||||
|
console.log('Testing post', post);
|
||||||
expect(post.id).toBe(originalPost.id);
|
expect(post.id).toBe(originalPost.id);
|
||||||
expect(post.content).toBe(originalPost.content);
|
expect(post.content).toBe(originalPost.content);
|
||||||
|
|
||||||
const actualAuthor = await post.author.get();
|
const actualAuthor = await post.author.get();
|
||||||
expect(actualAuthor).not.toBeNull()
|
expect(actualAuthor).not.toBeNull();
|
||||||
expect(await post.author.has(expectedAuthor)).toBeTruthy();
|
expect(await post.author.has(expectedAuthor)).toBeTruthy();
|
||||||
expect(actualAuthor!.equals(expectedAuthor)).toBe(true);
|
expect(actualAuthor?.equals(expectedAuthor)).toBe(true);
|
||||||
|
|
||||||
const authorRoles = await actualAuthor!.roles.get();
|
const authorRoles = await actualAuthor?.roles.get() || [];
|
||||||
console.log('Roles:');
|
console.log('Roles:');
|
||||||
expect(authorRoles.map(r => r.id)).toStrictEqual(expectedRoles.map(r => r.id));
|
expect(authorRoles.map(r => r.id)).toStrictEqual(expectedRoles.map(r => r.id));
|
||||||
|
|
||||||
const authorPermissions = (await Promise.all(authorRoles.map(async r => await r.permissions.get()))).flatMap(p => p);
|
const authorPermissions = (await Promise.all(authorRoles.map(async r => await r.permissions.get())))
|
||||||
|
.flatMap(p => p);
|
||||||
console.log('Permissions:');
|
console.log('Permissions:');
|
||||||
expect(authorPermissions.map(p => p.id)).toStrictEqual(expectedPermissions.map(p => p.id));
|
expect(authorPermissions.map(p => p.id)).toStrictEqual(expectedPermissions.map(p => p.id));
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,11 @@ describe('Test ModelQuery', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('function select', () => {
|
test('function select', () => {
|
||||||
const query = ModelQuery.select({table: 'model'} as unknown as ModelFactory<Model>, 'f1', new SelectFieldValue('_count', 'COUNT(*)', true));
|
const query = ModelQuery.select(
|
||||||
|
{table: 'model'} as unknown as ModelFactory<Model>,
|
||||||
|
'f1',
|
||||||
|
new SelectFieldValue('_count', 'COUNT(*)', true),
|
||||||
|
);
|
||||||
expect(query.toString(true)).toBe('SELECT `model`.`f1`,(COUNT(*)) AS `_count` FROM `model`');
|
expect(query.toString(true)).toBe('SELECT `model`.`f1`,(COUNT(*)) AS `_count` FROM `model`');
|
||||||
expect(query.variables).toStrictEqual([]);
|
expect(query.variables).toStrictEqual([]);
|
||||||
});
|
});
|
||||||
@ -81,6 +85,6 @@ describe('Test ModelQuery', () => {
|
|||||||
query.union(query2, 'model.f1', 'DESC', false, 8);
|
query.union(query2, 'model.f1', 'DESC', false, 8);
|
||||||
|
|
||||||
expect(query.toString(true)).toBe("(SELECT `model`.* FROM `model`) UNION (SELECT `model2`.* FROM `model2` WHERE `f2`=?) ORDER BY `model`.`f1` DESC LIMIT 8");
|
expect(query.toString(true)).toBe("(SELECT `model`.* FROM `model`) UNION (SELECT `model2`.* FROM `model2` WHERE `f2`=?) ORDER BY `model`.`f1` DESC LIMIT 8");
|
||||||
expect(query.variables).toStrictEqual(['v2'])
|
expect(query.variables).toStrictEqual(['v2']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
47
test/_app.ts
47
test/_app.ts
@ -1,7 +1,6 @@
|
|||||||
import {setupMailServer, teardownMailServer} from "./_mail_server";
|
import {setupMailServer, teardownMailServer} from "./_mail_server";
|
||||||
import Application from "../src/Application";
|
import Application from "../src/Application";
|
||||||
import Migration from "../src/db/Migration";
|
import Migration, {MigrationType} from "../src/db/Migration";
|
||||||
import {Type} from "../src/Utils";
|
|
||||||
import {MIGRATIONS} from "./_migrations";
|
import {MIGRATIONS} from "./_migrations";
|
||||||
import ExpressAppComponent from "../src/components/ExpressAppComponent";
|
import ExpressAppComponent from "../src/components/ExpressAppComponent";
|
||||||
import RedisComponent from "../src/components/RedisComponent";
|
import RedisComponent from "../src/components/RedisComponent";
|
||||||
@ -17,8 +16,9 @@ import FormHelperComponent from "../src/components/FormHelperComponent";
|
|||||||
import RedirectBackComponent from "../src/components/RedirectBackComponent";
|
import RedirectBackComponent from "../src/components/RedirectBackComponent";
|
||||||
import ServeStaticDirectoryComponent from "../src/components/ServeStaticDirectoryComponent";
|
import ServeStaticDirectoryComponent from "../src/components/ServeStaticDirectoryComponent";
|
||||||
import {Express} from "express";
|
import {Express} from "express";
|
||||||
|
import packageJson = require('../package.json');
|
||||||
|
|
||||||
export default function useApp(appSupplier?: (addr: string, port: number) => TestApp) {
|
export default function useApp(appSupplier?: (addr: string, port: number) => TestApp): void {
|
||||||
let app: Application;
|
let app: Application;
|
||||||
|
|
||||||
beforeAll(async (done) => {
|
beforeAll(async (done) => {
|
||||||
@ -30,8 +30,21 @@ export default function useApp(appSupplier?: (addr: string, port: number) => Tes
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async (done) => {
|
afterAll(async (done) => {
|
||||||
await app.stop();
|
const errors = [];
|
||||||
await teardownMailServer();
|
|
||||||
|
try {
|
||||||
|
await app.stop();
|
||||||
|
} catch (e) {
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await teardownMailServer();
|
||||||
|
} catch (e) {
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) throw errors;
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -41,23 +54,23 @@ export class TestApp extends Application {
|
|||||||
private readonly port: number;
|
private readonly port: number;
|
||||||
private expressAppComponent?: ExpressAppComponent;
|
private expressAppComponent?: ExpressAppComponent;
|
||||||
|
|
||||||
constructor(addr: string, port: number) {
|
public constructor(addr: string, port: number) {
|
||||||
super(require('../package.json').version, true);
|
super(packageJson.version, true);
|
||||||
this.addr = addr;
|
this.addr = addr;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getMigrations(): Type<Migration>[] {
|
protected getMigrations(): MigrationType<Migration>[] {
|
||||||
return MIGRATIONS;
|
return MIGRATIONS;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async init(): Promise<void> {
|
protected async init(): Promise<void> {
|
||||||
this.registerComponents();
|
this.registerComponents();
|
||||||
this.registerWebSocketListeners();
|
this.registerWebSocketListeners?.();
|
||||||
this.registerControllers();
|
this.registerControllers?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected registerComponents() {
|
protected registerComponents(): void {
|
||||||
this.expressAppComponent = new ExpressAppComponent(this.addr, this.port);
|
this.expressAppComponent = new ExpressAppComponent(this.addr, this.port);
|
||||||
const redisComponent = new RedisComponent();
|
const redisComponent = new RedisComponent();
|
||||||
const mysqlComponent = new MysqlComponent();
|
const mysqlComponent = new MysqlComponent();
|
||||||
@ -84,7 +97,7 @@ export class TestApp extends Application {
|
|||||||
// Auth
|
// Auth
|
||||||
this.use(new AuthComponent(new class extends AuthGuard<MagicLink> {
|
this.use(new AuthComponent(new class extends AuthGuard<MagicLink> {
|
||||||
public async getProofForSession(session: Express.Session): Promise<MagicLink | null> {
|
public async getProofForSession(session: Express.Session): Promise<MagicLink | null> {
|
||||||
return await MagicLink.bySessionID(session.id, ['login', 'register']);
|
return await MagicLink.bySessionId(session.id, ['login', 'register']);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -92,15 +105,11 @@ export class TestApp extends Application {
|
|||||||
this.use(new FormHelperComponent());
|
this.use(new FormHelperComponent());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected registerWebSocketListeners() {
|
protected registerWebSocketListeners?(): void;
|
||||||
}
|
|
||||||
|
|
||||||
protected registerControllers() {
|
protected registerControllers?(): void;
|
||||||
}
|
|
||||||
|
|
||||||
public getExpressApp(): Express {
|
public getExpressApp(): Express {
|
||||||
return this.expressAppComponent!.getExpressApp();
|
return this.as(ExpressAppComponent).getExpressApp();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_ADDR = 'http://localhost:8966';
|
|
@ -1,19 +1,19 @@
|
|||||||
const MailDev = require("maildev");
|
import MailDev from "maildev";
|
||||||
|
|
||||||
export const MAIL_SERVER = new MailDev({
|
export const MAIL_SERVER = new MailDev({
|
||||||
ip: 'localhost',
|
ip: 'localhost',
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function setupMailServer() {
|
export async function setupMailServer(): Promise<void> {
|
||||||
await new Promise((resolve, reject) => MAIL_SERVER.listen((err: Error) => {
|
await new Promise((resolve, reject) => MAIL_SERVER.listen((err?: Error) => {
|
||||||
if (err) reject(err);
|
if (err) reject(err);
|
||||||
else resolve();
|
else resolve();
|
||||||
}));
|
}));
|
||||||
};
|
}
|
||||||
|
|
||||||
export async function teardownMailServer() {
|
export async function teardownMailServer(): Promise<void> {
|
||||||
await new Promise((resolve, reject) => MAIL_SERVER.close((err: Error) => {
|
await new Promise((resolve, reject) => MAIL_SERVER.close((err?: Error) => {
|
||||||
if (err) reject(err);
|
if (err) reject(err);
|
||||||
else resolve();
|
else resolve();
|
||||||
}));
|
}));
|
||||||
};
|
}
|
||||||
|
@ -12,4 +12,4 @@ export const MIGRATIONS = [
|
|||||||
FixUserMainEmailRelation,
|
FixUserMainEmailRelation,
|
||||||
DropNameFromUsers,
|
DropNameFromUsers,
|
||||||
CreateMagicLinksTable,
|
CreateMagicLinksTable,
|
||||||
];
|
];
|
||||||
|
216
test/types/maildev.d.ts
vendored
Normal file
216
test/types/maildev.d.ts
vendored
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
// Type definitions for maildev 1.0.0-rc3
|
||||||
|
// Project: https://github.com/djfarrelly/maildev
|
||||||
|
// Definitions by: Cyril Schumacher <https://github.com/cyrilschumacher>
|
||||||
|
// Zak Barbuto <https://github.com/zbarbuto>
|
||||||
|
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||||
|
|
||||||
|
/// <reference types="node"/>
|
||||||
|
|
||||||
|
declare module 'maildev' {
|
||||||
|
import fs = require("fs");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for {@link MailDev}.
|
||||||
|
*/
|
||||||
|
export default class MailDev {
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
* @param {MailDevOptions} options The options.
|
||||||
|
*/
|
||||||
|
public constructor(options: MailDevOptions);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a given email by identifier.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
* @param {string} id The email identifier.
|
||||||
|
* @param {Function} callback The error callback.
|
||||||
|
*/
|
||||||
|
public deleteEmail(id: string, callback?: (error: Error) => void): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all email and their attachments.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
* @param {Function} callback The error callback.
|
||||||
|
*/
|
||||||
|
public deleteAllEmail(callback?: (error: Error) => void): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the SMTP server.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
* @param {Function} callback The error callback.
|
||||||
|
*/
|
||||||
|
public close(callback?: (error: Error) => void): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accepts e-mail identifier, returns email object.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
* @param {string} id The e-mail identifier.
|
||||||
|
* @param {Function} callback The error callback.
|
||||||
|
*/
|
||||||
|
public getEmail(id: string, callback?: (error: Error) => void): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a readable stream of the raw e-mail.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
* @param {string} id The e-mail identifier.
|
||||||
|
*/
|
||||||
|
public getRawEmail(id: string, callback?: (error: Error, readStream: fs.ReadStream) => void): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns array of all e-mail.
|
||||||
|
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public getAllEmail(done: (error: Error, emails: Array<Record<string, unknown>>) => void): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the SMTP server.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
* @param {Function} callback The error callback.
|
||||||
|
*/
|
||||||
|
public listen(callback?: (error: Error) => void): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event called when a new e-mail is received. Callback receives single mail object.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
* @param {string} eventName The event name.
|
||||||
|
* @param {Function} email The email.
|
||||||
|
*/
|
||||||
|
public on(eventName: string, callback: (email: Record<string, unknown>) => void): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relay the e-mail.
|
||||||
|
*
|
||||||
|
* @param {string} idOrMailObject The identifier or mail object.
|
||||||
|
* @param {Function} done The callback.
|
||||||
|
*/
|
||||||
|
public relayMail(idOrMailObject: string, done: (error: Error) => void): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for {@link MailDev} options.
|
||||||
|
*/
|
||||||
|
export interface MailDevOptions {
|
||||||
|
/**
|
||||||
|
* IP Address to bind SMTP service to', '0.0.0.0'
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
ip?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMTP host for outgoing emails
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
outgoingHost?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMTP password for outgoing emails
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
outgoingPass?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMTP port for outgoing emails.
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
outgoingPort?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMTP user for outgoing emails
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
outgoingUser?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use SMTP SSL for outgoing emails
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
outgoingSecure?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMTP port to catch emails.
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
smtp?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Port to use for web UI
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
web?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IP Address to bind HTTP service to
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
webIp?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do not start web UI
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
disableWeb?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do not output console.log messages
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
silent?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP user for GUI
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
webUser?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP password for GUI
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
webPass?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the Web GUI after startup
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
open?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for mail.
|
||||||
|
*/
|
||||||
|
export interface Mail {
|
||||||
|
/**
|
||||||
|
* Identifier.
|
||||||
|
*/
|
||||||
|
id?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client.
|
||||||
|
*/
|
||||||
|
envelope?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1 @@
|
|||||||
{{ getCSRFToken() }}
|
{{ getCsrfToken() }}
|
@ -6,8 +6,6 @@
|
|||||||
"stripInternal": true,
|
"stripInternal": true,
|
||||||
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
// "noUnusedLocals": true,
|
|
||||||
// "noUnusedParameters": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
|
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "Node",
|
||||||
|
14
tsconfig.test.json
Normal file
14
tsconfig.test.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"typeRoots": [
|
||||||
|
"node_modules/@types",
|
||||||
|
"src/types",
|
||||||
|
"test/types"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/types/**/*",
|
||||||
|
"test/**/*"
|
||||||
|
]
|
||||||
|
}
|
3132
tsconfig.tsbuildinfo
3132
tsconfig.tsbuildinfo
File diff suppressed because it is too large
Load Diff
@ -29,18 +29,18 @@
|
|||||||
<a href="/auth" class="button transparent">Go back</a>
|
<a href="/auth" class="button transparent">Go back</a>
|
||||||
<button type="submit" class="primary">Register</button>
|
<button type="submit" class="primary">Register</button>
|
||||||
|
|
||||||
{{ macros.csrf(getCSRFToken) }}
|
{{ macros.csrf(getCsrfToken) }}
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<form action="{{ action }}" method="POST" id="login-form">
|
<form action="{{ action }}" method="POST" id="login-form">
|
||||||
<h2>Log in or register</h2>
|
<h2>Log in or register</h2>
|
||||||
{# {{ macros.message('info', 'If we don\'t find your email address in our database, you will be able to register.', false, true) }}#}
|
{# {{ macros.message('info', 'If we don\'t find your email address in our database, you will be able to register.', false, true) }} #}
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
{{ macros.field(_locals, 'email', 'email', query.email or '', 'Your email address', "If we don't find your email address in our database, you will be able to register.", 'required') }}
|
{{ macros.field(_locals, 'email', 'email', query.email or '', 'Your email address', "If we don't find your email address in our database, you will be able to register.", 'required') }}
|
||||||
</div>
|
</div>
|
||||||
<button type="submit">Authenticate</button>
|
<button type="submit">Authenticate</button>
|
||||||
|
|
||||||
{{ macros.csrf(getCSRFToken) }}
|
{{ macros.csrf(getCsrfToken) }}
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -28,14 +28,14 @@
|
|||||||
<form action="{{ route('approve-account') }}" method="POST">
|
<form action="{{ route('approve-account') }}" method="POST">
|
||||||
<input type="hidden" name="user_id" value="{{ user.id }}">
|
<input type="hidden" name="user_id" value="{{ user.id }}">
|
||||||
<button class="success"><i data-feather="check"></i> Approve</button>
|
<button class="success"><i data-feather="check"></i> Approve</button>
|
||||||
{{ macros.csrf(getCSRFToken) }}
|
{{ macros.csrf(getCsrfToken) }}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form action="{{ route('reject-account') }}" method="POST"
|
<form action="{{ route('reject-account') }}" method="POST"
|
||||||
data-confirm="This will irrevocably delete the {{ user.mainEmail.getOrFail().email | default(user.name | default(user.id)) }} account.">
|
data-confirm="This will irrevocably delete the {{ user.mainEmail.getOrFail().email | default(user.name | default(user.id)) }} account.">
|
||||||
<input type="hidden" name="user_id" value="{{ user.id }}">
|
<input type="hidden" name="user_id" value="{{ user.id }}">
|
||||||
<button class="danger"><i data-feather="check"></i> Reject</button>
|
<button class="danger"><i data-feather="check"></i> Reject</button>
|
||||||
{{ macros.csrf(getCSRFToken) }}
|
{{ macros.csrf(getCsrfToken) }}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -28,8 +28,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro csrf(getCSRFToken) %}
|
{% macro csrf(getCsrfToken) %}
|
||||||
<input type="hidden" name="csrf" value="{{ getCSRFToken() }}">
|
<input type="hidden" name="csrf" value="{{ getCsrfToken() }}">
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro field(_locals, type, name, value, placeholder, hint, validation_attributes='', extraData='', icon=null) %}
|
{% macro field(_locals, type, name, value, placeholder, hint, validation_attributes='', extraData='', icon=null) %}
|
||||||
|
Loading…
Reference in New Issue
Block a user