Files
minecraft-account-manager/apps/web/src/lib/discord/suggestions-api.ts
T
dmg c2ac2ad16b
CI / validate (push) Successful in 6m49s
Release / release (push) Successful in 11m24s
feat(auth): verify machine tokens for admin read APIs
2026-09-10 14:53:47 -04:00

48 lines
3.0 KiB
TypeScript

import { problemDetails } from "@minecraft-account-manager/contracts";
import { authorizeAdminApi } from "@/lib/auth/admin-api-auth";
import { problemInstance, problemResponse } from "@/lib/problem-response";
import { createSuggestionsClient, SuggestionsError } from "./suggestions";
type Client = ReturnType<typeof createSuggestionsClient>;
let runtime: { token: string; guildId: string; forumId: string; client: Client } | undefined;
function getClient() {
const token = process.env.DISCORD_BOT_TOKEN?.trim() ?? "";
const guildId = process.env.DISCORD_GUILD_ID?.trim() ?? "";
const forumId = process.env.DISCORD_SUGGESTIONS_FORUM_ID?.trim() ?? "";
if (!runtime || runtime.token !== token || runtime.guildId !== guildId || runtime.forumId !== forumId) {
runtime = { token, guildId, forumId, client: createSuggestionsClient({ token, guildId, forumId }) };
}
return runtime.client;
}
export async function suggestionsApi(request: Request, operation: (client: Client) => Promise<unknown>) {
const authorization = await authorizeAdminApi(request);
if (authorization.response) return authorization.response;
try {
return Response.json(await operation(getClient()), { headers: { "cache-control": "no-store" } });
} catch (error) {
const safe = error instanceof SuggestionsError ? error : new SuggestionsError(503, "discord-unavailable", "Discord suggestions are unavailable.");
const titles: Record<number, string> = { 400: "Invalid request", 401: "Authentication required", 403: "Administrator role required", 404: "Suggestion not found", 405: "Method not allowed", 503: "Suggestions unavailable" };
const response = problemResponse(problemDetails(`urn:error:${safe.code}`, titles[safe.status] ?? "Suggestions unavailable", safe.status, safe.message, problemInstance(request)));
if (safe.retryAfter) response.headers.set("retry-after", String(safe.retryAfter));
return response;
}
}
export function suggestionQuery(request: Request, list = false) {
const params = new URL(request.url).searchParams;
const allowed = list ? ["limit", "cursor", "status"] : ["limit", "cursor"];
for (const key of params.keys()) {
if (!allowed.includes(key) || params.getAll(key).length !== 1 || !params.get(key)) throw new SuggestionsError(400, "invalid-request", "Unsupported or repeated query parameter.");
}
const rawLimit = params.get("limit");
if (rawLimit !== null && !/^\d{1,3}$/.test(rawLimit)) throw new SuggestionsError(400, "invalid-request", "Limit must be between 1 and 100.");
return { limit: rawLimit === null ? undefined : Number(rawLimit), cursor: params.get("cursor") ?? undefined, ...(list ? { status: params.get("status") ?? undefined } : {}) };
}
export function suggestionsReadOnly(request: Request) {
return suggestionsApi(request, async () => {
throw new SuggestionsError(405, "method-not-allowed", "Suggestions are read-only. Use GET.");
}).then((response) => { if (response.status === 405) response.headers.set("allow", "GET, HEAD"); return response; });
}