141 lines
No EOL
5.6 KiB
TypeScript
141 lines
No EOL
5.6 KiB
TypeScript
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<UserLevel> = [];
|
|
const actionAuthLevels: { [ key : string ]: Array<UserLevel> } = {};
|
|
|
|
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");
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
const requestCtx = new RequestCtx(req, res, controllerName, methodName, session);
|
|
controllerRequestHandler.bind(requestCtx)(req.method === "GET" ? req.query : req.body);
|
|
}
|
|
|
|
let funcMethods:Array<string> = [];
|
|
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) {}
|
|
} |