diff --git a/package.json b/package.json index c6d939b..ebcafd3 100644 --- a/package.json +++ b/package.json @@ -4,17 +4,18 @@ "private": true, "scripts": { "dev": "bun run --watch src/index.ts", + "dev:wait": "bun run --watch src/wait-for-mysql.ts", "start": "bun run src/index.ts", "start:wait": "bun run src/wait-for-mysql.ts" }, "dependencies": { "@node-rs/argon2": "^2.0.2", - "elysia": "^1.2.0", + "elysia": "^1.4.27", "jszip": "^3.10.1", - "mysql2": "^3.11.0" + "mysql2": "^3.18.2" }, "devDependencies": { "@types/bun": "latest", - "typescript": "^5.0.0" + "typescript": "^5.9.3" } } diff --git a/src/index.ts b/src/index.ts index e16fcee..bc67232 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,6 +38,7 @@ const app = new Elysia() }) .onError(({ error, set, code }) => { console.error("Error processing request:", error); + console.log(code) if (code === "NOT_FOUND") { set.status = 404; return { success: false, message: "Not found" }; diff --git a/src/logger.ts b/src/logger.ts index bc953a4..0aa489d 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -25,7 +25,19 @@ export function Log(source: string, ...args: unknown[]): void { const date = now.toISOString().slice(0, 10); const time = now.toTimeString().slice(0, 8); 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(" "); const line = `[${date}] - [${time}] - [${source}] - ${msg}`; diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index df1e2b6..0a36488 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -1,35 +1,35 @@ -import { Elysia } from "elysia"; import { config } from "../config"; import { Log } from "../logger"; -export const apiKeyGuard = new Elysia({ name: "api-key-guard" }).derive( - { as: "scoped" }, - ({ headers, error, request }) => { - const key = headers["x-api-key"]; - if (!key || key !== config.apiKey) { - const ip = - request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || - request.headers.get("x-real-ip") || - "unknown"; - Log("AUTH", `Invalid API key from ip=${ip}`); - return error(401, { success: false, message: "Invalid or missing API key" }); - } - return {}; - } -); +// simple middleware functions that enforce API or admin keys +export function apiKeyGuard(ctx: { request?: Request; set: any }) { + const request = ctx.request; + if (!request) return; // nothing to validate at setup time -export const adminKeyGuard = new Elysia({ name: "admin-key-guard" }).derive( - { as: "scoped" }, - ({ headers, error, request }) => { - const key = headers["x-admin-key"]; - if (!key || key !== config.adminKey) { - const ip = - request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || - request.headers.get("x-real-ip") || - "unknown"; - Log("AUTH", `Invalid admin key from ip=${ip}`); - return error(401, { success: false, message: "Invalid or missing admin key" }); - } - return {}; + const key = request.headers.get("x-api-key"); + if (!key || key !== config.apiKey) { + const ip = + request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || + request.headers.get("x-real-ip") || + "unknown"; + Log("AUTH", `Invalid API key from ip=${ip}`); + ctx.set.status = 401; + return { success: false, message: "Invalid or missing API key" }; } -); +} + +export function adminKeyGuard(ctx: { request?: Request; set: any }) { + const request = ctx.request; + if (!request) return; + + const key = request.headers.get("x-admin-key"); + if (!key || key !== config.adminKey) { + const ip = + request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || + request.headers.get("x-real-ip") || + "unknown"; + Log("AUTH", `Invalid admin key from ip=${ip}`); + ctx.set.status = 401; + return { success: false, message: "Invalid or missing admin key" }; + } +} diff --git a/src/middleware/rateLimit.ts b/src/middleware/rateLimit.ts index 70921d8..cb16ed7 100644 --- a/src/middleware/rateLimit.ts +++ b/src/middleware/rateLimit.ts @@ -8,7 +8,7 @@ const excludedHwids = new Set([ "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" }, // @ts-ignore async ({ body, error }) => { diff --git a/src/routes/admin.ts b/src/routes/admin.ts index 94b6a99..4b6461c 100644 --- a/src/routes/admin.ts +++ b/src/routes/admin.ts @@ -21,7 +21,7 @@ import { Log } from "../logger"; import type { BugReportStatus } from "../types"; export const adminRoutes = new Elysia({ prefix: "/api/admin" }) - .use(adminKeyGuard) + .onRequest(adminKeyGuard) .get( "/bug-reports", async ({ query }) => { @@ -60,10 +60,10 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" }) ) .get( "/bug-reports/:id", - async ({ params, error }) => { + async ({ params, status }) => { Log("ADMIN", `Get bug report id=${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; }, { @@ -73,14 +73,14 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" }) ) .patch( "/bug-reports/:id/status", - async ({ params, body, error }) => { + async ({ params, body, status }) => { Log("ADMIN", `Update status id=${params.id} status=${body.status}`); const updated = await updateBugReportStatus( parseInt(params.id), body.status ); 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" }; }, { @@ -98,10 +98,10 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" }) ) .get( "/bug-reports/:id/files/:fileId", - async ({ params, error, set }) => { + async ({ params, status, set }) => { const file = await getFile(parseInt(params.id), parseInt(params.fileId)); 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-disposition"] = @@ -115,11 +115,11 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" }) ) .get( "/bug-reports/:id/download", - async ({ params, error, set }) => { + async ({ params, status, set }) => { Log("ADMIN", `Download zip for report id=${params.id}`); const zipBuffer = await generateReportZip(parseInt(params.id)); 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-disposition"] = @@ -134,11 +134,11 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" }) ) .delete( "/bug-reports/:id", - async ({ params, error }) => { + async ({ params, status }) => { Log("ADMIN", `Delete bug report id=${params.id}`); const deleted = await deleteBugReport(parseInt(params.id)); 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" }; }, { @@ -157,14 +157,14 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" }) ) .post( "/users", - async ({ body, error }) => { + async ({ body, status }) => { Log("ADMIN", `Create user username=${body.username}`); try { const user = await createUser(body); return { success: true, user }; } catch (err) { 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; } @@ -181,11 +181,11 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" }) ) .patch( "/users/:id", - async ({ params, body, error }) => { + async ({ params, body, status }) => { Log("ADMIN", `Update user id=${params.id}`); const updated = await updateUser(params.id, body); 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" }; }, { @@ -199,11 +199,11 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" }) ) .post( "/users/:id/reset-password", - async ({ params, body, error }) => { + async ({ params, body, status }) => { Log("ADMIN", `Reset password for user id=${params.id}`); const updated = await resetPassword(params.id, body.password); 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" }; }, { @@ -214,18 +214,18 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" }) ) .delete( "/users/:id", - async ({ params, error }) => { + async ({ params, status }) => { Log("ADMIN", `Delete user id=${params.id}`); const user = await getUserById(params.id); if (!user) - return error(404, { success: false, message: "User not found" }); + throw status(404, { success: false, message: "User not found" }); 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); 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" }; }, { diff --git a/src/routes/auth.ts b/src/routes/auth.ts index 6ce68a7..4e85aea 100644 --- a/src/routes/auth.ts +++ b/src/routes/auth.ts @@ -4,7 +4,7 @@ import { loginUser, validateSession, logoutSession } from "../services/authServi import { Log } from "../logger"; export const authRoutes = new Elysia({ prefix: "/api/admin/auth" }) - .use(adminKeyGuard) + .onRequest(adminKeyGuard) .post( "/login", async ({ body, error }) => { diff --git a/src/routes/bugReports.ts b/src/routes/bugReports.ts index 23fb604..0b05ce3 100644 --- a/src/routes/bugReports.ts +++ b/src/routes/bugReports.ts @@ -13,7 +13,7 @@ const FILE_ROLES: { field: string; role: FileRole; mime: string }[] = [ ]; export const bugReportRoutes = new Elysia({ prefix: "/api/bug-reports" }) - .use(apiKeyGuard) + .onRequest(apiKeyGuard) .use(hwidRateLimit) .post( "/",