t00-multiuser/server/controller/Controller.ts

141 lines
5.6 KiB
TypeScript
Raw Permalink Normal View History

2024-09-19 00:41:40 +01:00
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";
2024-09-26 00:47:08 +01:00
import { UserLevel } from "../enums/UserLevel";
2024-09-19 00:41:40 +01:00
// 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));
2024-09-26 00:47:08 +01:00
const rawControllerParts = this.constructor.name.split("_");
const controllerName = rawControllerParts.splice(0, 1)[0].replace("Controller", "").toLowerCase();
2024-09-29 21:35:36 +01:00
const controllerAuthLevels: Array<UserLevel> = [];
const actionAuthLevels: { [ key : string ]: Array<UserLevel> } = {};
2024-09-26 00:47:08 +01:00
for (const prop of rawControllerParts) {
if (prop.startsWith("Auth")) {
const userLevel = prop.split("$")[1];
// @ts-ignore
2024-09-29 21:35:36 +01:00
controllerAuthLevels.push(UserLevel[userLevel]);
2024-09-26 00:47:08 +01:00
Console.printInfo(`Set Auth level requirement for ${this.constructor.name} to ${userLevel}`);
}
}
2024-09-19 00:41:40 +01:00
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("_");
2024-09-29 21:35:36 +01:00
const methodNameRaw = params.splice(0, 1)[0];
2024-09-19 00:41:40 +01:00
const methodName = methodNameRaw.toLowerCase();
const doAuth = !params.includes("AllowAnonymous");
2024-09-26 00:47:08 +01:00
2024-09-19 00:41:40 +01:00
// @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)}`);
}
2024-09-29 21:35:36 +01:00
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");
}
2024-09-26 00:47:08 +01:00
}
2024-09-19 00:41:40 +01:00
res.header("X-Powered-By", "MultiProbe");
if (controllerName !== "api") {
res.header("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
res.header("X-XSS-Protection", "1; mode=block");
res.header("Permissions-Policy", "microphone=(), geolocation=(), magnetometer=(), camera=(), payment=(), usb=(), accelerometer=(), gyroscope=()");
res.header("Referrer-Policy", "strict-origin-when-cross-origin");
res.header("Content-Security-Policy", "block-all-mixed-content;frame-ancestors 'self'");
res.header("X-Frame-Options", "SAMEORIGIN");
res.header("X-Content-Type-Options", "nosniff");
}
2024-09-19 00:41:40 +01:00
const requestCtx = new RequestCtx(req, res, controllerName, methodName, session);
controllerRequestHandler.bind(requestCtx)(req.method === "GET" ? req.query : req.body);
}
let funcMethods:Array<string> = [];
2024-09-29 21:35:36 +01:00
let thisMethodHttpMethod = "";
2024-09-19 00:41:40 +01:00
for (const param of params) {
if (param === "Get" || param === "Post" || param === "Put") {
funcMethods.push(param);
2024-09-29 21:35:36 +01:00
thisMethodHttpMethod = param.toLowerCase();
2024-09-19 00:41:40 +01:00
// @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}`);
}
2024-09-29 21:35:36 +01:00
} 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}`);
2024-09-19 00:41:40 +01:00
}
}
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) {}
2024-09-26 00:47:08 +01:00
forbidden(message?:string) {}
2024-09-19 00:41:40 +01:00
}