Files
minecraft-account-manager/docs/admin-suggestions-api.md
T
dmg eb8ef18688
CI / validate (push) Successful in 8m20s
Release / release (push) Successful in 10m52s
fix(suggestions): preserve one-item archived pagination
2026-09-10 16:00:02 -04:00

7.5 KiB

Admin suggestions API

The portal provides a read-only view of one Discord forum channel, using the existing NextAuth admin session or a verified Keycloak machine bearer token. Player sessions and Discord bot credentials are not accepted as API credentials. Browser users sign in at /admin/login; same-origin calls send the session cookie. Machine clients use Authorization: Bearer <access-token> with the configured portal audience and client role. See Admin API authentication for verification rules, safe identity checks, and failure behavior.

Portal interface

Open /admin/suggestions from the administrator navigation. The idea desk lists active or archived forum posts with tags, status, approximate message counts, author IDs, timestamps, and Discord links. Select a title to open /admin/suggestions/:id, read the starter post and its reaction counts, and page through discussion newest-first. The starter is not duplicated in the discussion view.

The UI uses these same admin-protected GET endpoints with its browser session, not a second integration. Both pages independently check admin access before rendering; the APIs recheck it on every read. Expired/unauthorized API access offers an admin sign-in link. Loading, empty/deleted content, missing-text, and retryable failure states are explicit. Changing status aborts obsolete requests, and pagination restores keyboard focus to the page indicator. Text is rendered literally with React escaping, never as HTML or interpreted Discord Markdown. No bot token, forum configuration value, reply input, vote button, or moderation control is added to the client bundle.

Pagination history is page-local and resets when switching status or leaving the page. Reload the browser to refresh a view; upstream reads may use the documented 30-second cache. The real forum ID is still configured only through GitOps, not the UI or source.

Runtime configuration

  • DISCORD_SUGGESTIONS_FORUM_ID: required forum channel snowflake, configured through GitOps. No source-code default.
  • DISCORD_GUILD_ID: existing Discord guild configuration; the forum must belong to it.
  • DISCORD_BOT_TOKEN: existing server-side credential, never returned to consumers.

The web workload needs these variables, not just the bot workload. The actual forum ID is maintained only in GitOps. No schema or bot Gateway changes are needed. The application uses Discord REST API v10 with the existing bot identity.

The bot must have View Channel and Read Message History for the forum and its posts. Message bodies are subject to Discord's Message Content privileged intent, including REST access: enable it for the application and obtain approval from Discord if required. Missing content can appear as an empty body rather than an HTTP error; check a known text post before production acceptance. No permissions or intents are changed by this feature.

Endpoints

GET endpoint Result
/api/suggestions?status=active&limit=25 Active posts, newest-created first. status defaults to active.
/api/suggestions?status=archived&limit=25 Archived public forum posts, newest archive timestamp first.
/api/suggestions/:id Suggestion metadata and originalPost; null when the starter message was deleted.
/api/suggestions/:id/messages?limit=25 Discussion messages, newest first, including the starter if reached.

Lists return { items, nextCursor }. Pass nextCursor back as the URL-encoded cursor parameter with the same status. Limits are integers from 1 to 100. Active cursors are thread IDs; archived cursors are UTC archive timestamps normalized to Z while preserving fractional precision; message cursors are message IDs. Treat cursors as opaque. Messages may yield a final empty page because Discord does not provide a has_more flag for messages. Active threads are fetched via the guild active-threads endpoint, filtered to the forum, sorted, and paginated locally. Archives are paginated by Discord. For limit=1, the portal requests Discord's minimum of two posts but returns at most one; the cursor follows the last returned post so the extra post remains available on the next page, even when Discord reports no further upstream pages. This is a live view, not a consistent snapshot: threads can move between active and archived lists.

Suggestion fields: id, title, authorId, createdAt, archived, locked, tags, messageCount, discordUrl. Discord's message count is approximate, not a vote count. Detail messages include id, author (id, name), content, createdAt, editedAt, reactions (emoji, count), and discordUrl. Reactions remain reaction counts, not interpreted votes. Attachments, embeds, and rendered Discord Markdown are not mirrored; use Discord links for the original presentation.

Errors and safety

Errors use RFC 9457 application/problem+json, HTTP-matching status, stable urn:error:* types, and safe details:

  • 401 unauthorized: no admin session or invalid supplied credentials; no redirect; includes WWW-Authenticate: Bearer realm="admin-api".
  • 403 forbidden: verified identity lacks the required role (configured-client role for bearer tokens).
  • 503 admin-auth-unavailable: authentication configuration, browser session service, or JWKS service unavailable; no session fallback for supplied credentials.
  • 400 invalid-request: invalid ID, cursor, limit, status, or list/message query parameter.
  • 404 suggestion-not-found: inaccessible/deleted thread, or thread outside the configured forum.
  • 405 method-not-allowed: writes are unsupported; Allow: GET, HEAD.
  • 503 suggestions-not-configured, discord-unavailable, discord-rate-limited, or discord-busy: configuration, permissions, upstream failure, or temporary backoff. Rate limits and capacity limits include Retry-After.

Every endpoint rechecks admin authorization before any cached or fresh data is returned. Responses use Cache-Control: no-store. The in-process Discord cache lasts 30 seconds, contains at most 200 entries, coalesces identical concurrent reads, and permits at most eight concurrent upstream requests. Requests have an eight-second timeout and never follow redirects. Discord rate limits establish a per-client cooldown without retry loops. Caches and cooldowns are per process, not shared with bot Gateway activity or other replicas.

The client checks the configured forum's guild/type and each requested thread's parent/type before reading messages. It only calls fixed Discord endpoints with validated snowflakes. Error bodies and credentials are not logged or forwarded. There is no database synchronization and no Discord mutation support.

Verification before rollout

Run the source checks and focused API tests in apps/web/src/lib/discord/suggestions.test.ts and apps/web/src/app/api/suggestions/route.test.ts, plus UI tests in apps/web/src/components/suggestions-browser.test.tsx, suggestion-reader.test.tsx, and apps/web/src/app/admin/(console)/suggestions/. Under the separately approved GitOps/release plan, verify one known active post, one archived post, message content, and admin/non-admin access against the actual forum. Offline fixtures do not establish live Discord permissions or intent approval.

References: Discord threads, Channel resource, Message content intent.