+++ title = "quotesdb/api: POST /api/admin/reset-auth-code endpoint" priority = 6 status = "done" ticket_type = "feature" dependencies = ["69a2c5"] +++ ## POST /api/admin/reset-auth-code endpoint Add the admin-protected endpoint that replaces the stored admin auth code. The caller must supply the current code via `X-Admin-Code`. A new code may be provided in the request body; if omitted, the server generates a fresh 4-word passphrase. --- ## Files to modify - `src/bin/api/db/mod.rs` — add `update_admin_auth_code` to the `QuoteRepository` trait - `src/bin/api/db/d1.rs` — implement `update_admin_auth_code` for D1 - `src/bin/api/db/native.rs` — implement `update_admin_auth_code` for native SQLite - `src/bin/api/handlers/mod.rs` (or a new `src/bin/api/handlers/admin.rs`) — add the `reset_auth_code` handler - `src/bin/api/main.rs` — register the new route --- ## New trait method (src/bin/api/db/mod.rs) Add to the `QuoteRepository` trait: ```rust /// Replace the admin auth code if `current` matches the stored value. /// If `new_code` is `None`, generates a fresh 4-word passphrase. /// Returns the new auth code string on success, or `DbError::Unauthorized` /// if `current` does not match. async fn update_admin_auth_code( &self, current: &str, new_code: Option<&str>, ) -> Result; ``` Implementation steps: 1. Fetch the stored `admin_auth_code` from `admin_config`. 2. If it does not match `current`, return `DbError::Unauthorized` (or a dedicated variant). 3. Determine the new code: use `new_code` if provided, otherwise call the existing passphrase-generation utility. 4. Write the new value to `admin_config` with `UPDATE`. 5. Return the new code string. --- ## Request / response types ```rust #[derive(Deserialize)] struct ResetAuthCodeRequest { new_code: Option, } #[derive(Serialize)] struct ResetAuthCodeResponse { auth_code: String, } ``` --- ## Handler ```rust /// POST /api/admin/reset-auth-code /// Requires X-Admin-Code header matching the stored admin passphrase. /// Body: { "new_code": "optional-string" } /// Response: 200 { "auth_code": "new-code" } or 403 on mismatch. pub async fn reset_auth_code( State(repo): State>, headers: HeaderMap, Json(payload): Json, ) -> impl IntoResponse { let admin_code = match headers.get("x-admin-code").and_then(|v| v.to_str().ok()) { Some(c) => c.to_owned(), None => return StatusCode::FORBIDDEN.into_response(), }; match repo.update_admin_auth_code(&admin_code, payload.new_code.as_deref()).await { Ok(new_code) => Json(ResetAuthCodeResponse { auth_code: new_code }).into_response(), Err(DbError::Unauthorized) => StatusCode::FORBIDDEN.into_response(), Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), } } ``` --- ## Route registration (src/bin/api/main.rs) ```rust .route("/api/admin/reset-auth-code", post(handlers::reset_auth_code)) ``` --- ## Tests - `POST /api/admin/reset-auth-code` with correct `X-Admin-Code` and no body `new_code` → `200`, response contains a non-empty `auth_code` - `POST /api/admin/reset-auth-code` with correct `X-Admin-Code` and explicit `new_code` → `200`, `auth_code` equals the supplied value - `POST /api/admin/reset-auth-code` with wrong `X-Admin-Code` → `403` - `POST /api/admin/reset-auth-code` with missing `X-Admin-Code` header → `403` - After a successful reset, subsequent calls with the old code return `403` and with the new code return `200` --- ## Validation ```sh cargo fmt && cargo check && cargo clippy && cargo test ``` --- ## Commit ``` feat(quotesdb): POST /api/admin/reset-auth-code endpoint ```