+++ title = "Admin super auth code: delete any quote regardless of per-quote auth" priority = 7 status = "done" ticket_type = "feature" dependencies = [] +++ ## Feature Add an **admin super auth code** — a single global passphrase that can delete (and update) any quote, bypassing the per-quote `auth_code` check. This allows the operator to moderate content without needing the original submitter's code. The admin code is: - Generated once on first startup using the same 4-word passphrase generator (`generate_auth_code` in `src/lib.rs`). - Stored in the database in a new `admin_config` table. - Printed prominently to stderr on every startup so the operator can note it. - Never exposed via the API. --- ## Part 1 — Database: new migration Add a new migration constant in `src/bin/api/db/migrations.rs`: ```rust /// Creates the admin_config key/value table for storing global configuration. /// /// Stores a single row for the admin auth code under the key `admin_auth_code`. pub const CREATE_ADMIN_CONFIG: &str = "\ CREATE TABLE IF NOT EXISTS admin_config ( key TEXT PRIMARY KEY, value TEXT NOT NULL )"; ``` Run this migration in `QuoteRepository::run_migrations` after the existing migrations. The implementation then seeds the admin auth code if absent (see Part 2). Update `infra/schema.sql` to include: ```sql CREATE TABLE IF NOT EXISTS admin_config ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); ``` --- ## Part 2 — Repository trait: `db/mod.rs` Add two new methods to `QuoteRepository`: ```rust /// Retrieve the admin super auth code from `admin_config`. /// /// Returns `Ok(None)` if the table is empty (should not happen after migrations). async fn get_admin_auth_code(&self) -> Result, DbError>; /// Insert the admin auth code into `admin_config` if it is not already set. /// /// Called once during startup, after `run_migrations`. async fn seed_admin_auth_code(&self, code: &str) -> Result<(), DbError>; ``` The startup sequence (in `main.rs`) becomes: ```rust repo.run_migrations().await?; // Seed admin code on first run if repo.get_admin_auth_code().await?.is_none() { let code = quotesdb::generate_auth_code(); repo.seed_admin_auth_code(&code).await?; } // Always print the admin code at startup let admin_code = repo.get_admin_auth_code().await?.unwrap(); eprintln!("╔══════════════════════════════════════════════╗"); eprintln!("║ ADMIN AUTH CODE: {admin_code:<28}║"); eprintln!("╚══════════════════════════════════════════════╝"); ``` --- ## Part 3 — Native implementation: `db/native.rs` Implement `get_admin_auth_code` and `seed_admin_auth_code` using rusqlite. **Extend `delete_quote`** to accept the admin code as a fallback: ```rust async fn delete_quote(&self, id: &str, auth_code: &str) -> Result { // ... existing logic ... // Before returning Forbidden, check admin auth code let admin_code = self.get_admin_auth_code().await?; if Some(auth_code) == admin_code.as_deref() { // Admin code matches — delete unconditionally // ... execute DELETE without checking quotes.auth_code ... return Ok(DeleteResult::Deleted); } Ok(DeleteResult::Forbidden) } ``` Similarly extend `update_quote` to allow admin override. The cleanest approach is to refactor `delete_quote` and `update_quote` to first attempt the per-quote auth check, and if it fails, check against the admin code. --- ## Part 4 — D1 implementation: `db/d1.rs` Apply the same changes as Part 3 for the WASM/Cloudflare D1 path. --- ## Part 5 — API startup: `src/bin/api/main.rs` Update the startup sequence as shown in Part 2. The admin code print must be clearly visible in logs. --- ## Part 6 — Mock repo in tests: `handlers/mod.rs` Add stub implementations of `get_admin_auth_code` and `seed_admin_auth_code` to `MockRepo`: ```rust async fn get_admin_auth_code(&self) -> Result, DbError> { Ok(None) // no admin code in tests by default } async fn seed_admin_auth_code(&self, _code: &str) -> Result<(), DbError> { Ok(()) } ``` --- ## Design notes - The admin code is **never returned by any API endpoint** — there is no way to discover it via HTTP. - The admin code is stored plaintext in `admin_config`, consistent with per-quote auth codes. This is acceptable given the stated security model (simple passphrase, no user accounts). - Only `delete_quote` and `update_quote` check the admin code. Read operations are unaffected. - The admin code is **not rotatable** via the API — an operator who needs to rotate it must manually update the database row. --- ## Files touched - `src/bin/api/db/migrations.rs` — `CREATE_ADMIN_CONFIG` constant - `src/bin/api/db/mod.rs` — two new trait methods + updated docstrings for `delete_quote`/`update_quote` - `src/bin/api/db/native.rs` — implementations + admin fallback logic - `src/bin/api/db/d1.rs` — same for D1 - `src/bin/api/handlers/mod.rs` — `MockRepo` stubs - `src/bin/api/main.rs` — seed + print admin code on startup - `infra/schema.sql` — `admin_config` table ## Validation ```sh # From quotesdb/ root cargo fmt && cargo check && cargo clippy && cargo test ``` Manual test: 1. Start the server: `cargo run` 2. Observe admin code printed to stderr. 3. Create a quote: `curl -X PUT http://localhost:3000/api/quotes -H 'Content-Type: application/json' -d '{"text":"Test","author":"A","tags":[]}'` 4. Try deleting with wrong code: should return 403. 5. Try deleting with admin code: should return 204. 6. Restart the server: same admin code should be printed (not regenerated). ## Commit scope `feat(quotesdb): admin super auth code for quote moderation`