//! `CurrentUser` axum extractor. //! //! Resolves a request to a logged-in user by trying, in order: //! 1. a `mangalord_session` cookie (session lookup by `sha256(value)`); //! 2. an `Authorization: Bearer ` header (api_token lookup). //! //! Both paths look up by hash, never by raw value. Failure to resolve //! either way returns 401 via `AppError::Unauthenticated`. use axum::async_trait; use axum::extract::FromRequestParts; use axum::http::request::Parts; use axum_extra::extract::cookie::CookieJar; use axum_extra::headers::authorization::Bearer; use axum_extra::headers::Authorization; use axum_extra::TypedHeader; use crate::app::AppState; use crate::auth::token::hash_token; use crate::domain::User; use crate::error::AppError; use crate::repo; pub const SESSION_COOKIE_NAME: &str = "mangalord_session"; pub struct CurrentUser(pub User); #[async_trait] impl FromRequestParts for CurrentUser { type Rejection = AppError; async fn from_request_parts( parts: &mut Parts, state: &AppState, ) -> Result { let jar = CookieJar::from_headers(&parts.headers); if let Some(cookie) = jar.get(SESSION_COOKIE_NAME) { let hash = hash_token(cookie.value()); if let Some(session) = repo::session::find_active(&state.db, &hash).await? { if let Some(user) = repo::user::find_by_id(&state.db, session.user_id).await? { return Ok(CurrentUser(user)); } } } if let Ok(TypedHeader(Authorization(bearer))) = TypedHeader::>::from_request_parts(parts, state).await { let hash = hash_token(bearer.token()); if let Some(token) = repo::api_token::find_active(&state.db, &hash).await? { if let Some(user) = repo::user::find_by_id(&state.db, token.user_id).await? { // Fire-and-forget would be ideal but the test harness needs // a deterministic write so the touched timestamp shows up // when the test inspects state. Synchronous is fine. let _ = repo::api_token::touch_last_used(&state.db, token.id).await; return Ok(CurrentUser(user)); } } } Err(AppError::Unauthenticated) } }