import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import { Console } from "hsconsole"; import Session from "../objects/Session"; import SessionUser from "../objects/SessionUser"; import RequestCtx from "../objects/RequestCtx"; import { UserLevel } from "../enums/UserLevel"; // 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 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 userLevel = prop.split("$")[1]; // @ts-ignore controllerAuthLevels.push(UserLevel[userLevel]); Console.printInfo(`Set Auth level requirement for ${this.constructor.name} to ${userLevel}`); } } 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.userLevel) { wasMethodMatch = true; } } } if (!wasMethodMatch && session !== undefined && controllerAuthLevels.length > 0) { let hasLevelMatch = false; for (const level of controllerAuthLevels) { if (level === session.userLevel) { hasLevelMatch = true; } } if (!hasLevelMatch) { return res.status(403).send("Forbidden"); } } 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); Console.printInfo(`Registered ${this.constructor.name}.${method} to "/${controllerName}/${methodName === "index" ? "" : methodName}" as ${param}`); if (methodName === "index") { // @ts-ignore Controller.FastifyInstance[param.toLowerCase()](`/${controllerName}/${methodName}`, requestHandler); Console.printInfo(`Registered ${this.constructor.name}.${method} to "/${controllerName}/${methodName}" as ${param}`); // @ts-ignore Controller.FastifyInstance[param.toLowerCase()](`/${controllerName}`, requestHandler); Console.printInfo(`Registered ${this.constructor.name}.${method} to "/${controllerName}" as ${param}`); } } else if (param.startsWith("Auth")) { const nameWithMethod = `${controllerName}_${methodName}_${thisMethodHttpMethod}`; const userLevel = param.split("$")[1]; if (!(nameWithMethod in actionAuthLevels)) { actionAuthLevels[nameWithMethod] = []; } // @ts-ignore actionAuthLevels[nameWithMethod].push(UserLevel[userLevel]); Console.printInfo(`Set Auth level requirement for ${this.constructor.name}.${method} to ${userLevel}`); } } if (controllerName === "home" && methodName === "index") { for (const httpMethod of funcMethods) { Console.printInfo(`Registered ${this.constructor.name}.${method} to "/" as ${httpMethod}`); // @ts-ignore Controller.FastifyInstance[httpMethod.toLowerCase()](`/`, requestHandler); } } } } // not real, these 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) {} redirectToAction(action:string, controller?:string) {} ok(message?:string) {} badRequest(message?:string) {} unauthorised(message?:string) {} forbidden(message?:string) {} }