/[sudobot]/branches/6.x/src/api/Server.ts
ViewVC logotype

Contents of /branches/6.x/src/api/Server.ts

Parent Directory Parent Directory | Revision Log Revision Log


Revision 577 - (show annotations)
Mon Jul 29 18:52:37 2024 UTC (8 months ago) by rakinar2
File MIME type: application/typescript
File size: 8031 byte(s)
chore: add old version archive branches (2.x to 9.x-dev)
1 /**
2 * This file is part of SudoBot.
3 *
4 * Copyright (C) 2021-2023 OSN Developers.
5 *
6 * SudoBot is free software; you can redistribute it and/or modify it
7 * under the terms of the GNU Affero General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * SudoBot is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Affero General Public License for more details.
15 *
16 * You should have received a copy of the GNU Affero General Public License
17 * along with SudoBot. If not, see <https://www.gnu.org/licenses/>.
18 */
19
20 import cors from "cors";
21 import express, { Request as ExpressRequest, Response as ExpressResponse, NextFunction } from "express";
22 import rateLimit from "express-rate-limit";
23 import fs from "fs/promises";
24 import { Server as HttpServer } from "http";
25 import { join, resolve } from "path";
26 import Client from "../core/Client";
27 import { RouteMetadata } from "../types/RouteMetadata";
28 import { log, logError, logInfo, logWarn } from "../utils/logger";
29 import Controller from "./Controller";
30 import Response from "./Response";
31
32 export default class Server {
33 protected expressApp = express();
34 public readonly port = process.env.PORT ?? 4000;
35 protected controllersDirectory = resolve(__dirname, "controllers");
36 expressServer?: HttpServer;
37
38 constructor(protected client: Client) {}
39
40 async onReady() {
41 if (this.client.configManager.systemConfig.api.enabled) {
42 await this.boot();
43 }
44 }
45
46 async boot() {
47 const router = express.Router();
48 await this.loadControllers(undefined, router);
49
50 this.expressApp.use((err: unknown, req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
51 if (err instanceof SyntaxError && "status" in err && err.status === 400 && "body" in err) {
52 res.status(400).json({
53 error: "Invalid JSON payload"
54 });
55
56 return;
57 }
58 });
59
60 this.expressApp.use(cors());
61
62 const limiter = rateLimit({
63 windowMs: 30 * 1000,
64 max: 28,
65 standardHeaders: true,
66 legacyHeaders: false
67 });
68
69 const configLimiter = rateLimit({
70 windowMs: 10 * 1000,
71 max: 7,
72 standardHeaders: true,
73 legacyHeaders: false
74 });
75
76 if (this.client.configManager.systemConfig.trust_proxies !== undefined) {
77 logInfo("Set express trust proxy option value to ", this.client.configManager.systemConfig.trust_proxies);
78 this.expressApp.set("trust proxy", this.client.configManager.systemConfig.trust_proxies);
79 }
80
81 this.expressApp.use(limiter);
82 this.expressApp.use("/config", configLimiter);
83 this.expressApp.use(express.json());
84 this.expressApp.use("/", router);
85 }
86
87 async loadControllers(directory = this.controllersDirectory, router: express.Router) {
88 const files = await fs.readdir(directory);
89
90 for (const file of files) {
91 const filePath = join(directory, file);
92 const isDirectory = (await fs.lstat(filePath)).isDirectory();
93
94 if (isDirectory) {
95 await this.loadControllers(filePath, router);
96 continue;
97 }
98
99 if ((!file.endsWith(".ts") && !file.endsWith(".js")) || file.endsWith(".d.ts")) {
100 continue;
101 }
102
103 const { default: ControllerClass } = await import(filePath);
104 const controller: Controller = new ControllerClass(this.client);
105
106 const metadata: Record<string, RouteMetadata> | undefined = Reflect.getMetadata(
107 "action_methods",
108 ControllerClass.prototype
109 );
110
111 const authMiddleware = Reflect.getMetadata("auth_middleware", ControllerClass.prototype) ?? {};
112 const gacMiddleware = Reflect.getMetadata("gac_middleware", ControllerClass.prototype) ?? {};
113 const aacMiddleware = Reflect.getMetadata("aac_middleware", ControllerClass.prototype) ?? {};
114 const validatonMiddleware = Reflect.getMetadata("validation_middleware", ControllerClass.prototype) ?? {};
115
116 if (metadata) {
117 for (const methodName in metadata) {
118 if (!metadata[methodName]) {
119 continue;
120 }
121
122 for (const method in metadata[methodName]!) {
123 const data = metadata[methodName][method as keyof RouteMetadata]!;
124
125 if (!data) {
126 continue;
127 }
128
129 const { middleware, handler, path } = data;
130
131 if (!handler) {
132 continue;
133 }
134
135 if (!path) {
136 logError(`[Server] No path specified at function ${handler.name} in controller ${file}. Skipping.`);
137 continue;
138 }
139
140 if (method && !["GET", "POST", "HEAD", "PUT", "PATCH", "DELETE"].includes(method)) {
141 logError(
142 `[Server] Invalid method '${method}' specified at function ${handler.name} in controller ${file}. Skipping.`
143 );
144 continue;
145 }
146
147 log(`Added handler for ${method?.toUpperCase() ?? "GET"} ${path}`);
148
149 const finalMiddlewareArray = [
150 ...(authMiddleware[methodName] ? [authMiddleware[methodName]] : []),
151 ...(gacMiddleware[methodName] ? [gacMiddleware[methodName]] : []),
152 ...(aacMiddleware[methodName] ? [aacMiddleware[methodName]] : []),
153 ...(validatonMiddleware[methodName] ? [validatonMiddleware[methodName]] : []),
154 ...(middleware ?? [])
155 ];
156
157 (router[(method?.toLowerCase() ?? "get") as keyof typeof router] as Function)(
158 path,
159 ...(finalMiddlewareArray?.map(
160 fn => (req: ExpressRequest, res: ExpressResponse, next: NextFunction) =>
161 fn(this.client, req, res, next)
162 ) ?? []),
163 async (req: ExpressRequest, res: ExpressResponse) => {
164 const userResponse = await handler.bind(controller)(req, res);
165
166 if (!res.headersSent) {
167 if (userResponse instanceof Response) {
168 userResponse.send(res);
169 } else if (userResponse && typeof userResponse === "object") {
170 res.json(userResponse);
171 } else if (typeof userResponse === "string") {
172 res.send(userResponse);
173 } else if (typeof userResponse === "number") {
174 res.send(userResponse.toString());
175 } else {
176 logWarn("Invalid value was returned from the controller. Not sending a response.");
177 }
178 }
179 }
180 );
181 }
182 }
183 }
184 }
185 }
186
187 async start() {
188 this.expressServer = this.expressApp.listen(this.port, () => logInfo(`API server is listening at port ${this.port}`));
189 }
190 }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26