use axum::{ extract::Request, http::StatusCode, middleware::Next, response::Response, }; // API key auth middleware. Checks X-API-Key header against configured key. // Fixed P5-001 (2026-04-23): previously #[allow(dead_code)] — the function // existed but was never layered onto the router, so [auth] enabled=true // silently enforced nothing. Now wired via from_fn_with_state in main.rs. pub async fn api_key_auth( axum::extract::State(expected): axum::extract::State, request: Request, next: Next, ) -> Result { // /health stays public (LB/systemd probes). Every other route is gated. if request.uri().path() == "/health" { return Ok(next.run(request).await); } let provided = request .headers() .get("x-api-key") .and_then(|v| v.to_str().ok()); // Constant-time-ish eq on the raw bytes; good enough for a shared-secret // X-API-Key. Timing-attack resistance here matters less than the // equivalent HMAC check would; adopt subtle crate if key-space grows. match provided { Some(key) if eq_ct(key.as_bytes(), expected.0.as_bytes()) => { Ok(next.run(request).await) } _ => { tracing::warn!( path = %request.uri().path(), "unauthorized request: missing or invalid API key", ); Err(StatusCode::UNAUTHORIZED) } } } fn eq_ct(a: &[u8], b: &[u8]) -> bool { if a.len() != b.len() { return false; } let mut diff: u8 = 0; for (x, y) in a.iter().zip(b.iter()) { diff |= x ^ y; } diff == 0 } /// Wrapper type for the API key, stored in request extensions. #[derive(Clone)] pub struct ApiKey(pub String);