docs(quotesdb): admin features design doc
parent
2ccad33921
commit
95cd0a8183
@ -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<String, DbError>;
|
||||||
|
|
||||||
|
/// Return whether submissions are currently locked.
|
||||||
|
async fn get_submissions_locked(&self) -> Result<bool, DbError>;
|
||||||
|
|
||||||
|
/// 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: <value-from-field>`.
|
||||||
|
- `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<StatusResponse, ApiError>
|
||||||
|
pub async fn admin_reset_auth_code(current: &str, new_code: Option<&str>, admin_code: &str) -> Result<String, ApiError>
|
||||||
|
pub async fn admin_lock(admin_code: &str) -> Result<bool, ApiError>
|
||||||
|
pub async fn admin_unlock(admin_code: &str) -> Result<bool, ApiError>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 |
|
||||||
Loading…
Reference in New Issue