Some checks failed
Deploy to Production / test (push) Failing after 30s
The web/ folder contains the REST API, WebSocket server, and OAuth routes — not a web frontend. Renaming to api/ clarifies this distinction since the actual web frontend lives in panel/. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
218 lines
7.7 KiB
TypeScript
218 lines
7.7 KiB
TypeScript
/**
|
|
* @fileoverview Moderation case management endpoints for Aurora API.
|
|
* Provides endpoints for viewing, creating, and resolving moderation cases.
|
|
*/
|
|
|
|
import type { RouteContext, RouteModule } from "./types";
|
|
import {
|
|
jsonResponse,
|
|
errorResponse,
|
|
parseBody,
|
|
withErrorHandling
|
|
} from "./utils";
|
|
import { CreateCaseSchema, ClearCaseSchema, CaseIdPattern } from "./schemas";
|
|
|
|
/**
|
|
* Moderation routes handler.
|
|
*
|
|
* Endpoints:
|
|
* - GET /api/moderation - List cases with filters
|
|
* - GET /api/moderation/:caseId - Get single case
|
|
* - POST /api/moderation - Create new case
|
|
* - PUT /api/moderation/:caseId/clear - Clear/resolve case
|
|
*/
|
|
async function handler(ctx: RouteContext): Promise<Response | null> {
|
|
const { pathname, method, req, url } = ctx;
|
|
|
|
// Only handle requests to /api/moderation*
|
|
if (!pathname.startsWith("/api/moderation")) {
|
|
return null;
|
|
}
|
|
|
|
const { moderationService } = await import("@shared/modules/moderation/moderation.service");
|
|
|
|
/**
|
|
* @route GET /api/moderation
|
|
* @description Returns moderation cases with optional filtering.
|
|
*
|
|
* @query userId - Filter by target user ID
|
|
* @query moderatorId - Filter by moderator ID
|
|
* @query type - Filter by case type (warn, timeout, kick, ban, note, prune)
|
|
* @query active - Filter by active status (true/false)
|
|
* @query limit - Max results (default: 50)
|
|
* @query offset - Pagination offset (default: 0)
|
|
*
|
|
* @response 200 - `{ cases: ModerationCase[] }`
|
|
* @response 500 - Error fetching cases
|
|
*
|
|
* @example
|
|
* // Request
|
|
* GET /api/moderation?type=warn&active=true&limit=10
|
|
*
|
|
* // Response
|
|
* {
|
|
* "cases": [
|
|
* {
|
|
* "id": "1",
|
|
* "caseId": "CASE-0001",
|
|
* "type": "warn",
|
|
* "userId": "123456789",
|
|
* "username": "User1",
|
|
* "moderatorId": "987654321",
|
|
* "moderatorName": "Mod1",
|
|
* "reason": "Spam",
|
|
* "active": true,
|
|
* "createdAt": "2024-01-15T12:00:00Z"
|
|
* }
|
|
* ]
|
|
* }
|
|
*/
|
|
if (pathname === "/api/moderation" && method === "GET") {
|
|
return withErrorHandling(async () => {
|
|
const filter: any = {};
|
|
if (url.searchParams.get("userId")) filter.userId = url.searchParams.get("userId");
|
|
if (url.searchParams.get("moderatorId")) filter.moderatorId = url.searchParams.get("moderatorId");
|
|
if (url.searchParams.get("type")) filter.type = url.searchParams.get("type");
|
|
const activeParam = url.searchParams.get("active");
|
|
if (activeParam !== null) filter.active = activeParam === "true";
|
|
filter.limit = url.searchParams.get("limit") ? parseInt(url.searchParams.get("limit")!) : 50;
|
|
filter.offset = url.searchParams.get("offset") ? parseInt(url.searchParams.get("offset")!) : 0;
|
|
|
|
const cases = await moderationService.searchCases(filter);
|
|
return jsonResponse({ cases });
|
|
}, "fetch moderation cases");
|
|
}
|
|
|
|
/**
|
|
* @route GET /api/moderation/:caseId
|
|
* @description Returns a single moderation case by case ID.
|
|
* Case IDs follow the format CASE-XXXX (e.g., CASE-0001).
|
|
*
|
|
* @param caseId - Case ID in CASE-XXXX format
|
|
* @response 200 - Full case object
|
|
* @response 404 - Case not found
|
|
* @response 500 - Error fetching case
|
|
*/
|
|
if (pathname.match(/^\/api\/moderation\/CASE-\d+$/i) && method === "GET") {
|
|
const caseId = pathname.split("/").pop()!.toUpperCase();
|
|
|
|
return withErrorHandling(async () => {
|
|
const moderationCase = await moderationService.getCaseById(caseId);
|
|
|
|
if (!moderationCase) {
|
|
return errorResponse("Case not found", 404);
|
|
}
|
|
|
|
return jsonResponse(moderationCase);
|
|
}, "fetch moderation case");
|
|
}
|
|
|
|
/**
|
|
* @route POST /api/moderation
|
|
* @description Creates a new moderation case.
|
|
*
|
|
* @body {
|
|
* type: "warn" | "timeout" | "kick" | "ban" | "note" | "prune" (required),
|
|
* userId: string (required) - Target user's Discord ID,
|
|
* username: string (required) - Target user's username,
|
|
* moderatorId: string (required) - Moderator's Discord ID,
|
|
* moderatorName: string (required) - Moderator's username,
|
|
* reason: string (required) - Reason for the action,
|
|
* metadata?: object - Additional case metadata (e.g., duration)
|
|
* }
|
|
* @response 201 - `{ success: true, case: ModerationCase }`
|
|
* @response 400 - Missing required fields
|
|
* @response 500 - Error creating case
|
|
*
|
|
* @example
|
|
* // Request
|
|
* POST /api/moderation
|
|
* {
|
|
* "type": "warn",
|
|
* "userId": "123456789",
|
|
* "username": "User1",
|
|
* "moderatorId": "987654321",
|
|
* "moderatorName": "Mod1",
|
|
* "reason": "Rule violation",
|
|
* "metadata": { "duration": "24h" }
|
|
* }
|
|
*/
|
|
if (pathname === "/api/moderation" && method === "POST") {
|
|
return withErrorHandling(async () => {
|
|
const data = await req.json() as Record<string, any>;
|
|
|
|
if (!data.type || !data.userId || !data.username || !data.moderatorId || !data.moderatorName || !data.reason) {
|
|
return errorResponse(
|
|
"Missing required fields: type, userId, username, moderatorId, moderatorName, reason",
|
|
400
|
|
);
|
|
}
|
|
|
|
const newCase = await moderationService.createCase({
|
|
type: data.type,
|
|
userId: data.userId,
|
|
username: data.username,
|
|
moderatorId: data.moderatorId,
|
|
moderatorName: data.moderatorName,
|
|
reason: data.reason,
|
|
metadata: data.metadata || {},
|
|
});
|
|
|
|
return jsonResponse({ success: true, case: newCase }, 201);
|
|
}, "create moderation case");
|
|
}
|
|
|
|
/**
|
|
* @route PUT /api/moderation/:caseId/clear
|
|
* @description Clears/resolves a moderation case.
|
|
* Sets the case as inactive and records who cleared it.
|
|
*
|
|
* @param caseId - Case ID in CASE-XXXX format
|
|
* @body {
|
|
* clearedBy: string (required) - Discord ID of user clearing the case,
|
|
* clearedByName: string (required) - Username of user clearing the case,
|
|
* reason?: string - Reason for clearing (default: "Cleared via API")
|
|
* }
|
|
* @response 200 - `{ success: true, case: ModerationCase }`
|
|
* @response 400 - Missing required fields
|
|
* @response 404 - Case not found
|
|
* @response 500 - Error clearing case
|
|
*
|
|
* @example
|
|
* // Request
|
|
* PUT /api/moderation/CASE-0001/clear
|
|
* { "clearedBy": "987654321", "clearedByName": "Admin1", "reason": "Appeal accepted" }
|
|
*/
|
|
if (pathname.match(/^\/api\/moderation\/CASE-\d+\/clear$/i) && method === "PUT") {
|
|
const caseId = (pathname.split("/")[3] || "").toUpperCase();
|
|
|
|
return withErrorHandling(async () => {
|
|
const data = await req.json() as Record<string, any>;
|
|
|
|
if (!data.clearedBy || !data.clearedByName) {
|
|
return errorResponse("Missing required fields: clearedBy, clearedByName", 400);
|
|
}
|
|
|
|
const updatedCase = await moderationService.clearCase({
|
|
caseId,
|
|
clearedBy: data.clearedBy,
|
|
clearedByName: data.clearedByName,
|
|
reason: data.reason || "Cleared via API",
|
|
});
|
|
|
|
if (!updatedCase) {
|
|
return errorResponse("Case not found", 404);
|
|
}
|
|
|
|
return jsonResponse({ success: true, case: updatedCase });
|
|
}, "clear moderation case");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export const moderationRoutes: RouteModule = {
|
|
name: "moderation",
|
|
handler
|
|
};
|