use axum::extract::{FromRef, FromRequestParts}; use axum::http::{request::Parts, StatusCode}; use axum::response::{IntoResponse, Response}; use axum::Json; use serde::Serialize; use crate::application::auth::{AuthError, load_current_user}; use crate::domain::CurrentUser; use crate::http::cookies::read_session_cookie; use crate::config::Config; use sqlx::PgPool; /// Extracts the authenticated user from the session cookie. #[derive(Debug, Clone)] pub struct AuthenticatedUser(pub CurrentUser); /// HTTP rejection for authentication failures. #[derive(Debug)] pub struct AuthRejection { status: StatusCode, body: Json, } impl IntoResponse for AuthRejection { fn into_response(self) -> Response { (self.status, self.body).into_response() } } impl AuthRejection { pub fn status(&self) -> StatusCode { self.status } pub fn message(&self) -> String { self.body.error.message.clone() } } impl FromRequestParts for AuthenticatedUser where S: Send + Sync, Config: FromRef, PgPool: FromRef, { type Rejection = AuthRejection; async fn from_request_parts( parts: &mut Parts, state: &S, ) -> Result { let config = Config::from_ref(state); let db_pool = PgPool::from_ref(state); let session_id = read_session_cookie(&parts.headers, &config.oauth_cookie_name) .ok_or_else(missing_session_cookie)?; let user = load_current_user(&db_pool, session_id) .await .map_err(map_auth_error)?; Ok(Self(user)) } } #[derive(Debug, Serialize)] struct ErrorResponse { error: ErrorDetail, } #[derive(Debug, Serialize)] struct ErrorDetail { message: String, } fn map_auth_error(error: AuthError) -> AuthRejection { AuthRejection { status: error.status(), body: Json(ErrorResponse { error: ErrorDetail { message: error.message(), }, }), } } fn missing_session_cookie() -> AuthRejection { AuthRejection { status: StatusCode::UNAUTHORIZED, body: Json(ErrorResponse { error: ErrorDetail { message: "Missing session cookie.".to_string(), }, }), } }