48 lines
3.0 KiB
TypeScript
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; });
|
|
}
|