import { Hono } from "hono"; import { isProgrammingError } from "../lib/errors.js"; import { logger } from "../lib/logger.js"; /** * Single proxy endpoint for all moderation actions. * * Reads `action`, `id`, and `reason` from the form body and dispatches * to the correct AppView mod endpoint. Returns HX-Refresh on success * so HTMX reloads the current page to show updated state. * * Action dispatch table: * lock → POST /api/mod/lock body: { topicId, reason } * unlock → DELETE /api/mod/lock/:id body: { reason } * hide → POST /api/mod/hide body: { postId, reason } * unhide → DELETE /api/mod/hide/:id body: { reason } * ban → POST /api/mod/ban body: { targetDid, reason } * unban → DELETE /api/mod/ban/:id body: { reason } */ export function createModActionRoute(appviewUrl: string) { return new Hono().post("/mod/action", async (c) => { let body: Record; try { body = await c.req.parseBody(); } catch { return c.html(`

Invalid form submission.

`); } const action = typeof body.action === "string" ? body.action.trim() : ""; const id = typeof body.id === "string" ? body.id.trim() : ""; const reason = typeof body.reason === "string" ? body.reason.trim() : ""; const cookieHeader = c.req.header("cookie") ?? ""; // Validate action const validActions = ["lock", "unlock", "hide", "unhide", "ban", "unban"]; if (!validActions.includes(action)) { return c.html(`

Unknown action.

`); } // Validate reason if (!reason) { return c.html(`

Reason is required.

`); } if (reason.length > 3000) { return c.html(`

Reason must not exceed 3000 characters.

`); } // Validate id if (!id) { return c.html(`

Target ID is required.

`); } // Build AppView request let appviewEndpoint: string; let method: "POST" | "DELETE"; let appviewBody: Record; switch (action) { case "lock": appviewEndpoint = `${appviewUrl}/api/mod/lock`; method = "POST"; appviewBody = { topicId: id, reason }; break; case "unlock": appviewEndpoint = `${appviewUrl}/api/mod/lock/${id}`; method = "DELETE"; appviewBody = { reason }; break; case "hide": appviewEndpoint = `${appviewUrl}/api/mod/hide`; method = "POST"; appviewBody = { postId: id, reason }; break; case "unhide": appviewEndpoint = `${appviewUrl}/api/mod/hide/${id}`; method = "DELETE"; appviewBody = { reason }; break; case "ban": appviewEndpoint = `${appviewUrl}/api/mod/ban`; method = "POST"; appviewBody = { targetDid: id, reason }; break; case "unban": appviewEndpoint = `${appviewUrl}/api/mod/ban/${id}`; method = "DELETE"; appviewBody = { reason }; break; default: return c.html(`

Unknown action.

`); } // Forward to AppView let appviewRes: Response; try { appviewRes = await fetch(appviewEndpoint, { method, headers: { "Content-Type": "application/json", Cookie: cookieHeader, }, body: JSON.stringify(appviewBody), }); } catch (error) { if (isProgrammingError(error)) throw error; logger.error("Failed to proxy mod action to AppView", { operation: `${method} ${appviewEndpoint}`, action, error: error instanceof Error ? error.message : String(error), }); return c.html( `

Forum temporarily unavailable. Please try again.

` ); } if (appviewRes.ok) { return new Response(null, { status: 200, headers: { "HX-Refresh": "true" }, }); } // Handle error responses let errorMessage = "Something went wrong. Please try again."; if (appviewRes.status === 401) { logger.error("AppView returned 401 for mod action — session may have expired", { operation: `${method} ${appviewEndpoint}`, action, }); errorMessage = "You must be logged in to perform this action."; } else if (appviewRes.status === 403) { logger.error("AppView returned 403 for mod action — permission mismatch", { operation: `${method} ${appviewEndpoint}`, action, }); errorMessage = "You don't have permission for this action."; } else if (appviewRes.status === 404) { errorMessage = "Target not found."; } else if (appviewRes.status === 409) { // 409 = already active (e.g. locking an already-locked topic) errorMessage = "This action is already active."; } else if (appviewRes.status >= 500) { logger.error("AppView returned server error for mod action", { operation: `${method} ${appviewEndpoint}`, action, status: appviewRes.status, }); } return c.html(`

${errorMessage}

`); }); }