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.

3.6 KiB

+++ 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:

/// 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<String, DbError>;

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

#[derive(Deserialize)]
struct ResetAuthCodeRequest {
    new_code: Option<String>,
}

#[derive(Serialize)]
struct ResetAuthCodeResponse {
    auth_code: String,
}

Handler

/// 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<Arc<dyn QuoteRepository>>,
    headers: HeaderMap,
    Json(payload): Json<ResetAuthCodeRequest>,
) -> 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)

.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_code200, response contains a non-empty auth_code
  • POST /api/admin/reset-auth-code with correct X-Admin-Code and explicit new_code200, auth_code equals the supplied value
  • POST /api/admin/reset-auth-code with wrong X-Admin-Code403
  • 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

cargo fmt && cargo check && cargo clippy && cargo test

Commit

feat(quotesdb): POST /api/admin/reset-auth-code endpoint