Enhances API infrastructure with Swagger, feature flags, and refactored middleware

Implements @elysiajs/swagger for automated API documentation and introduces a feature flag system to expose service capabilities based on environment variables.

Refactors authentication guards into native Elysia scoped middleware for improved integration and type safety. Updates error handling to support custom status codes and adds instance-specific headers to responses for better observability.

Includes an IP fallback mechanism for bug reports that utilizes internal system info when the direct submitter IP is unavailable.
This commit is contained in:
Flavio Fois
2026-03-17 10:30:42 +01:00
parent 9458d1e8ad
commit 5624019f23
13 changed files with 149 additions and 49 deletions

View File

@@ -30,7 +30,7 @@ RATE_LIMIT_MAX=5
RATE_LIMIT_WINDOW_HOURS=24
# Test DB flag
ENABLE_TEST_DB = false
FLAG_ENABLE_TEST_DB = false
# Cloudflare Tunnel
CLOUDFLARE_TUNNEL_TOKEN=change_me_cloudflare_tunnel_token

View File

@@ -9,6 +9,7 @@
"start:wait": "bun run src/wait-for-mysql.ts"
},
"dependencies": {
"@elysiajs/swagger": "^1.3.1",
"@node-rs/argon2": "^2.0.2",
"elysia": "^1.4.27",
"jszip": "^3.10.1",

View File

@@ -20,7 +20,7 @@ export const config = {
max: parseInt(process.env.RATE_LIMIT_MAX || "5"),
windowHours: parseInt(process.env.RATE_LIMIT_WINDOW_HOURS || "24"),
},
enableTestDB: process.env.ENABLE_TEST_DB === "true",
enableTestDB: process.env.FLAG_ENABLE_TEST_DB === "true",
} as const;
// Validate required config on startup

View File

