5.8 KiB
| title | status | type | priority | created_at | updated_at |
|---|---|---|---|---|---|
| Admin super auth code: delete any quote regardless of per-quote auth | completed | feature | high | 2026-03-10T23:32:08Z | 2026-03-10T23:32:08Z |
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_codeinsrc/lib.rs). - Stored in the database in a new
admin_configtable. - 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:
/// 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:
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:
/// 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<Option<String>, 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:
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:
async fn delete_quote(&self, id: &str, auth_code: &str) -> Result<DeleteResult, DbError> {
// ... 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:
async fn get_admin_auth_code(&self) -> Result<Option<String>, 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_quoteandupdate_quotecheck 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_CONFIGconstantsrc/bin/api/db/mod.rs— two new trait methods + updated docstrings fordelete_quote/update_quotesrc/bin/api/db/native.rs— implementations + admin fallback logicsrc/bin/api/db/d1.rs— same for D1src/bin/api/handlers/mod.rs—MockRepostubssrc/bin/api/main.rs— seed + print admin code on startupinfra/schema.sql—admin_configtable
Validation
# From quotesdb/ root
cargo fmt && cargo check && cargo clippy && cargo test
Manual test:
- Start the server:
cargo run - Observe admin code printed to stderr.
- Create a quote:
curl -X PUT http://localhost:3000/api/quotes -H 'Content-Type: application/json' -d '{"text":"Test","author":"A","tags":[]}' - Try deleting with wrong code: should return 403.
- Try deleting with admin code: should return 204.
- Restart the server: same admin code should be printed (not regenerated).
Commit scope
feat(quotesdb): admin super auth code for quote moderation