Built for people who think better out loud.
1use axum::extract::{FromRef, FromRequestParts};
2use axum::http::{request::Parts, StatusCode};
3use axum::response::{IntoResponse, Response};
4use axum::Json;
5use serde::Serialize;
6
7use crate::application::auth::{AuthError, load_current_user};
8use crate::domain::CurrentUser;
9use crate::http::cookies::read_session_cookie;
10use crate::config::Config;
11use sqlx::PgPool;
12
13/// Extracts the authenticated user from the session cookie.
14#[derive(Debug, Clone)]
15pub struct AuthenticatedUser(pub CurrentUser);
16
17/// HTTP rejection for authentication failures.
18#[derive(Debug)]
19pub struct AuthRejection {
20 status: StatusCode,
21 body: Json<ErrorResponse>,
22}
23
24impl IntoResponse for AuthRejection {
25 fn into_response(self) -> Response {
26 (self.status, self.body).into_response()
27 }
28}
29
30impl AuthRejection {
31 pub fn status(&self) -> StatusCode {
32 self.status
33 }
34
35 pub fn message(&self) -> String {
36 self.body.error.message.clone()
37 }
38}
39
40impl<S> FromRequestParts<S> for AuthenticatedUser
41where
42 S: Send + Sync,
43 Config: FromRef<S>,
44 PgPool: FromRef<S>,
45{
46 type Rejection = AuthRejection;
47
48 async fn from_request_parts(
49 parts: &mut Parts,
50 state: &S,
51 ) -> Result<Self, Self::Rejection> {
52 let config = Config::from_ref(state);
53 let db_pool = PgPool::from_ref(state);
54 let session_id = read_session_cookie(&parts.headers, &config.oauth_cookie_name)
55 .ok_or_else(missing_session_cookie)?;
56 let user = load_current_user(&db_pool, session_id)
57 .await
58 .map_err(map_auth_error)?;
59 Ok(Self(user))
60 }
61}
62
63#[derive(Debug, Serialize)]
64struct ErrorResponse {
65 error: ErrorDetail,
66}
67
68#[derive(Debug, Serialize)]
69struct ErrorDetail {
70 message: String,
71}
72
73fn map_auth_error(error: AuthError) -> AuthRejection {
74 AuthRejection {
75 status: error.status(),
76 body: Json(ErrorResponse {
77 error: ErrorDetail {
78 message: error.message(),
79 },
80 }),
81 }
82}
83
84fn missing_session_cookie() -> AuthRejection {
85 AuthRejection {
86 status: StatusCode::UNAUTHORIZED,
87 body: Json(ErrorResponse {
88 error: ErrorDetail {
89 message: "Missing session cookie.".to_string(),
90 },
91 }),
92 }
93}