Upgrade dependencies and update to express session new typings

This commit is contained in:
Alice Gaudon 2020-12-04 14:42:09 +01:00
parent 7be3e00c46
commit 87b4facea0
21 changed files with 387 additions and 324 deletions

View File

@ -23,9 +23,9 @@
}, },
"devDependencies": { "devDependencies": {
"@types/compression": "^1.7.0", "@types/compression": "^1.7.0",
"@types/config": "^0.0.36", "@types/config": "^0.0.37",
"@types/connect-flash": "^0.0.35", "@types/connect-flash": "^0.0.36",
"@types/connect-redis": "^0.0.14", "@types/connect-redis": "^0.0.15",
"@types/cookie": "^0.4.0", "@types/cookie": "^0.4.0",
"@types/cookie-parser": "^1.4.2", "@types/cookie-parser": "^1.4.2",
"@types/express": "^4.17.6", "@types/express": "^4.17.6",
@ -74,7 +74,7 @@
"on-finished": "^2.3.0", "on-finished": "^2.3.0",
"redis": "^3.0.2", "redis": "^3.0.2",
"ts-node": "^9.0.0", "ts-node": "^9.0.0",
"tslog": "^2.10.0", "tslog": "^3.0.1",
"uuid": "^8.0.0", "uuid": "^8.0.0",
"ws": "^7.2.3" "ws": "^7.2.3"
} }

View File

