feat(admin): expose read-only Discord suggestions API
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
import { problemDetails } from "@minecraft-account-manager/contracts";
|
||||
import { adminAuthOptions, requiredAdminRole } from "@/lib/auth/admin-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>) {
|
||||
try {
|
||||
const session = await getServerSession(adminAuthOptions);
|
||||
if (!session) throw new SuggestionsError(401, "unauthorized", "Sign in as an administrator.");
|
||||
const roles = (session.user as { roles?: unknown } | undefined)?.roles;
|
||||
if (!Array.isArray(roles) || !roles.includes(requiredAdminRole)) throw new SuggestionsError(403, "forbidden", "This API is restricted to administrators.");
|
||||
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; });
|
||||
}
|
||||
Reference in New Issue
Block a user