# Admin Features — Design **Date:** 2026-03-04 --- ## Overview Adds a set of admin-only controls to the QuotesDB API and UI: - **Auth code reset** — change the admin passphrase in-place. - **Submission lock / unlock** — globally prevent new quotes from being created. - **Public status endpoint** — lets the UI know whether submissions are open. - **Admin UI page** — a single `/admin` page with an auth code field, a reset form, and a lock/unlock toggle. - **Submit page gate** — shows a "submissions are closed" banner instead of the form when the lock is active. --- ## API ### Public endpoints | Method | Path | Auth | Description | |--------|------|------|-------------| | `GET` | `/api/status` | None | Returns `{ "submissions_locked": bool }` | ### Admin endpoints All admin endpoints require the `X-Admin-Code` header matching the value stored in `admin_config`. A mismatch returns `403 Forbidden`. | Method | Path | Body | Response | |--------|------|------|----------| | `POST` | `/api/admin/reset-auth-code` | `{ "new_code"?: "..." }` | `200 { "auth_code": "new-code" }` | | `POST` | `/api/admin/lock` | — | `200 { "submissions_locked": true }` | | `POST` | `/api/admin/unlock` | — | `200 { "submissions_locked": false }` | `new_code` is optional. If omitted, the server generates a new 4-word passphrase. ### Modified endpoints `PUT /api/quotes` — gains a pre-flight lock check: - If `submissions_locked = true` → `423 Locked` with `{ "error": "submissions are closed" }`. --- ## Database The existing `admin_config` key/value table gains a second row: | key | value | |-----|-------| | `admin_auth_code` | the 4-word passphrase (already exists) | | `submissions_locked` | `"0"` or `"1"` (new) | `submissions_locked` is seeded to `"0"` alongside `admin_auth_code` on startup. ### New `QuoteRepository` trait methods ```rust /// Replace the admin auth code if `current` matches; generate or use `new_code`. /// Returns the new auth code string. async fn update_admin_auth_code( &self, current: &str, new_code: Option<&str>, ) -> Result; /// Return whether submissions are currently locked. async fn get_submissions_locked(&self) -> Result; /// Set the submissions lock state. async fn set_submissions_locked(&self, locked: bool) -> Result<(), DbError>; ``` --- ## Frontend ### New route | Route | Page | |-------|------| | `/admin` | Admin controls page | ### `/admin` page - A single persistent `Admin auth code` text input at the top. - **Reset auth code section:** optional `New passphrase` field + "Reset" button. On success, shows the new code prominently. - **Submissions section:** shows current lock state (fetched via `GET /api/status` on mount) and a single "Lock submissions" / "Unlock submissions" button. - All actions send `X-Admin-Code: `. - `403` responses show an "Wrong auth code" inline error. ### `/submit` page - On mount, calls `GET /api/status`. - If `submissions_locked = true`, hides the form entirely and shows a banner: > "Submissions are currently closed." - The submit nav link in the top nav remains active (users still reach the page and see the closed notice). ### New API client functions (`src/bin/ui/api.rs`) ```rust pub async fn get_status() -> Result pub async fn admin_reset_auth_code(current: &str, new_code: Option<&str>, admin_code: &str) -> Result pub async fn admin_lock(admin_code: &str) -> Result pub async fn admin_unlock(admin_code: &str) -> Result ``` --- ## Auth flow The admin code is a 4-word passphrase printed to stderr on API startup. It is never exposed over the API (no GET endpoint). The admin enters it manually into the `/admin` page's persistent field each session. --- ## Error handling | Scenario | HTTP status | UI message | |----------|-------------|------------| | Wrong admin code | `403 Forbidden` | "Wrong auth code." | | Submissions locked on create | `423 Locked` | Banner: "Submissions are currently closed." | | DB error | `500 Internal Server Error` | Generic error display |