import { cyan } from "dyetty"; import { Console } from "hsconsole"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import RequestCtx from "../objects/RequestCtx"; import Session from "../objects/Session"; import SessionUser from "../objects/SessionUser"; import UserType from "../enums/UserType"; import HeaderUtility from "../utilities/HeaderUtility"; // prepare for ts-ignore :3 // TODO: figure out some runtime field / type checking so // can auto badRequest on missing stuff. export default abstract class Controller { public static FastifyInstance:FastifyInstance; public static RegisteredPaths:Array = []; private logInfo(logText: string) { Console.printInfo(`[ ${cyan("CONTROLLER")} ] ${logText}`); } public constructor() { const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this)); const rawControllerParts = this.constructor.name.split("_"); const controllerName = rawControllerParts.splice(0, 1)[0].replace("Controller", "").toLowerCase(); const controllerAuthLevels: Array = []; const actionAuthLevels: { [ key : string ]: Array } = {}; for (const prop of rawControllerParts) { if (prop.startsWith("Auth")) { const userType = prop.split("$")[1]; // @ts-ignore controllerAuthLevels.push(UserType[userType]); this.logInfo(`Set Auth level requirement for ${this.constructor.name} to ${userType}`); } } for (const method of methods) { if (method === "constructor" || method[0] !== method[0].toUpperCase()) { // * Anything that starts with lowercase we'll consider "private" continue; } const params = method.split("_"); const methodNameRaw = params.splice(0, 1)[0]; const methodName = methodNameRaw.toLowerCase(); const doAuth = !params.includes("AllowAnonymous"); // @ts-ignore const controllerRequestHandler = this[method]; const requestHandler = (req:FastifyRequest, res:FastifyReply) => { let session = Session.CheckValiditiy(req.cookies); if (doAuth && session === undefined) { return res.redirect(`/account/login?returnTo=${encodeURIComponent(req.url)}`); } const methodAuthCheck = actionAuthLevels[`${controllerName}_${methodName}_${req.method.toLowerCase()}`]; let wasMethodMatch = false; if (methodAuthCheck && session !== undefined) { for (const auth of methodAuthCheck) { if (auth === session.userType) { wasMethodMatch = true; } } } if (!wasMethodMatch && session !== undefined && controllerAuthLevels.length > 0) { let hasLevelMatch = false; for (const level of controllerAuthLevels) { if (level === session.userType) { hasLevelMatch = true; } } if (!hasLevelMatch) { return res.status(403).send("Forbidden"); } } if (controllerName !== "api") { HeaderUtility.AddBakedHeaders(res); } const requestCtx = new RequestCtx(req, res, controllerName, methodName, session); controllerRequestHandler.bind(requestCtx)(req.method === "GET" ? req.query : req.body); } let funcMethods:Array = []; let thisMethodHttpMethod = ""; for (const param of params) { if (param === "Get" || param === "Post" || param === "Put") { funcMethods.push(param); thisMethodHttpMethod = param.toLowerCase(); // @ts-ignore Controller.FastifyInstance[param.toLowerCase()](`/${controllerName}/${methodName === "index" ? "" : methodName}`, requestHandler); this.logInfo(`Registered ${this.constructor.name}.${method} to "/${controllerName}/${methodName === "index" ? "" : methodName}" as ${param}`); Controller.RegisteredPaths.push(`/${controllerName}/${methodName === "index" ? "" : methodName}`); if (methodName === "index") { // @ts-ignore Controller.FastifyInstance[param.toLowerCase()](`/${controllerName}/${methodName}`, requestHandler); this.logInfo(`Registered ${this.constructor.name}.${method} to "/${controllerName}/${methodName}" as ${param}`); Controller.RegisteredPaths.push(`/${controllerName}/${methodName}`); // @ts-ignore Controller.FastifyInstance[param.toLowerCase()](`/${controllerName}`, requestHandler); this.logInfo(`Registered ${this.constructor.name}.${method} to "/${controllerName}" as ${param}`); Controller.RegisteredPaths.push(`/${controllerName}`); } else if (controllerName === "home") { // @ts-ignore Controller.FastifyInstance[param.toLowerCase()](`/${methodName}`, requestHandler); this.logInfo(`Registered ${this.constructor.name}.${method} to "/${methodName}" as ${param}`); Controller.RegisteredPaths.push(`/${methodName}`); } } else if (param.startsWith("Auth")) { const nameWithMethod = `${controllerName}_${methodName}_${thisMethodHttpMethod}`; const userType = param.split("$")[1]; if (!(nameWithMethod in actionAuthLevels)) { actionAuthLevels[nameWithMethod] = []; } // @ts-ignore actionAuthLevels[nameWithMethod].push(UserType[userType]); this.logInfo(`Set Auth level requirement for ${this.constructor.name}.${method} to ${userType}`); } } if (controllerName === "home" && methodName === "index") { for (const httpMethod of funcMethods) { // @ts-ignore Controller.FastifyInstance[httpMethod.toLowerCase()](`/`, requestHandler); this.logInfo(`Registered ${this.constructor.name}.${method} to "/" as ${httpMethod}`); Controller.RegisteredPaths.push(`/`); } } } } // not real, these should mirror RequestCtx so they autocomplete :) // yeah, i know. this is terrible. // Fields // @ts-ignore public session:SessionUser; // @ts-ignore public req: FastifyRequest; // @ts-ignore public res: FastifyReply; // Methods view(view?:string | Object, model?: Object) { view; model; } redirectToAction(action:string, controller?:string) { action; controller; } ok(message?:string) { message } badRequest(message?:string) { message } unauthorised(message?:string) { message } forbidden(message?:string) { message } }