From 95cd0a81833469861aaa92b2f28daaab8b91cd6b Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Wed, 4 Mar 2026 12:32:29 -0800 Subject: [PATCH] docs(quotesdb): admin features design doc --- .../plans/2026-03-04-admin-features-design.md | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 quotesdb/docs/plans/2026-03-04-admin-features-design.md diff --git a/quotesdb/docs/plans/2026-03-04-admin-features-design.md b/quotesdb/docs/plans/2026-03-04-admin-features-design.md new file mode 100644 index 0000000..e068cdf --- /dev/null +++ b/quotesdb/docs/plans/2026-03-04-admin-features-design.md @@ -0,0 +1,135 @@ +# 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 |