You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
168 lines
5.8 KiB
Markdown
168 lines
5.8 KiB
Markdown
+++
|
|
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<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:
|
|
```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<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`:
|
|
```rust
|
|
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_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` |