backend/src/server.ts
creations 93ed37b3e9
All checks were successful
Code quality checks / biome (push) Successful in 10s
- better reading of avalable routes
- add name validation for channel names and catagories
- add create, delete, move for channels
2025-05-11 10:51:48 -04:00

242 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { resolve } from "node:path";
import { environment } from "@config";
import { logger } from "@creations.works/logger";
import { jsonResponse } from "@lib/http";
import {
type BunFile,
FileSystemRouter,
type MatchedRoute,
type Serve,
} from "bun";
import { webSocketHandler } from "@/websocket";
import { getSession } from "@lib/jwt";
class ServerHandler {
private router: FileSystemRouter;
constructor(
private port: number,
private host: string,
) {
this.router = new FileSystemRouter({
style: "nextjs",
dir: "./src/routes",
fileExtensions: [".ts"],
origin: `http://${this.host}:${this.port}`,
});
}
public initialize(): void {
const server: Serve = Bun.serve({
port: this.port,
hostname: this.host,
fetch: this.handleRequest.bind(this),
websocket: {
open: webSocketHandler.handleOpen.bind(webSocketHandler),
message: webSocketHandler.handleMessage.bind(webSocketHandler),
close: webSocketHandler.handleClose.bind(webSocketHandler),
},
});
logger.info(`Server running at http://${server.hostname}:${server.port}`, {
breakLine: true,
});
this.logRoutes();
}
private logRoutes(): void {
logger.info("Available routes:");
const sortedRoutes: [string, string][] = Object.entries(
this.router.routes,
).sort(([pathA], [pathB]) => pathA.localeCompare(pathB));
let lastCategory: string | null = null;
for (const [path, filePath] of sortedRoutes) {
const parts = path.split("/").filter(Boolean);
const category = parts.length === 0 ? "" : parts[0];
if (category !== lastCategory) {
if (lastCategory !== null) logger.space();
logger.info(` ${category}/`);
lastCategory = category;
}
logger.info(` Route: ${path}, File: ${filePath}`);
}
}
private async serveStaticFile(pathname: string): Promise<Response> {
try {
let filePath: string;
if (pathname === "/favicon.ico") {
filePath = resolve("public", "assets", "favicon.ico");
} else {
filePath = resolve(`.${pathname}`);
}
const file: BunFile = Bun.file(filePath);
if (await file.exists()) {
const fileContent: ArrayBuffer = await file.arrayBuffer();
const contentType: string = file.type || "application/octet-stream";
return new Response(fileContent, {
headers: { "Content-Type": contentType },
});
}
logger.warn(`File not found: ${filePath}`);
return new Response("Not Found", { status: 404 });
} catch (error) {
logger.error([`Error serving static file: ${pathname}`, error as Error]);
return new Response("Internal Server Error", { status: 500 });
}
}
private async handleRequest(
request: Request,
server: BunServer,
): Promise<Response> {
const extendedRequest: ExtendedRequest = request as ExtendedRequest;
extendedRequest.startPerf = performance.now();
const pathname: string = new URL(request.url).pathname;
if (pathname.startsWith("/public") || pathname === "/favicon.ico") {
return await this.serveStaticFile(pathname);
}
const match: MatchedRoute | null = this.router.match(request);
let requestBody: unknown = {};
let response: Response;
if (match) {
const { filePath, params, query } = match;
try {
const routeModule: RouteModule = await import(filePath);
const contentType: string | null = request.headers.get("Content-Type");
const actualContentType: string | null = contentType
? contentType.split(";")[0].trim()
: null;
if (
routeModule.routeDef.needsBody === "json" &&
actualContentType === "application/json"
) {
try {
requestBody = await request.json();
} catch {
requestBody = {};
}
} else if (
routeModule.routeDef.needsBody === "multipart" &&
actualContentType === "multipart/form-data"
) {
try {
requestBody = await request.formData();
} catch {
requestBody = {};
}
}
if (
(Array.isArray(routeModule.routeDef.method) &&
!routeModule.routeDef.method.includes(request.method)) ||
(!Array.isArray(routeModule.routeDef.method) &&
routeModule.routeDef.method !== request.method)
) {
response = jsonResponse(405, {
error: `Method ${request.method} Not Allowed, expected ${
Array.isArray(routeModule.routeDef.method)
? routeModule.routeDef.method.join(", ")
: routeModule.routeDef.method
}`,
});
} else {
const expectedContentType: string | string[] | null =
routeModule.routeDef.accepts;
let matchesAccepts: boolean;
if (Array.isArray(expectedContentType)) {
matchesAccepts =
expectedContentType.includes("*/*") ||
expectedContentType.includes(actualContentType || "");
} else {
matchesAccepts =
expectedContentType === "*/*" ||
actualContentType === expectedContentType;
}
if (!matchesAccepts) {
response = jsonResponse(406, {
error: `Content-Type ${actualContentType} Not Acceptable, expected ${
Array.isArray(expectedContentType)
? expectedContentType.join(", ")
: expectedContentType
}`,
});
} else {
extendedRequest.params = params;
extendedRequest.query = query;
extendedRequest.session = (await getSession(request)) || null;
response = await routeModule.handler(
extendedRequest,
requestBody,
server,
);
if (routeModule.routeDef.returns !== "*/*") {
response.headers.set(
"Content-Type",
routeModule.routeDef.returns,
);
}
}
}
} catch (error: unknown) {
logger.error([`Error handling route ${request.url}:`, error as Error]);
response = jsonResponse(500);
}
} else {
response = jsonResponse(404);
}
const headers = request.headers;
let ip = server.requestIP(request)?.address;
if (!ip || ip.startsWith("172.") || ip === "127.0.0.1") {
ip =
headers.get("CF-Connecting-IP")?.trim() ||
headers.get("X-Real-IP")?.trim() ||
headers.get("X-Forwarded-For")?.split(",")[0].trim() ||
"unknown";
}
logger.custom(
`[${request.method}]`,
`(${response.status})`,
[
request.url,
`${(performance.now() - extendedRequest.startPerf).toFixed(2)}ms`,
ip || "unknown",
],
"90",
);
return response;
}
}
const serverHandler: ServerHandler = new ServerHandler(
environment.port,
environment.host,
);
export { serverHandler };