@ -36,7 +36,7 @@ export default abstract class Application implements Extendable<ApplicationCompo
protected abstract getMigrations(): MigrationType<Migration>[]; protected abstract getMigrations(): MigrationType<Migration>[];
protected abstract async init(): Promise<void>; protected abstract init(): Promise<void>;
protected use(thing: Controller | WebSocketListener<this> | ApplicationComponent): void { protected use(thing: Controller | WebSocketListener<this> | ApplicationComponent): void {
if (thing instanceof Controller) { if (thing instanceof Controller) {

View File

@ -37,7 +37,7 @@ export default abstract class ApplicationComponent {
protected async close(thingName: string, fn: (callback: (err?: Error | null) => void) => void): Promise<void> { protected async close(thingName: string, fn: (callback: (err?: Error | null) => void) => void): Promise<void> {
try { try {
await new Promise((resolve, reject) => fn((err?: Error | null) => { await new Promise<void>((resolve, reject) => fn((err?: Error | null) => {
if (err) reject(err); if (err) reject(err);
else resolve(); else resolve();
})); }));

View File

@ -10,7 +10,7 @@ export default abstract class Middleware {
} }
protected abstract async handle(req: Request, res: Response, next: NextFunction): Promise<void>; protected abstract handle(req: Request, res: Response, next: NextFunction): Promise<void>;
public getRequestHandler(): RequestHandler { public getRequestHandler(): RequestHandler {
return async (req, res, next): Promise<void> => { return async (req, res, next): Promise<void> => {

View File

@ -1,6 +1,7 @@
import WebSocket from "ws"; import WebSocket from "ws";
import {IncomingMessage} from "http"; import {IncomingMessage} from "http";
import Application from "./Application"; import Application from "./Application";
import {Session} from "express-session";
export default abstract class WebSocketListener<T extends Application> { export default abstract class WebSocketListener<T extends Application> {
private app!: T; private app!: T;
@ -15,9 +16,9 @@ export default abstract class WebSocketListener<T extends Application> {
public abstract path(): string; public abstract path(): string;
public abstract async handle( public abstract handle(
socket: WebSocket, socket: WebSocket,
request: IncomingMessage, request: IncomingMessage,
session: Express.Session | null, session: Session | null,
): Promise<void>; ): Promise<void>;
} }

View File

@ -10,6 +10,7 @@ import config from "config";
import Application from "../Application"; import Application from "../Application";
import NunjucksComponent from "../components/NunjucksComponent"; import NunjucksComponent from "../components/NunjucksComponent";
import AuthMethod from "./AuthMethod"; import AuthMethod from "./AuthMethod";
import {Session, SessionData} from "express-session";
export default class AuthGuard { export default class AuthGuard {
private readonly authMethods: AuthMethod<AuthProof<User>>[]; private readonly authMethods: AuthMethod<AuthProof<User>>[];
@ -54,14 +55,14 @@ export default class AuthGuard {
public async getProofs(req: Request): Promise<AuthProof<User>[]> { public async getProofs(req: Request): Promise<AuthProof<User>[]> {
const proofs = []; const proofs = [];
if (req.session) { if (req.getSessionOptional()) {
proofs.push(...await this.getProofsForSession(req.session)); proofs.push(...await this.getProofsForSession(req.session));
} }
proofs.push(...await this.getProofsForRequest(req)); proofs.push(...await this.getProofsForRequest(req));
return proofs; return proofs;
} }
public async getProofsForSession(session: Express.Session): Promise<AuthProof<User>[]> { public async getProofsForSession(session: Session & Partial<SessionData>): Promise<AuthProof<User>[]> {
if (!session.is_authenticated) return []; if (!session.is_authenticated) return [];
const proofs = []; const proofs = [];
@ -104,7 +105,7 @@ export default class AuthGuard {
} }
public async authenticateOrRegister( public async authenticateOrRegister(
session: Express.Session, session: Session & Partial<SessionData>,
proof: AuthProof<User>, proof: AuthProof<User>,
onLogin?: (user: User) => Promise<void>, onLogin?: (user: User) => Promise<void>,
beforeRegister?: (connection: Connection, user: User) => Promise<RegisterCallback[]>, beforeRegister?: (connection: Connection, user: User) => Promise<RegisterCallback[]>,

View File

@ -1,6 +1,7 @@
import User from "./models/User"; import User from "./models/User";
import AuthProof from "./AuthProof"; import AuthProof from "./AuthProof";
import {Request, Response} from "express"; import {Request, Response} from "express";
import {Session} from "express-session";
export default interface AuthMethod<P extends AuthProof<User>> { export default interface AuthMethod<P extends AuthProof<User>> {
@ -19,7 +20,7 @@ export default interface AuthMethod<P extends AuthProof<User>> {
findUserByIdentifier(identifier: string): Promise<User | null>; findUserByIdentifier(identifier: string): Promise<User | null>;
getProofsForSession?(session: Express.Session): Promise<P[]>; getProofsForSession?(session: Session): Promise<P[]>;
getProofsForRequest?(req: Request): Promise<P[]>; getProofsForRequest?(req: Request): Promise<P[]>;

View File

@ -14,6 +14,7 @@ import AuthMagicLinkActionType from "./AuthMagicLinkActionType";
import Validator, {EMAIL_REGEX} from "../../db/Validator"; import Validator, {EMAIL_REGEX} from "../../db/Validator";
import ModelFactory from "../../db/ModelFactory"; import ModelFactory from "../../db/ModelFactory";
import UserNameComponent from "../models/UserNameComponent"; import UserNameComponent from "../models/UserNameComponent";
import {Session} from "express-session";
export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> { export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> {
public constructor( public constructor(
@ -39,7 +40,7 @@ export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> {
.first())?.user.getOrFail() || null; .first())?.user.getOrFail() || null;
} }
public async getProofsForSession(session: Express.Session): Promise<MagicLink[]> { public async getProofsForSession(session: Session): Promise<MagicLink[]> {
return await MagicLink.select() return await MagicLink.select()
.where('session_id', session.id) .where('session_id', session.id)
.where('action_type', [AuthMagicLinkActionType.LOGIN, AuthMagicLinkActionType.REGISTER], WhereTest.IN) .where('action_type', [AuthMagicLinkActionType.LOGIN, AuthMagicLinkActionType.REGISTER], WhereTest.IN)

View File

@ -3,6 +3,7 @@ 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"; import Application from "../../Application";
import {Session} from "express-session";
export default class MagicLinkWebSocketListener<A extends Application> extends WebSocketListener<A> { export default class MagicLinkWebSocketListener<A extends Application> extends WebSocketListener<A> {
private readonly connections: { [p: string]: (() => void)[] | undefined } = {}; private readonly connections: { [p: string]: (() => void)[] | undefined } = {};
@ -14,7 +15,7 @@ export default class MagicLinkWebSocketListener<A extends Application> extends W
} }
} }
public async handle(socket: WebSocket, request: IncomingMessage, session: Express.Session | null): Promise<void> { public async handle(socket: WebSocket, request: IncomingMessage, session: 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.');

View File

@ -13,6 +13,7 @@ import UserPasswordComponent from "./UserPasswordComponent";
import UserNameComponent, {USERNAME_REGEXP} from "../models/UserNameComponent"; import UserNameComponent, {USERNAME_REGEXP} from "../models/UserNameComponent";
import ModelFactory from "../../db/ModelFactory"; import ModelFactory from "../../db/ModelFactory";
import {ServerError} from "../../HttpError"; import {ServerError} from "../../HttpError";
import {Session} from "express-session";
export default class PasswordAuthMethod implements AuthMethod<PasswordAuthProof> { export default class PasswordAuthMethod implements AuthMethod<PasswordAuthProof> {
public constructor( public constructor(
@ -46,7 +47,7 @@ export default class PasswordAuthMethod implements AuthMethod<PasswordAuthProof>
return null; return null;
} }
public async getProofsForSession(session: Express.Session): Promise<PasswordAuthProof[]> { public async getProofsForSession(session: Session): Promise<PasswordAuthProof[]> {
const proof = PasswordAuthProof.getProofForSession(session); const proof = PasswordAuthProof.getProofForSession(session);
return proof ? [proof] : []; return proof ? [proof] : [];
} }

View File

@ -1,14 +1,15 @@
import AuthProof from "../AuthProof"; import AuthProof from "../AuthProof";
import User from "../models/User"; import User from "../models/User";
import UserPasswordComponent from "./UserPasswordComponent"; import UserPasswordComponent from "./UserPasswordComponent";
import {Session, SessionData} from "express-session";
export default class PasswordAuthProof implements AuthProof<User> { export default class PasswordAuthProof implements AuthProof<User> {
public static getProofForSession(session: Express.Session): PasswordAuthProof | null { public static getProofForSession(session: Session & Partial<SessionData>): PasswordAuthProof | null {
return session.auth_password_proof ? new PasswordAuthProof(session) : null; return session.auth_password_proof ? new PasswordAuthProof(session) : null;
} }
public static createAuthorizedProofForRegistration(session: Express.Session): PasswordAuthProof { public static createAuthorizedProofForRegistration(session: Session): PasswordAuthProof {
const proofForSession = new PasswordAuthProof(session); const proofForSession = new PasswordAuthProof(session);
proofForSession.authorized = true; proofForSession.authorized = true;
proofForSession.forRegistration = true; proofForSession.forRegistration = true;
@ -16,17 +17,17 @@ export default class PasswordAuthProof implements AuthProof<User> {
return proofForSession; return proofForSession;
} }
public static createProofForLogin(session: Express.Session): PasswordAuthProof { public static createProofForLogin(session: Session & Partial<SessionData>): PasswordAuthProof {
return new PasswordAuthProof(session); return new PasswordAuthProof(session);
} }
private readonly session: Express.Session; private readonly session: Session & Partial<SessionData>;
private authorized: boolean; private authorized: boolean;
private forRegistration: boolean = false; private forRegistration: boolean = false;
private userId: number | null; private userId: number | null;
private userPassword: UserPasswordComponent | null = null; private userPassword: UserPasswordComponent | null = null;
private constructor(session: Express.Session) { private constructor(session: Session & Partial<SessionData>) {
this.session = session; this.session = session;
this.authorized = session.auth_password_proof?.authorized || false; this.authorized = session.auth_password_proof?.authorized || false;
this.forRegistration = session.auth_password_proof?.forRegistration || false; this.forRegistration = session.auth_password_proof?.forRegistration || false;
@ -80,3 +81,9 @@ export default class PasswordAuthProof implements AuthProof<User> {
}; };
} }
} }
export type PasswordAuthProofSessionData = {
authorized: boolean,
forRegistration: boolean,
userId: number | null,
};

View File

@ -3,11 +3,12 @@ import {Request, Router} from "express";
import crypto from "crypto"; import crypto from "crypto";
import {BadRequestError} from "../HttpError"; import {BadRequestError} from "../HttpError";
import {AuthMiddleware} from "../auth/AuthComponent"; import {AuthMiddleware} from "../auth/AuthComponent";
import {Session, SessionData} from "express-session";
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: Session & Partial<SessionData>): 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');
} }

View File

@ -31,7 +31,7 @@ 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.session?.id, sessionId: req.sessionID,
result: { result: {
code: res.statusCode, code: res.statusCode,
}, },

View File

@ -5,7 +5,7 @@ import onFinished from "on-finished";
export default class RedirectBackComponent extends ApplicationComponent { export default class RedirectBackComponent extends ApplicationComponent {
public static getPreviousURL(req: Request, defaultUrl?: string): string | undefined { public static getPreviousURL(req: Request, defaultUrl?: string): string | undefined {
return req.session?.previousUrl || defaultUrl; return req.getSessionOptional()?.previousUrl || defaultUrl;
} }
public async handle(router: Router): Promise<void> { public async handle(router: Router): Promise<void> {
@ -21,7 +21,7 @@ export default class RedirectBackComponent extends ApplicationComponent {
}; };
onFinished(res, (err) => { onFinished(res, (err) => {
const session = req.session; const session = req.getSessionOptional();
if (session) { if (session) {
const contentType = res.getHeader('content-type'); const contentType = res.getHeader('content-type');
if (!err && res.statusCode === 200 && ( if (!err && res.statusCode === 200 && (

View File

@ -38,10 +38,14 @@ export default class SessionComponent extends ApplicationComponent {
router.use(flash()); router.use(flash());
router.use((req, res, next) => { router.use((req, res, next) => {
req.getSession = () => { req.getSessionOptional = () => {
if (!req.session) throw new Error('Session not initialized.');
return req.session; return req.session;
}; };
req.getSession = () => {
const session = req.getSessionOptional();
if (!session) throw new Error('Session not initialized.');
return session;
};
res.locals.session = req.getSession(); res.locals.session = req.getSession();

View File

@ -65,7 +65,7 @@ export default class WebSocketServerComponent extends ApplicationComponent {
session.id = sid; session.id = sid;
store.createSession(<Request>request, session); store.createSession(<Request>request, session);
listener.handle(socket, request, <Express.Session>session).catch(err => { listener.handle(socket, request, (<Request>request).session).catch(err => {
log.error(err, 'Error in websocket listener.'); log.error(err, 'Error in websocket listener.');
}); });
}); });

View File

@ -14,9 +14,9 @@ export default abstract class Migration {
return this.version > currentVersion; return this.version > currentVersion;
} }
public abstract async install(): Promise<void>; public abstract install(): Promise<void>;
public abstract async rollback(): Promise<void>; public abstract rollback(): Promise<void>;
public registerModels?(): void; public registerModels?(): void;

View File

@ -97,8 +97,11 @@ export default abstract class ModelRelation<S extends Model, O extends Model, R
public async has(model: O): Promise<boolean> { public async has(model: O): Promise<boolean> {
const models = await this.get(); const models = await this.get();
if (Array.isArray(models)) return models.find(m => m.equals(model)) !== undefined; if (models instanceof Array) {
else return models && models.equals(model); return models.find(m => m.equals(model)) !== undefined;
} else {
return models !== null && models.equals(model);
}
} }
} }

View File

@ -57,7 +57,7 @@ export default class MysqlConnectionManager {
password: config.get('mysql.password'), password: config.get('mysql.password'),
charset: 'utf8mb4', charset: 'utf8mb4',
}); });
await new Promise((resolve, reject) => { await new Promise<void>((resolve, reject) => {
connection.query(`CREATE DATABASE IF NOT EXISTS ${dbName}`, (error) => { connection.query(`CREATE DATABASE IF NOT EXISTS ${dbName}`, (error) => {
return error !== null ? return error !== null ?
reject(error) : reject(error) :
@ -140,7 +140,7 @@ export default class MysqlConnectionManager {
connection.beginTransaction((err?: MysqlError) => { connection.beginTransaction((err?: MysqlError) => {
if (err) { if (err) {
reject(err); reject(err);
this.pool.releaseConnection(connection); connection.release();
return; return;
} }
@ -148,16 +148,16 @@ export default class MysqlConnectionManager {
connection.commit((err?: MysqlError) => { connection.commit((err?: MysqlError) => {
if (err) { if (err) {
this.rejectAndRollback(connection, err, reject); this.rejectAndRollback(connection, err, reject);
this.pool.releaseConnection(connection); connection.release();
return; return;
} }
this.pool.releaseConnection(connection); connection.release();
resolve(val); resolve(val);
}); });
}).catch(err => { }).catch(err => {
this.rejectAndRollback(connection, err, reject); this.rejectAndRollback(connection, err, reject);
this.pool.releaseConnection(connection); connection.release();
}); });
}); });
}); });

View File

@ -3,13 +3,18 @@ import {Type} from "../Utils";
import Middleware from "../Middleware"; import Middleware from "../Middleware";
import {FlashMessages} from "../components/SessionComponent"; import {FlashMessages} from "../components/SessionComponent";
import {Logger} from "tslog"; import {Logger} from "tslog";
import {Session, SessionData} from "express-session";
import {PasswordAuthProofSessionData} from "../auth/password/PasswordAuthProof";
declare global { declare global {
namespace Express { namespace Express {
export interface Request { export interface Request {
log: Logger; log: Logger;
getSession(): Session;
getSession(): Session & Partial<SessionData>;
getSessionOptional(): Session & Partial<SessionData> | undefined;
files: Files; files: Files;
@ -32,3 +37,17 @@ declare global {
} }
} }
} }
declare module 'express-session' {
interface SessionData {
id?: string;
previousUrl?: string;
is_authenticated?: boolean;
auth_password_proof?: PasswordAuthProofSessionData;
csrf?: string;
}
}

595
yarn.lock

File diff suppressed because it is too large Load Diff