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:
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" };
|
||||||
|
|||||||
@@ -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}`;
|
||||||
|
|||||||
@@ -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 {};
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|||||||
@@ -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 }) => {
|
||||||
|
|||||||
@@ -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" };
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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 }) => {
|
||||||
|
|||||||
@@ -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(
|
||||||
"/",
|
"/",
|
||||||
|
|||||||
Reference in New Issue
Block a user