+++ title = "quotesdb/api: POST /api/admin/lock and /api/admin/unlock endpoints" priority = 6 status = "done" ticket_type = "feature" dependencies = ["69a2c5"] +++ ## POST /api/admin/lock and /api/admin/unlock endpoints Add the two admin-protected endpoints that toggle the global submissions lock. Both require `X-Admin-Code` and return the current lock state after the operation. Note: This ticket depends on ticket 35685a (GET /api/status) because that ticket adds `get_submissions_locked` and `set_submissions_locked` to the `QuoteRepository` trait and seeds the `submissions_locked` row in the database. Complete 35685a first. --- ## Files to modify - `src/bin/api/handlers/mod.rs` (or `src/bin/api/handlers/admin.rs`) — add `lock_submissions` and `unlock_submissions` handlers - `src/bin/api/main.rs` — register the two new routes No new DB trait methods are needed; both handlers reuse `set_submissions_locked(bool)` introduced in 35685a. --- ## Handlers ```rust /// POST /api/admin/lock /// Requires X-Admin-Code header. Sets submissions_locked = true. /// Response: 200 { "submissions_locked": true } or 403 on bad code. pub async fn lock_submissions( State(repo): State>, headers: HeaderMap, ) -> impl IntoResponse { let admin_code = extract_admin_code(&headers); if !verify_admin_code(&repo, admin_code).await { ... } match repo.set_submissions_locked(true).await { Ok(()) => Json(json!({ "submissions_locked": true })).into_response(), Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), } } /// POST /api/admin/unlock /// Requires X-Admin-Code header. Sets submissions_locked = false. /// Response: 200 { "submissions_locked": false } or 403 on bad code. pub async fn unlock_submissions( State(repo): State>, headers: HeaderMap, ) -> impl IntoResponse { // same pattern, locked = false } ``` Implement a shared helper `verify_admin_code(repo, code) -> bool` (or extract inline) that fetches the stored admin code from `admin_config` and compares it. Use constant-time comparison if possible. --- ## Route registration (src/bin/api/main.rs) ```rust .route("/api/admin/lock", post(handlers::lock_submissions)) .route("/api/admin/unlock", post(handlers::unlock_submissions)) ``` --- ## Tests - `POST /api/admin/lock` with correct `X-Admin-Code` → `200 { "submissions_locked": true }` - `POST /api/admin/unlock` with correct `X-Admin-Code` → `200 { "submissions_locked": false }` - `POST /api/admin/lock` with wrong code → `403` - `POST /api/admin/unlock` with missing header → `403` - Lock/unlock idempotent: locking when already locked still returns `200 { "submissions_locked": true }` --- ## Validation ```sh cargo fmt && cargo check && cargo clippy && cargo test ``` --- ## Commit ``` feat(quotesdb): POST /api/admin/lock and /api/admin/unlock endpoints ```