use axum::{ extract::{FromRef, FromRequestParts}, http::{StatusCode, header, header::AUTHORIZATION, request::Parts}, response::{IntoResponse, Redirect}, }; use domain::{errors::DomainError, value_objects::UserId}; use crate::{errors::ApiError, state::AppState}; pub struct AuthenticatedUser(pub UserId); impl FromRequestParts for AuthenticatedUser where AppState: FromRef, S: Send + Sync, { type Rejection = ApiError; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app_state = AppState::from_ref(state); let token = parts .headers .get(AUTHORIZATION) .and_then(|v| v.to_str().ok()) .and_then(|v| v.strip_prefix("Bearer ")) .ok_or_else(|| { ApiError(DomainError::Unauthorized( "Missing or invalid auth token".into(), )) })?; let user_id = app_state.app_ctx.auth_service.validate_token(token).await?; Ok(AuthenticatedUser(user_id)) } } pub struct OptionalCookieUser(pub Option); pub struct RequiredCookieUser(pub UserId); fn extract_token_from_cookie(parts: &Parts) -> Option { parts .headers .get(header::COOKIE) .and_then(|v| v.to_str().ok()) .and_then(|cookies| { cookies .split(';') .find_map(|c| c.trim().strip_prefix("token=").map(str::to_string)) }) } impl FromRequestParts for OptionalCookieUser where AppState: FromRef, S: Send + Sync, { type Rejection = std::convert::Infallible; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app_state = AppState::from_ref(state); let Some(token) = extract_token_from_cookie(parts) else { return Ok(OptionalCookieUser(None)); }; let user_id = app_state .app_ctx .auth_service .validate_token(&token) .await .ok(); Ok(OptionalCookieUser(user_id)) } } impl FromRequestParts for RequiredCookieUser where AppState: FromRef, S: Send + Sync, { type Rejection = axum::response::Response; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app_state = AppState::from_ref(state); let token = extract_token_from_cookie(parts) .ok_or_else(|| Redirect::to("/login").into_response())?; let user_id = app_state .app_ctx .auth_service .validate_token(&token) .await .map_err(|_| Redirect::to("/login").into_response())?; Ok(RequiredCookieUser(user_id)) } } pub struct AdminUser(pub UserId); impl FromRequestParts for AdminUser where AppState: FromRef, S: Send + Sync, { type Rejection = axum::response::Response; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app_state = AppState::from_ref(state); let RequiredCookieUser(user_id) = RequiredCookieUser::from_request_parts(parts, state).await?; let user = app_state .app_ctx .user_repository .find_by_id(&user_id) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())? .ok_or_else(|| StatusCode::UNAUTHORIZED.into_response())?; match user.role() { domain::models::UserRole::Admin => Ok(AdminUser(user_id)), _ => Err(StatusCode::FORBIDDEN.into_response()), } } } #[cfg(test)] #[path = "tests/extractors.rs"] mod tests;