//! Auth enforcement middleware: axum extractor that validates bearer tokens. //! //! Reads `Authorization: Bearer ` from request headers, validates via //! [`crate::routes::auth::validate_token`], and returns the authenticated //! fingerprint or a 401 rejection. use axum::{ extract::FromRequestParts, http::{request::Parts, StatusCode}, response::{IntoResponse, Response}, }; use crate::state::AppState; /// Extractor that validates a bearer token and provides the authenticated fingerprint. /// /// Place this as the **first** parameter in any handler that requires authentication. /// The extractor will reject the request with 401 if the token is missing or invalid. /// /// # Example /// /// ```ignore /// async fn my_handler( /// auth: AuthFingerprint, /// State(state): State, /// ) -> impl IntoResponse { /// let fp = auth.fingerprint; // guaranteed valid /// // ... /// } /// ``` pub struct AuthFingerprint { pub fingerprint: String, } #[axum::async_trait] impl FromRequestParts for AuthFingerprint { type Rejection = AuthError; async fn from_request_parts( parts: &mut Parts, state: &AppState, ) -> Result { let header = parts .headers .get("authorization") .and_then(|v| v.to_str().ok()) .and_then(|s| s.strip_prefix("Bearer ")) .map(|s| s.trim().to_string()); let token = match header { Some(t) if !t.is_empty() => t, _ => return Err(AuthError::MissingToken), }; match crate::routes::auth::validate_token(&state.db.tokens, &token) { Some(fingerprint) => Ok(AuthFingerprint { fingerprint }), None => Err(AuthError::InvalidToken), } } } /// Rejection type for [`AuthFingerprint`] extractor failures. pub enum AuthError { /// No `Authorization: Bearer ` header was present (or it was empty). MissingToken, /// The token was present but did not pass validation (expired or unknown). InvalidToken, } impl IntoResponse for AuthError { fn into_response(self) -> Response { let (status, msg) = match self { AuthError::MissingToken => ( StatusCode::UNAUTHORIZED, "missing or empty Authorization: Bearer header", ), AuthError::InvalidToken => ( StatusCode::UNAUTHORIZED, "invalid or expired token", ), }; (status, axum::Json(serde_json::json!({ "error": msg }))).into_response() } }