import { cyan, green, red } from 'colors'; import { Express, Request, Response } from 'express'; import { Utils } from '../Utils'; import RouteSubscriber from './RouteSubscriber'; import { AdminPrivileges } from './SocketData'; import { DashUserModel } from './authentication/DashUserModel'; export enum Method { GET, POST, PATCH, DELETE, } export interface CoreArguments { req: Request; res: Response; isRelease: boolean; } export type AuthorizedCore = CoreArguments & { user: Partial }; export type SecureHandler = (core: AuthorizedCore) => unknown | Promise; export type PublicHandler = (core: CoreArguments) => unknown | Promise; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type ErrorHandler = (core: CoreArguments & { error: any }) => unknown | Promise; export const STATUS = { OK: 200, BAD_REQUEST: 400, EXECUTION_ERROR: 500, PERMISSION_DENIED: 403, }; // eslint-disable-next-line @typescript-eslint/no-explicit-any export function _error(res: Response, message: string, error?: any) { console.error(message, error); res.statusMessage = message; res.status(STATUS.EXECUTION_ERROR).send(error); } export function _success(res: Response, body: unknown) { res.status(STATUS.OK).send(body); } export function _invalid(res: Response, message: string) { res.status(STATUS.BAD_REQUEST).send(message); } export function _permissionDenied(res: Response, message?: string) { if (message) { res.statusMessage = message; } res.status(STATUS.PERMISSION_DENIED).send(`Permission Denied! ${message}`); } export interface RouteInitializer { method: Method; subscription: string | RouteSubscriber | (string | RouteSubscriber)[]; secureHandler: SecureHandler; publicHandler?: PublicHandler; errorHandler?: ErrorHandler; requireAdminInRelease?: true; } const registered = new Map>(); enum RegistrationError { Malformed, Duplicate, } export default class RouteManager { private server: Express; private _isRelease: boolean; private failedRegistrations: { route: string; reason: RegistrationError }[] = []; public get isRelease() { return this._isRelease; } constructor(server: Express, isRelease: boolean) { this.server = server; this._isRelease = isRelease; } logRegistrationOutcome = () => { if (this.failedRegistrations.length) { let duplicateCount = 0; let malformedCount = 0; this.failedRegistrations.forEach(({ reason, route }) => { let error: string; if (reason === RegistrationError.Duplicate) { error = `duplicate registration error: ${route} is already registered `; duplicateCount++; } else { error = `malformed route error: ${route} is invalid`; malformedCount++; } console.log(red(error)); }); console.log(); if (duplicateCount) { console.log('please remove all duplicate routes before continuing'); } if (malformedCount) { console.log(`please ensure all routes adhere to ^/$|^/[A-Za-z]+(/:[A-Za-z?_]+)*$`); } process.exit(1); } else { console.log(green('all server routes have been successfully registered:')); Array.from(registered.keys()) .sort() .forEach(route => console.log(cyan(route))); console.log(); } }; static routes: string[] = []; /** * * @param initializer */ addSupervisedRoute = (initializer: RouteInitializer): void => { const { method, subscription, secureHandler, publicHandler, errorHandler, requireAdminInRelease: requireAdmin } = initializer; typeof initializer.subscription === 'string' && RouteManager.routes.push(initializer.subscription); initializer.subscription instanceof RouteSubscriber && RouteManager.routes.push(initializer.subscription.root); initializer.subscription instanceof Array && initializer.subscription.forEach(sub => { typeof sub === 'string' && RouteManager.routes.push(sub); sub instanceof RouteSubscriber && RouteManager.routes.push(sub.root); }); const isRelease = this._isRelease; const supervised = async (req: Request, res: Response) => { let user = req.user as Partial | undefined; const { originalUrl: target } = req; if (process.env.DB === 'MEM' && !user) { user = { id: 'guest', email: 'guest', userDocumentId: Utils.GuestID() }; } const core = { req, res, isRelease }; if (user) { if (requireAdmin && isRelease && process.env.PASSWORD) { if (AdminPrivileges.get(user.id)) { AdminPrivileges.delete(user.id); } else { res.redirect(`/admin/${req.originalUrl.substring(1).replace('/', ':')}`); return; } } try { await secureHandler({ ...core, user }); } catch (e) { console.log(red(target), user && 'email' in user ? '' : undefined); if (errorHandler) { errorHandler({ ...core, error: e }); } else { _error(res, `The server encountered an internal error when serving ${target}.`, e); } } } // req.session!.target = target; else if (publicHandler) { try { await publicHandler(core); } catch (e) { console.log(red(target), user && 'email' in user ? '' : undefined); if (errorHandler) { errorHandler({ ...core, error: e }); } else { _error(res, `The server encountered an internal error when serving ${target}.`, e); } } if (!res.headersSent) { // res.redirect("/login"); } } else { res.redirect('/login'); } setTimeout(() => { if (!res.headersSent) { console.log(red(`Initiating fallback for ${target}. Please remove dangling promise from route handler`)); const warning = `request to ${target} fell through - this is a fallback response`; res.send({ warning }); } }, 1000); }; const subscribe = (subscriber: RouteSubscriber | string) => { let route: string; if (typeof subscriber === 'string') { route = subscriber; } else { route = subscriber.build; } if (!/^\/$|^\/[A-Za-z*]+(\/:[A-Za-z?_*]+)*$/g.test(route)) { this.failedRegistrations.push({ reason: RegistrationError.Malformed, route, }); } else { const existing = registered.get(route); if (existing) { if (existing.has(method)) { this.failedRegistrations.push({ reason: RegistrationError.Duplicate, route, }); return; } } else { const specific = new Set(); specific.add(method); registered.set(route, specific); } switch (method) { case Method.GET: this.server.get(route, supervised); break; case Method.POST: this.server.post(route, supervised); break; case Method.PATCH: this.server.patch(route, supervised); break; case Method.DELETE: this.server.delete(route, supervised); break; default: } } }; if (Array.isArray(subscription)) { subscription.forEach(subscribe); } else { subscribe(subscription); } }; }