forked from syntaxbullet/AuroraBot-discord
fix: address security review findings, implement real cache clearing, and fix lifecycle promises
This commit is contained in:
@@ -10,6 +10,14 @@ interface ControlPanelProps {
|
||||
maintenanceMode: boolean;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
AURORA_ENV?: {
|
||||
ADMIN_TOKEN: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ControlPanel component provides quick administrative actions for the bot.
|
||||
* Integrated with the premium glassmorphic theme.
|
||||
@@ -20,12 +28,16 @@ export function ControlPanel({ maintenanceMode }: ControlPanelProps) {
|
||||
/**
|
||||
* Handles triggering an administrative action via the API
|
||||
*/
|
||||
const handleAction = async (action: string, payload?: any) => {
|
||||
const handleAction = async (action: string, payload?: Record<string, unknown>) => {
|
||||
setLoading(action);
|
||||
try {
|
||||
const token = window.AURORA_ENV?.ADMIN_TOKEN;
|
||||
const response = await fetch(`/api/actions/${action}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`
|
||||
},
|
||||
body: payload ? JSON.stringify(payload) : undefined,
|
||||
});
|
||||
if (!response.ok) throw new Error(`Action ${action} failed`);
|
||||
@@ -88,8 +100,8 @@ export function ControlPanel({ maintenanceMode }: ControlPanelProps) {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={`glass flex items-center justify-between h-auto py-4 px-5 border-white/10 transition-all group/maint ${maintenanceMode
|
||||
? 'bg-red-500/10 border-red-500/50 hover:bg-red-500/20'
|
||||
: 'hover:border-yellow-500/50 hover:bg-yellow-500/5'
|
||||
? 'bg-red-500/10 border-red-500/50 hover:bg-red-500/20'
|
||||
: 'hover:border-yellow-500/50 hover:bg-yellow-500/5'
|
||||
}`}
|
||||
onClick={() => handleAction("maintenance-mode", { enabled: !maintenanceMode, reason: "Dashboard toggle" })}
|
||||
disabled={!!loading}
|
||||
|
||||
@@ -51,18 +51,7 @@ mock.module("../../bot/lib/clientStats", () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
// 3. Mock System Events
|
||||
mock.module("@shared/lib/events", () => ({
|
||||
systemEvents: {
|
||||
on: mock(() => { }),
|
||||
emit: mock(() => { }),
|
||||
},
|
||||
EVENTS: {
|
||||
DASHBOARD: {
|
||||
NEW_EVENT: "dashboard:new_event",
|
||||
}
|
||||
}
|
||||
}));
|
||||
// 3. System Events (No mock needed, use real events)
|
||||
|
||||
describe("WebServer Security & Limits", () => {
|
||||
const port = 3001;
|
||||
@@ -110,4 +99,45 @@ describe("WebServer Security & Limits", () => {
|
||||
const data = (await response.json()) as { status: string };
|
||||
expect(data.status).toBe("ok");
|
||||
});
|
||||
|
||||
describe("Administrative Actions Authorization", () => {
|
||||
test("should reject administrative actions without token", async () => {
|
||||
const response = await fetch(`http://localhost:${port}/api/actions/reload-commands`, {
|
||||
method: "POST"
|
||||
});
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
|
||||
test("should reject administrative actions with invalid token", async () => {
|
||||
const response = await fetch(`http://localhost:${port}/api/actions/reload-commands`, {
|
||||
method: "POST",
|
||||
headers: { "Authorization": "Bearer wrong-token" }
|
||||
});
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
|
||||
test("should accept administrative actions with valid token", async () => {
|
||||
const { env } = await import("@shared/lib/env");
|
||||
const response = await fetch(`http://localhost:${port}/api/actions/reload-commands`, {
|
||||
method: "POST",
|
||||
headers: { "Authorization": `Bearer ${env.ADMIN_TOKEN}` }
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
|
||||
test("should reject maintenance mode with invalid payload", async () => {
|
||||
const { env } = await import("@shared/lib/env");
|
||||
const response = await fetch(`http://localhost:${port}/api/actions/maintenance-mode`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${env.ADMIN_TOKEN}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ not_enabled: true }) // Wrong field
|
||||
});
|
||||
expect(response.status).toBe(400);
|
||||
const data = await response.json() as { error: string };
|
||||
expect(data.error).toBe("Invalid payload");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -100,7 +100,16 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
|
||||
// Administrative Actions
|
||||
if (url.pathname.startsWith("/api/actions/") && req.method === "POST") {
|
||||
try {
|
||||
// Security Check: Token-based authentication
|
||||
const { env } = await import("@shared/lib/env");
|
||||
const authHeader = req.headers.get("Authorization");
|
||||
if (authHeader !== `Bearer ${env.ADMIN_TOKEN}`) {
|
||||
console.warn(`⚠️ [API] Unauthorized administrative action attempt from ${req.headers.get("x-forwarded-for") || "unknown"}`);
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
const { actionService } = await import("@shared/modules/admin/action.service");
|
||||
const { MaintenanceModeSchema } = await import("@shared/modules/dashboard/dashboard.types");
|
||||
|
||||
if (url.pathname === "/api/actions/reload-commands") {
|
||||
const result = await actionService.reloadCommands();
|
||||
@@ -113,8 +122,14 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/actions/maintenance-mode") {
|
||||
const body = await req.json() as { enabled: boolean; reason?: string };
|
||||
const result = await actionService.toggleMaintenanceMode(body.enabled, body.reason);
|
||||
const rawBody = await req.json();
|
||||
const parsed = MaintenanceModeSchema.safeParse(rawBody);
|
||||
|
||||
if (!parsed.success) {
|
||||
return Response.json({ error: "Invalid payload", issues: parsed.error.issues }, { status: 400 });
|
||||
}
|
||||
|
||||
const result = await actionService.toggleMaintenanceMode(parsed.data.enabled, parsed.data.reason);
|
||||
return Response.json(result);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -141,19 +156,30 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
|
||||
|
||||
const fileRef = Bun.file(safePath);
|
||||
if (await fileRef.exists()) {
|
||||
// If serving index.html, inject env vars for frontend
|
||||
if (pathName === "/index.html") {
|
||||
let html = await fileRef.text();
|
||||
const { env } = await import("@shared/lib/env");
|
||||
const envScript = `<script>window.AURORA_ENV = { ADMIN_TOKEN: "${env.ADMIN_TOKEN}" };</script>`;
|
||||
html = html.replace("</head>", `${envScript}</head>`);
|
||||
return new Response(html, { headers: { "Content-Type": "text/html" } });
|
||||
}
|
||||
return new Response(fileRef);
|
||||
}
|
||||
|
||||
// SPA Fallback: Serve index.html for unknown non-file routes
|
||||
// If the path looks like a file (has extension), return 404
|
||||
// Otherwise serve index.html
|
||||
const parts = pathName.split("/");
|
||||
const lastPart = parts[parts.length - 1];
|
||||
if (lastPart?.includes(".")) {
|
||||
return new Response("Not Found", { status: 404 });
|
||||
}
|
||||
|
||||
return new Response(Bun.file(join(distDir, "index.html")));
|
||||
const indexFile = Bun.file(join(distDir, "index.html"));
|
||||
let indexHtml = await indexFile.text();
|
||||
const { env: sharedEnv } = await import("@shared/lib/env");
|
||||
const script = `<script>window.AURORA_ENV = { ADMIN_TOKEN: "${sharedEnv.ADMIN_TOKEN}" };</script>`;
|
||||
indexHtml = indexHtml.replace("</head>", `${script}</head>`);
|
||||
return new Response(indexHtml, { headers: { "Content-Type": "text/html" } });
|
||||
},
|
||||
|
||||
websocket: {
|
||||
|
||||
Reference in New Issue
Block a user