diff --git a/src/util/util/lambert-server/HTTPError.ts b/src/util/util/lambert-server/HTTPError.ts
new file mode 100644
index 000000000..70ba92a82
--- /dev/null
+++ b/src/util/util/lambert-server/HTTPError.ts
@@ -0,0 +1,5 @@
+export class HTTPError extends Error {
+ constructor(message: string, public code: number = 400) {
+ super(message);
+ }
+}
diff --git a/src/util/util/lambert-server/Server.ts b/src/util/util/lambert-server/Server.ts
new file mode 100644
index 000000000..2325ef492
--- /dev/null
+++ b/src/util/util/lambert-server/Server.ts
@@ -0,0 +1,174 @@
+import express, { Application, NextFunction, Request, Response, Router } from "express";
+import { traverseDirectory } from "./Utils";
+import { Server as HTTPServer } from "http";
+import { HTTPError } from "./HTTPError";
+import "express-async-errors";
+import "missing-native-js-functions";
+import bodyParser from "body-parser";
+// import helmet from "helmet";
+import http from "http";
+import chalk from "chalk";
+
+declare global {
+ namespace Express {
+ interface Request {
+ server: Server;
+ }
+ }
+}
+
+export type ServerOptions = {
+ port: number;
+ host: string;
+ production: boolean;
+ serverInitLogging: boolean;
+ errorHandler?: { (err: Error, req: Request, res: Response, next: NextFunction): any };
+ jsonBody: boolean;
+ server: http.Server;
+ app: Application;
+};
+
+// Overwrite default options for Router with default value true for mergeParams
+const oldRouter = express.Router;
+express.Router = function (options?: express.RouterOptions | undefined): Router {
+ if (!options) options = {};
+ if (options.mergeParams == null) options.mergeParams = true;
+
+ return oldRouter(options);
+};
+
+export class Server {
+ public app: Application;
+ public http: HTTPServer;
+ public options: ServerOptions;
+ public routes: Router[];
+
+ constructor(opts?: Partial<ServerOptions>) {
+ if (!opts) opts = {};
+ if (!opts.port) opts.port = 8080;
+ if (!opts.host) opts.host = "0.0.0.0";
+ if (opts.production == null) opts.production = false;
+ if (opts.serverInitLogging == null) opts.serverInitLogging = true;
+ if (opts.errorHandler == null) opts.errorHandler = this.errorHandler;
+ if (opts.jsonBody == null) opts.jsonBody = true;
+ if (opts.server) this.http = opts.server;
+
+ this.options = <ServerOptions>opts;
+
+ if (opts.app) this.app = opts.app;
+ else this.app = express();
+ }
+
+ // protected secureExpress() {
+ // this.app.use(helmet.contentSecurityPolicy());
+ // this.app.use(helmet.expectCt);
+ // this.app.use(helmet.originAgentCluster());
+ // this.app.use(helmet.referrerPolicy({ policy: "same-origin" }));
+ // this.app.use(helmet.hidePoweredBy());
+ // this.app.use(helmet.noSniff());
+ // this.app.use(helmet.dnsPrefetchControl({ allow: true }));
+ // this.app.use(helmet.ieNoOpen());
+ // this.app.use(helmet.frameguard({ action: "deny" }));
+ // this.app.use(helmet.permittedCrossDomainPolicies({ permittedPolicies: "none" }));
+ // }
+
+ public errorHandler = (error: Error, req: Request, res: Response, next: NextFunction) => {
+ try {
+ let code;
+ let message = error?.toString();
+
+ if (error instanceof HTTPError && error.code) code = error.code || 400;
+ else {
+ console.error(error);
+ if (this.options.production) {
+ message = "Internal Server Error";
+ }
+ code = 500;
+ }
+
+ res.status(code).json({ success: false, code: code, error: true, message });
+ } catch (e) {
+ console.error(e);
+ return res.status(500).json({ success: false, code: 500, error: true, message: "Internal Server Error" });
+ }
+ };
+
+ async start() {
+ const server = this.http || this.app;
+ if (!server.listening) {
+ await new Promise<void>((res) => {
+ this.http = server.listen(this.options.port, () => res());
+ });
+ if(this.options.serverInitLogging) this.log("info", `[Server] started on ${this.options.host}:${this.options.port}`);
+ }
+ }
+
+ async registerRoutes(root: string) {
+ this.app.use((req, res, next) => {
+ req.server = this;
+ next();
+ });
+ if (this.options.jsonBody) this.app.use(bodyParser.json());
+ const result = await traverseDirectory({ dirname: root, recursive: true }, this.registerRoute.bind(this, root));
+ if (this.options.errorHandler) this.app.use(this.options.errorHandler);
+ // if (this.options.production) this.secureExpress();
+ return result;
+ }
+
+ log(l: "info" | "error" | "warn" | "verbose", ...args: any[]) {
+ // @ts-ignore
+ if (!console[l]) l = "verbose";
+
+ const level = l === "verbose" ? "log" : l;
+
+ var color: "red" | "yellow" | "blue" | "reset";
+
+ switch (level) {
+ case "error":
+ color = "red";
+ break;
+ case "warn":
+ color = "yellow";
+ break;
+ case "info":
+ color = "blue";
+ case "log":
+ default:
+ color = "reset";
+ }
+
+ if (this.options.production && l === "verbose") return;
+
+ console[level](chalk[color](`[${new Date().toTimeString().split(" ")[0]}]`), ...args);
+ }
+
+ registerRoute(root: string, file: string): any {
+ if (root.endsWith("/") || root.endsWith("\\")) root = root.slice(0, -1); // removes slash at the end of the root dir
+ let path = file.replace(root, ""); // remove root from path and
+ path = path.split(".").slice(0, -1).join("."); // trancate .js/.ts file extension of path
+ path = path.replaceAll("#", ":").replaceAll("!", "?").replaceAll("\\", "/");
+ if (path.endsWith("/index")) path = path.slice(0, -6); // delete index from path
+ if (!path.length) path = "/"; // first root index.js file must have a / path
+
+ try {
+ var router = require(file);
+ if (router.router) router = router.router;
+ if (router.default) router = router.default;
+ if (!router || router?.prototype?.constructor?.name !== "router")
+ throw `File doesn't export any default router`;
+
+ if (this.options.errorHandler) router.use(this.options.errorHandler);
+ this.app.use(path, <Router>router);
+
+ if(this.options.serverInitLogging) this.log("verbose", `[Server] Route ${path} registered`);
+
+ return router;
+ } catch (error) {
+ console.error(new Error(`[Server] Failed to register route ${path}: ${error}`));
+ }
+ }
+
+ stop() {
+ return new Promise<void>((res) => this.http.close(() => res()));
+ }
+}
diff --git a/src/util/util/lambert-server/Utils.ts b/src/util/util/lambert-server/Utils.ts
new file mode 100644
index 000000000..2e30c24d4
--- /dev/null
+++ b/src/util/util/lambert-server/Utils.ts
@@ -0,0 +1,40 @@
+import fs from "fs";
+import "missing-native-js-functions";
+
+export interface traverseDirectoryOptions {
+ dirname: string;
+ filter?: RegExp;
+ excludeDirs?: RegExp;
+ recursive?: boolean;
+}
+
+const DEFAULT_EXCLUDE_DIR = /^\./;
+const DEFAULT_FILTER = /^([^\.].*)(?<!\.d)\.(ts|js)$/;
+
+export async function traverseDirectory<T>(
+ options: traverseDirectoryOptions,
+ action: (path: string) => T
+): Promise<T[]> {
+ if (!options.filter) options.filter = DEFAULT_FILTER;
+ if (!options.excludeDirs) options.excludeDirs = DEFAULT_EXCLUDE_DIR;
+
+ const routes = fs.readdirSync(options.dirname);
+ const promises = <Promise<T | T[] | undefined>[]>routes
+ .sort((a, b) => (a.startsWith("#") ? 1 : -1)) // load #parameter routes last
+ .map(async (file) => {
+ const path = options.dirname + file;
+ const stat = fs.lstatSync(path);
+ if (path.match(<RegExp>options.excludeDirs)) return;
+
+ if (path.match(<RegExp>options.filter) && stat.isFile()) {
+ return action(path);
+ } else if (options.recursive && stat.isDirectory()) {
+ return traverseDirectory({ ...options, dirname: path + "/" }, action);
+ }
+ });
+ const result = await Promise.all(promises);
+
+ const t = <(T | undefined)[]>result.flat();
+
+ return <T[]>t.filter((x) => x != undefined);
+}
diff --git a/src/util/util/lambert-server/check.ts b/src/util/util/lambert-server/check.ts
new file mode 100644
index 000000000..c025bcbf4
--- /dev/null
+++ b/src/util/util/lambert-server/check.ts
@@ -0,0 +1,127 @@
+import "missing-native-js-functions";
+import { NextFunction, Request, Response } from "express";
+import { HTTPError } from ".";
+
+const OPTIONAL_PREFIX = "$";
+const EMAIL_REGEX =
+ /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+
+export function check(schema: any) {
+ return (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const result = instanceOf(schema, req.body, { path: "body" });
+ if (result === true) return next();
+ throw result;
+ } catch (error) {
+ next(new HTTPError((error as any).toString(), 400));
+ }
+ };
+}
+
+export class Tuple {
+ public types: any[];
+ constructor(...types: any[]) {
+ this.types = types;
+ }
+}
+
+export class Email {
+ constructor(public email: string) {}
+ check() {
+ return !!this.email.match(EMAIL_REGEX);
+ }
+}
+
+export function instanceOf(
+ type: any,
+ value: any,
+ { path = "", optional = false }: { path?: string; optional?: boolean } = {}
+): Boolean {
+ if (!type) return true; // no type was specified
+
+ if (value == null) {
+ if (optional) return true;
+ throw `${path} is required`;
+ }
+
+ switch (type) {
+ case String:
+ if (typeof value === "string") return true;
+ throw `${path} must be a string`;
+ case Number:
+ value = Number(value);
+ if (typeof value === "number" && !isNaN(value)) return true;
+ throw `${path} must be a number`;
+ case BigInt:
+ try {
+ value = BigInt(value);
+ if (typeof value === "bigint") return true;
+ } catch (error) {}
+ throw `${path} must be a bigint`;
+ case Boolean:
+ if (value == "true") value = true;
+ if (value == "false") value = false;
+ if (typeof value === "boolean") return true;
+ throw `${path} must be a boolean`;
+ case Object:
+ if (typeof value === "object" && value !== null) return true;
+ throw `${path} must be a object`;
+ }
+
+ if (typeof type === "object") {
+ if (Array.isArray(type)) {
+ if (!Array.isArray(value)) throw `${path} must be an array`;
+ if (!type.length) return true; // type array didn't specify any type
+
+ return value.every((val, i) => instanceOf(type[0], val, { path: `${path}[${i}]`, optional }));
+ }
+ if (type?.constructor?.name != "Object") {
+ if (type instanceof Tuple) {
+ if (
+ (<Tuple>type).types.some((x) => {
+ try {
+ return instanceOf(x, value, { path, optional });
+ } catch (error) {
+ return false;
+ }
+ })
+ ) {
+ return true;
+ }
+ throw `${path} must be one of ${type.types}`;
+ }
+ if (type instanceof Email) {
+ if ((<Email>type).check()) return true;
+ throw `${path} is not a valid E-Mail`;
+ }
+ if (value instanceof type) return true;
+ throw `${path} must be an instance of ${type}`;
+ }
+ if (typeof value !== "object") throw `${path} must be a object`;
+
+ const diff = Object.keys(value).missing(
+ Object.keys(type).map((x) => (x.startsWith(OPTIONAL_PREFIX) ? x.slice(OPTIONAL_PREFIX.length) : x))
+ );
+
+ if (diff.length) throw `Unkown key ${diff}`;
+
+ return Object.keys(type).every((key) => {
+ let newKey = key;
+ const OPTIONAL = key.startsWith(OPTIONAL_PREFIX);
+ if (OPTIONAL) newKey = newKey.slice(OPTIONAL_PREFIX.length);
+
+ return instanceOf(type[key], value[newKey], {
+ path: `${path}.${newKey}`,
+ optional: OPTIONAL,
+ });
+ });
+ } else if (typeof type === "number" || typeof type === "string" || typeof type === "boolean") {
+ if (value === type) return true;
+ throw `${path} must be ${value}`;
+ } else if (typeof type === "bigint") {
+ if (BigInt(value) === type) return true;
+ throw `${path} must be ${value}`;
+ }
+
+ return type == value;
+}
diff --git a/src/util/util/lambert-server/index.ts b/src/util/util/lambert-server/index.ts
new file mode 100644
index 000000000..6575a7d3e
--- /dev/null
+++ b/src/util/util/lambert-server/index.ts
@@ -0,0 +1,4 @@
+export * from "./check";
+export * from "./Server";
+export * from "./Utils";
+export * from "./HTTPError";
|