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.

121 lines
3.6 KiB
Markdown

+++
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<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
```rust
#[derive(Deserialize)]
struct ResetAuthCodeRequest {
new_code: Option<String>,
}
#[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<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)
```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
```