Refactor middleware and routes to use onRequest for API key and admin key guards; update dependencies and improve logging for error handling

This commit is contained in:
Flavio Fois
2026-03-02 19:30:59 +01:00
parent 5761cbaa55
commit 3f15edae75
8 changed files with 72 additions and 58 deletions

View File

@@ -4,17 +4,18 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "bun run --watch src/index.ts", "dev": "bun run --watch src/index.ts",
"dev:wait": "bun run --watch src/wait-for-mysql.ts",
"start": "bun run src/index.ts", "start": "bun run src/index.ts",
"start:wait": "bun run src/wait-for-mysql.ts" "start:wait": "bun run src/wait-for-mysql.ts"
}, },
"dependencies": { "dependencies": {
"@node-rs/argon2": "^2.0.2", "@node-rs/argon2": "^2.0.2",
"elysia": "^1.2.0", "elysia": "^1.4.27",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"mysql2": "^3.11.0" "mysql2": "^3.18.2"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",
"typescript": "^5.0.0" "typescript": "^5.9.3"
} }
} }

View File

@@ -38,6 +38,7 @@ const app = new Elysia()
}) })
.onError(({ error, set, code }) => { .onError(({ error, set, code }) => {
console.error("Error processing request:", error); console.error("Error processing request:", error);
console.log(code)
if (code === "NOT_FOUND") { if (code === "NOT_FOUND") {
set.status = 404; set.status = 404;
return { success: false, message: "Not found" }; return { success: false, message: "Not found" };

View File

@@ -25,7 +25,19 @@ export function Log(source: string, ...args: unknown[]): void {
const date = now.toISOString().slice(0, 10); const date = now.toISOString().slice(0, 10);
const time = now.toTimeString().slice(0, 8); const time = now.toTimeString().slice(0, 8);
const msg = args const msg = args
.map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a))) .map((a) => {
if (a instanceof Error) {
return `${a.message}${a.stack ? "\n" + a.stack : ""}`;
}
if (typeof a === "object") {
try {
return JSON.stringify(a);
} catch {
return String(a);
}
}
return String(a);
})
.join(" "); .join(" ");
const line = `[${date}] - [${time}] - [${source}] - ${msg}`; const line = `[${date}] - [${time}] - [${source}] - ${msg}`;

View File

@@ -1,35 +1,35 @@
import { Elysia } from "elysia";
import { config } from "../config"; import { config } from "../config";
import { Log } from "../logger"; import { Log } from "../logger";
export const apiKeyGuard = new Elysia({ name: "api-key-guard" }).derive( // simple middleware functions that enforce API or admin keys
{ as: "scoped" }, export function apiKeyGuard(ctx: { request?: Request; set: any }) {
({ headers, error, request }) => { const request = ctx.request;
const key = headers["x-api-key"]; if (!request) return; // nothing to validate at setup time
const key = request.headers.get("x-api-key");
if (!key || key !== config.apiKey) { if (!key || key !== config.apiKey) {
const ip = const ip =
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
request.headers.get("x-real-ip") || request.headers.get("x-real-ip") ||
"unknown"; "unknown";
Log("AUTH", `Invalid API key from ip=${ip}`); Log("AUTH", `Invalid API key from ip=${ip}`);
return error(401, { success: false, message: "Invalid or missing API key" }); ctx.set.status = 401;
return { success: false, message: "Invalid or missing API key" };
} }
return {}; }
}
);
export const adminKeyGuard = new Elysia({ name: "admin-key-guard" }).derive( export function adminKeyGuard(ctx: { request?: Request; set: any }) {
{ as: "scoped" }, const request = ctx.request;
({ headers, error, request }) => { if (!request) return;
const key = headers["x-admin-key"];
const key = request.headers.get("x-admin-key");
if (!key || key !== config.adminKey) { if (!key || key !== config.adminKey) {
const ip = const ip =
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
request.headers.get("x-real-ip") || request.headers.get("x-real-ip") ||
"unknown"; "unknown";
Log("AUTH", `Invalid admin key from ip=${ip}`); Log("AUTH", `Invalid admin key from ip=${ip}`);
return error(401, { success: false, message: "Invalid or missing admin key" }); ctx.set.status = 401;
return { success: false, message: "Invalid or missing admin key" };
} }
return {}; }
}
);

View File

@@ -8,7 +8,7 @@ const excludedHwids = new Set<string>([
"95e025d1-7567-462e-9354-ac88b965cd22", "95e025d1-7567-462e-9354-ac88b965cd22",
]); ]);
export const hwidRateLimit = new Elysia({ name: "hwid-rate-limit" }).derive( export const hwidRateLimit = new Elysia({ name: "hwid-rate-limit" }).onBeforeHandle(
{ as: "scoped" }, { as: "scoped" },
// @ts-ignore // @ts-ignore
async ({ body, error }) => { async ({ body, error }) => {

View File

@@ -21,7 +21,7 @@ import { Log } from "../logger";
import type { BugReportStatus } from "../types"; import type { BugReportStatus } from "../types";
export const adminRoutes = new Elysia({ prefix: "/api/admin" }) export const adminRoutes = new Elysia({ prefix: "/api/admin" })
.use(adminKeyGuard) .onRequest(adminKeyGuard)
.get( .get(
"/bug-reports", "/bug-reports",
async ({ query }) => { async ({ query }) => {
@@ -60,10 +60,10 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" })
) )
.get( .get(
"/bug-reports/:id", "/bug-reports/:id",
async ({ params, error }) => { async ({ params, status }) => {
Log("ADMIN", `Get bug report id=${params.id}`); Log("ADMIN", `Get bug report id=${params.id}`);
const result = await getBugReport(parseInt(params.id)); const result = await getBugReport(parseInt(params.id));
if (!result) return error(404, { success: false, message: "Report not found" }); if (!result) return status(404, { success: false, message: "Report not found" });
return result; return result;
}, },
{ {
@@ -73,14 +73,14 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" })
) )
.patch( .patch(
"/bug-reports/:id/status", "/bug-reports/:id/status",
async ({ params, body, error }) => { async ({ params, body, status }) => {
Log("ADMIN", `Update status id=${params.id} status=${body.status}`); Log("ADMIN", `Update status id=${params.id} status=${body.status}`);
const updated = await updateBugReportStatus( const updated = await updateBugReportStatus(
parseInt(params.id), parseInt(params.id),
body.status body.status
); );
if (!updated) if (!updated)
return error(404, { success: false, message: "Report not found" }); return status(404, { success: false, message: "Report not found" });
return { success: true, message: "Status updated" }; return { success: true, message: "Status updated" };
}, },
{ {
@@ -98,10 +98,10 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" })
) )
.get( .get(
"/bug-reports/:id/files/:fileId", "/bug-reports/:id/files/:fileId",
async ({ params, error, set }) => { async ({ params, status, set }) => {
const file = await getFile(parseInt(params.id), parseInt(params.fileId)); const file = await getFile(parseInt(params.id), parseInt(params.fileId));
if (!file) if (!file)
return error(404, { success: false, message: "File not found" }); return status(404, { success: false, message: "File not found" });
set.headers["content-type"] = file.mime_type; set.headers["content-type"] = file.mime_type;
set.headers["content-disposition"] = set.headers["content-disposition"] =
@@ -115,11 +115,11 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" })
) )
.get( .get(
"/bug-reports/:id/download", "/bug-reports/:id/download",
async ({ params, error, set }) => { async ({ params, status, set }) => {
Log("ADMIN", `Download zip for report id=${params.id}`); Log("ADMIN", `Download zip for report id=${params.id}`);
const zipBuffer = await generateReportZip(parseInt(params.id)); const zipBuffer = await generateReportZip(parseInt(params.id));
if (!zipBuffer) if (!zipBuffer)
return error(404, { success: false, message: "Report not found" }); return status(404, { success: false, message: "Report not found" });
set.headers["content-type"] = "application/zip"; set.headers["content-type"] = "application/zip";
set.headers["content-disposition"] = set.headers["content-disposition"] =
@@ -134,11 +134,11 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" })
) )
.delete( .delete(
"/bug-reports/:id", "/bug-reports/:id",
async ({ params, error }) => { async ({ params, status }) => {
Log("ADMIN", `Delete bug report id=${params.id}`); Log("ADMIN", `Delete bug report id=${params.id}`);
const deleted = await deleteBugReport(parseInt(params.id)); const deleted = await deleteBugReport(parseInt(params.id));
if (!deleted) if (!deleted)
return error(404, { success: false, message: "Report not found" }); return status(404, { success: false, message: "Report not found" });
return { success: true, message: "Report deleted" }; return { success: true, message: "Report deleted" };
}, },
{ {
@@ -157,14 +157,14 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" })
) )
.post( .post(
"/users", "/users",
async ({ body, error }) => { async ({ body, status }) => {
Log("ADMIN", `Create user username=${body.username}`); Log("ADMIN", `Create user username=${body.username}`);
try { try {
const user = await createUser(body); const user = await createUser(body);
return { success: true, user }; return { success: true, user };
} catch (err) { } catch (err) {
if (err instanceof Error && err.message === "Username already exists") { if (err instanceof Error && err.message === "Username already exists") {
return error(409, { success: false, message: err.message }); return status(409, { success: false, message: err.message });
} }
throw err; throw err;
} }
@@ -181,11 +181,11 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" })
) )
.patch( .patch(
"/users/:id", "/users/:id",
async ({ params, body, error }) => { async ({ params, body, status }) => {
Log("ADMIN", `Update user id=${params.id}`); Log("ADMIN", `Update user id=${params.id}`);
const updated = await updateUser(params.id, body); const updated = await updateUser(params.id, body);
if (!updated) if (!updated)
return error(404, { success: false, message: "User not found" }); return status(404, { success: false, message: "User not found" });
return { success: true, message: "User updated" }; return { success: true, message: "User updated" };
}, },
{ {
@@ -199,11 +199,11 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" })
) )
.post( .post(
"/users/:id/reset-password", "/users/:id/reset-password",
async ({ params, body, error }) => { async ({ params, body, status }) => {
Log("ADMIN", `Reset password for user id=${params.id}`); Log("ADMIN", `Reset password for user id=${params.id}`);
const updated = await resetPassword(params.id, body.password); const updated = await resetPassword(params.id, body.password);
if (!updated) if (!updated)
return error(404, { success: false, message: "User not found" }); return status(404, { success: false, message: "User not found" });
return { success: true, message: "Password reset" }; return { success: true, message: "Password reset" };
}, },
{ {
@@ -214,18 +214,18 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" })
) )
.delete( .delete(
"/users/:id", "/users/:id",
async ({ params, error }) => { async ({ params, status }) => {
Log("ADMIN", `Delete user id=${params.id}`); Log("ADMIN", `Delete user id=${params.id}`);
const user = await getUserById(params.id); const user = await getUserById(params.id);
if (!user) if (!user)
return error(404, { success: false, message: "User not found" }); throw status(404, { success: false, message: "User not found" });
if (user.role === "admin") if (user.role === "admin")
return error(400, { success: false, message: "Cannot delete an admin user" }); return status(400, { success: false, message: "Cannot delete an admin user" });
const deleted = await deleteUser(params.id); const deleted = await deleteUser(params.id);
if (!deleted) if (!deleted)
return error(404, { success: false, message: "User not found" }); return status(404, { success: false, message: "User not found" });
return { success: true, message: "User deleted" }; return { success: true, message: "User deleted" };
}, },
{ {

View File

@@ -4,7 +4,7 @@ import { loginUser, validateSession, logoutSession } from "../services/authServi
import { Log } from "../logger"; import { Log } from "../logger";
export const authRoutes = new Elysia({ prefix: "/api/admin/auth" }) export const authRoutes = new Elysia({ prefix: "/api/admin/auth" })
.use(adminKeyGuard) .onRequest(adminKeyGuard)
.post( .post(
"/login", "/login",
async ({ body, error }) => { async ({ body, error }) => {

View File

@@ -13,7 +13,7 @@ const FILE_ROLES: { field: string; role: FileRole; mime: string }[] = [
]; ];
export const bugReportRoutes = new Elysia({ prefix: "/api/bug-reports" }) export const bugReportRoutes = new Elysia({ prefix: "/api/bug-reports" })
.use(apiKeyGuard) .onRequest(apiKeyGuard)
.use(hwidRateLimit) .use(hwidRateLimit)
.post( .post(
"/", "/",