fix(quotesdb): atomic update_admin_auth_code, fix handler docstring

Replace the two-step read-check-write in update_admin_auth_code with a
single atomic UPDATE … WHERE key = 'admin_auth_code' AND value = ?current
in both NativeRepository and D1Repository. Rows-affected count is checked:
zero means the code was absent or mismatched → DbError::Forbidden; one
means success.

Also remove the now-unnecessary replacement2 clone binding in native.rs.

Fix the reset_auth_code handler doc comment to accurately describe that a
missing X-Admin-Code header is caught by the handler itself (before any DB
call), while a wrong-but-present code reaches the DB layer which returns
DbError::Forbidden.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
quotesdb
Elijah Voigt 3 months ago
parent ab398b690c
commit a4d59b4371

@ -569,32 +569,45 @@ impl QuoteRepository for D1Repository {
/// Replace the admin auth code if `current` matches the stored value.
///
/// Generates a fresh 4-word passphrase when `new_code` is `None`.
/// Returns `Err(DbError::Forbidden)` if `current` does not match the stored code.
///
/// The check and update are performed atomically via a single
/// `UPDATE … WHERE key = 'admin_auth_code' AND value = ?current` statement.
/// The result metadata's `changes` count is inspected: if zero rows were
/// affected the stored code either does not exist or did not match `current`,
/// and `Err(DbError::Forbidden)` is returned. If one row was affected the new
/// code is returned.
async fn update_admin_auth_code(
&self,
current: &str,
new_code: Option<&str>,
) -> Result<String, DbError> {
let stored = self.get_admin_auth_code().await?;
if stored.as_deref() != Some(current) {
return Err(DbError::Forbidden);
}
let replacement = new_code
.map(|s| s.to_owned())
.unwrap_or_else(generate_auth_code);
self.db
let result = self
.db
.prepare(
"INSERT INTO admin_config (key, value) VALUES ('admin_auth_code', ?1) \
ON CONFLICT(key) DO UPDATE SET value = excluded.value",
"UPDATE admin_config \
SET value = ?1 \
WHERE key = 'admin_auth_code' AND value = ?2",
)
.bind(&[JsValue::from_str(&replacement)])
.bind(&[JsValue::from_str(&replacement), JsValue::from_str(current)])
.map_err(|e| DbError::Internal(e.to_string()))?
.run()
.await
.map_err(|e| DbError::Internal(e.to_string()))?;
let changes = result
.meta()
.map_err(|e| DbError::Internal(e.to_string()))?
.and_then(|m| m.changes)
.unwrap_or(0);
if changes == 0 {
return Err(DbError::Forbidden);
}
Ok(replacement)
}

@ -513,34 +513,40 @@ impl QuoteRepository for NativeRepository {
/// Replace the admin auth code if `current` matches the stored value.
///
/// Generates a fresh 4-word passphrase when `new_code` is `None`.
/// Returns `Err(DbError::Forbidden)` if `current` does not match the stored code.
///
/// The check and update are performed atomically via a single
/// `UPDATE … WHERE key = 'admin_auth_code' AND value = ?current` statement.
/// If zero rows are affected the stored code either does not exist or did not
/// match `current`, and `Err(DbError::Forbidden)` is returned. If one row is
/// affected the new code is returned.
async fn update_admin_auth_code(
&self,
current: &str,
new_code: Option<&str>,
) -> Result<String, DbError> {
let stored = self.get_admin_auth_code().await?;
if stored.as_deref() != Some(current) {
return Err(DbError::Forbidden);
}
let replacement = new_code
.map(|s| s.to_owned())
.unwrap_or_else(generate_auth_code);
let replacement2 = replacement.clone();
let current = current.to_owned();
let replacement_inner = replacement.clone();
self.conn
let changed = self
.conn
.call(move |conn| {
conn.execute(
"INSERT INTO admin_config (key, value) VALUES ('admin_auth_code', ?1) \
ON CONFLICT(key) DO UPDATE SET value = excluded.value",
rusqlite::params![replacement2],
)?;
Ok(())
Ok(conn.execute(
"UPDATE admin_config \
SET value = ?1 \
WHERE key = 'admin_auth_code' AND value = ?2",
rusqlite::params![replacement_inner, current],
)?)
})
.await
.map_err(|e| DbError::Internal(e.to_string()))?;
if changed == 0 {
return Err(DbError::Forbidden);
}
Ok(replacement)
}

@ -510,9 +510,12 @@ struct ResetAuthCodeResponse {
/// { "auth_code": "word-word-word-word" }
/// ```
///
/// Returns `403 Forbidden` if the header is absent or the code is incorrect.
/// The DB layer (`update_admin_auth_code`) performs the auth check internally
/// and returns `DbError::Forbidden` on mismatch.
/// Returns `403 Forbidden` in two cases:
/// - Missing `X-Admin-Code` header — the handler returns `403` immediately,
/// before any database call.
/// - Wrong code — the DB layer (`update_admin_auth_code`) returns
/// `DbError::Forbidden` when the supplied code does not match the stored
/// value, which the handler maps to `403`.
#[cfg_attr(target_arch = "wasm32", worker::send)]
async fn reset_auth_code(
State(repo): State<Repo>,

Loading…
Cancel
Save