@@ -7,7 +7,7 @@ let pool: mysql.Pool | null = null;
export function getPool(useTestDb?: boolean): mysql.Pool {
if (!pool) {
if (useTestDb && config.enableTestDB) {
Log("db", "using test db");
Log("DB", "Using Test DB Pool Connection");
return mysql.createPool({
host: config.testing_mysql.host,
port: config.testing_mysql.port,
@@ -33,7 +33,7 @@ export function getPool(useTestDb?: boolean): mysql.Pool {
});
}
if (useTestDb && config.enableTestDB) {
Log("db", "using test db");
Log("DB", "Using Test DB Pool Connection");
return mysql.createPool({
host: config.testing_mysql.host,
port: config.testing_mysql.port,

7
src/features.json Normal file
View File

@@ -0,0 +1,7 @@
{
"testDb": {
"label": "Testing Database",
"description": "Accepts bug reports routed to a separate testing database via the x-db-env header",
"key": "FLAG_ENABLE_TEST_DB"
}
}

View File

@@ -1,11 +1,14 @@
import { Elysia } from "elysia";
import { swagger } from "@elysiajs/swagger";
import { config, validateConfig } from "./config";
import { runMigrations } from "./db/migrate";
import { closePool } from "./db/connection";
import { bugReportRoutes } from "./routes/bugReports";
import { adminRoutes } from "./routes/admin";
import { authRoutes } from "./routes/auth";
import { featuresRoutes } from "./routes/features";
import { initLogger, Log } from "./logger";
import { adminKeyGuard2 } from "./middleware/auth";
const INSTANCE_ID =
process.env.HOSTNAME + "_" + Math.random().toString(16).slice(2, 6);
@@ -14,7 +17,12 @@ const INSTANCE_ID =
initLogger();
// Validate environment
try {
validateConfig();
} catch (error) {
Log("ERROR", "Failed to validate config:", error);
process.exit(1);
}
// Run database migrations
try {
@@ -25,8 +33,9 @@ try {
}
const app = new Elysia()
.onRequest(({ request }) => {
.onRequest(({ request, set }) => {
const url = new URL(request.url);
const ua = request.headers.get("user-agent") ?? "unknown";
const ip =
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
request.headers.get("x-real-ip") ||
@@ -36,6 +45,8 @@ const app = new Elysia()
"HTTP",
`[${INSTANCE_ID}] ${request.method} ${url.pathname} from ${ip}`,
);
set.headers["x-instance-id"] = INSTANCE_ID;
set.headers["x-server"] = "EMLy-API";
})
.onAfterResponse(({ request, set }) => {
const url = new URL(request.url);
@@ -53,6 +64,10 @@ const app = new Elysia()
set.status = 422;
return { success: false, message: "Validation error" };
}
if (typeof code === "number") {
set.status = code;
return (error as any).response;
}
Log("ERROR", "Unhandled error:", error);
set.status = 500;
return { success: false, message: "Internal server error" };
@@ -63,6 +78,29 @@ const app = new Elysia()
timestamp: new Date().toISOString(),
}))
.get("/", () => ({ status: "ok", message: "API is running" }))
.use(
new Elysia().use(adminKeyGuard2).use(
swagger({
path: "/swagger",
documentation: {
info: { title: "EMLy Bug Report API", version: "1.0.0" },
tags: [
{ name: "Bug Reports", description: "Submit bug reports" },
{ name: "Auth", description: "Admin authentication" },
{ name: "Admin", description: "Admin bug report management" },
{ name: "Features", description: "Feature flags" },
],
components: {
securitySchemes: {
apiKey: { type: "apiKey", in: "header", name: "x-api-key" },
adminKey: { type: "apiKey", in: "header", name: "x-admin-key" },
},
},
},
}),
),
)
.use(featuresRoutes)
.use(bugReportRoutes)
.use(authRoutes)
.use(adminRoutes)

View File

@@ -1,40 +1,26 @@
import { config } from "../config";
import { Log } from "../logger";
import Elysia from "elysia";
import type { UnauthorizedResponse } from "../types";
// 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
if (request.url.includes("/health")) 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-API-KEYGUARD", `Invalid API key from ip=${ip}`);
ctx.set.status = 401;
return { success: false, message: "Invalid or missing API key" };
}
export const apiKeyGuard2 = new Elysia({ name: "api-key-guard" }).derive(
{ as: "scoped" },
({ headers, status }): UnauthorizedResponse | {} => {
const apiKey = headers["x-api-key"];
if (!apiKey || apiKey !== config.apiKey) {
throw status(401, { success: false as const, message: "Unauthorized API Key" });
}
return {};
},
);
export function adminKeyGuard(ctx: { request?: Request; set: any }) {
const request = ctx.request;
if (!request) return;
if (request.url.includes("/health")) return;
if (request.url.includes("/bug-reports")) 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-ADMIN-KEYGUARD", `Invalid admin key from ip=${ip}`);
ctx.set.status = 401;
return { success: false, message: "Invalid or missing admin key" };
}
export const adminKeyGuard2 = new Elysia({ name: "admin-key-guard" }).derive(
{ as: "scoped" },
({ headers, status }): UnauthorizedResponse | {} => {
const apiKey = headers["x-admin-key"];
if (!apiKey || apiKey !== config.adminKey) {
throw status(401, { success: false as const, message: "Unauthorized Admin Key" });
}
return {};
},
);

View File

@@ -1,5 +1,5 @@
import { Elysia, t } from "elysia";
import { adminKeyGuard } from "../middleware/auth";
import { adminKeyGuard2 } from "../middleware/auth";
import {
listBugReports,
getBugReport,
@@ -21,7 +21,7 @@ import { Log } from "../logger";
import type { BugReportStatus, DbEnv } from "../types";
export const adminRoutes = new Elysia({ prefix: "/api/admin" })
.onRequest(adminKeyGuard)
.use(adminKeyGuard2)
.get(
"/bug-reports",
async ({ query, headers }) => {
@@ -37,7 +37,7 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" })
"ADMIN",
`List bug reports page=${page} pageSize=${pageSize} status=${status || "all"} search=${search || ""}`,
);
return await listBugReports(
const res = await listBugReports(
{
page,
pageSize,
@@ -46,6 +46,7 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" })
},
useTestDb,
);
return res;
},
{
query: t.Object({

View File

@@ -1,5 +1,5 @@
import { Elysia, t } from "elysia";
import { adminKeyGuard } from "../middleware/auth";
import { adminKeyGuard2 } from "../middleware/auth";
import {
loginUser,
validateSession,
@@ -8,7 +8,8 @@ import {
import { Log } from "../logger";
export const authRoutes = new Elysia({ prefix: "/api/admin/auth" })
.onRequest(adminKeyGuard)
//.onRequest(adminKeyGuard)
.use(adminKeyGuard2)
.post(
"/login",
async ({ body, status }) => {

View File

@@ -1,5 +1,5 @@
import { Elysia, t } from "elysia";
import { apiKeyGuard } from "../middleware/auth";
import { apiKeyGuard2, adminKeyGuard2 } from "../middleware/auth";
import { hwidRateLimit } from "../middleware/rateLimit";
import { createBugReport, addFile } from "../services/bugReportService";
import { Log } from "../logger";
@@ -13,7 +13,8 @@ const FILE_ROLES: { field: string; role: FileRole; mime: string }[] = [
];
export const bugReportRoutes = new Elysia({ prefix: "/api/bug-reports" })
.onRequest(apiKeyGuard)
//.onRequest(apiKeyGuard)
.use(apiKeyGuard2)
//.use(hwidRateLimit)
.post(
"/",

40
src/routes/features.ts Normal file
View File

@@ -0,0 +1,40 @@
import { Elysia, t } from "elysia";
import { readFileSync } from "fs";
import { join } from "path";
import { FeaturesJson, FeaturesRawJson } from "../types";
import { apiKeyGuard2 } from "../middleware/auth";
const featuresPath = join(import.meta.dir, "../features.json");
const FeatureSchema = t.Object({
label: t.String(),
description: t.String(),
enabled: t.Boolean(),
});
export const featuresRoutes = new Elysia({ prefix: "/api/features" })
.use(apiKeyGuard2)
.get(
"/",
() => {
const raw = readFileSync(featuresPath, "utf-8");
const jsonData: FeaturesRawJson = JSON.parse(raw);
const returnData: FeaturesJson = {};
for (const key in jsonData) {
// Try to log the feature flag value from the .env
const envKey = jsonData[key].key;
const envValue = process.env[envKey];
if (envValue !== undefined) {
returnData[key] = { ...jsonData[key], enabled: envValue === "true" };
}
}
return returnData as Record<
string,
{ label: string; description: string; enabled: boolean }
>;
},
{
response: t.Record(t.String(), FeatureSchema),
detail: { summary: "Get available features and their enabled state" },
},
);

View File

@@ -207,6 +207,18 @@ export async function getBugReport(
"SELECT id, report_id, file_role, filename, mime_type, file_size, created_at FROM bug_report_files WHERE report_id = ?",
[id],
);
// If the report's submitter_ip is "unknown", use the report system_info's InternalIP if available
const report = reportRows[0] as BugReport;
if (
report.submitter_ip === "unknown" &&
report.system_info !== null &&
typeof report.system_info === "object" &&
"InternalIP" in report.system_info
) {
report.submitter_ip = report.system_info.InternalIP as string;
}
console.log("Fetched report:", reportRows[0]);
return {
report: reportRows[0] as BugReport,

View File

@@ -57,3 +57,16 @@ export interface PaginatedResponse<T> {
}
export type DbEnv = "prod" | "test";
export type UnauthorizedResponse = import("elysia").ElysiaCustomStatusResponse<
401,
{ success: false; message: string }
>;
export interface FeaturesRawJson {
[key: string]: { label: string; description: string; key: string };
}
export interface FeaturesJson {
[key: string]: { label: string; description: string; enabled: boolean };
}