larust_auth/extractor.rs
1use crate::guard;
2use crate::Authenticatable;
3use axum::extract::FromRequestParts;
4use axum::http::request::Parts;
5use axum::http::StatusCode;
6use larust_core::AppError;
7use larust_http::session::Session;
8
9/// Extracts the currently authenticated user, or rejects with 401 if there
10/// isn't one - for handlers that require a logged-in user and want the
11/// `User` itself, not just a yes/no check. Pair with the `require_auth`
12/// middleware (which redirects a guest to the login page) on routes meant
13/// for browsers; use `Auth<U>` alone on routes meant to fail loudly instead
14/// (e.g. an API endpoint).
15///
16/// For an optional current user (Laravel's nullable `Auth::user()`), call
17/// [`crate::user`] directly instead of using this extractor.
18pub struct Auth<U>(pub U);
19
20// GOTCHAS.md: axum-core declares `FromRequestParts` via `#[async_trait]`,
21// not native async-fn-in-traits - an impl written as a plain `async fn`
22// fails with a confusing E0195 lifetime error instead of a clear message
23// about the mismatch.
24#[axum::async_trait]
25impl<S, U> FromRequestParts<S> for Auth<U>
26where
27 S: Send + Sync,
28 U: Authenticatable,
29{
30 type Rejection = AppError;
31
32 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
33 // `Session`'s own `Rejection` is `(StatusCode, &'static str)`, not a
34 // real `Error` impl (it can't go directly into `AppError::Internal`'s
35 // `Box<dyn Error + Send + Sync>`) - it only ever fires if
36 // `SessionManagerLayer` isn't installed on this router, i.e. a
37 // developer misconfiguration, so it's always an internal error here.
38 let session = Session::from_request_parts(parts, state)
39 .await
40 .map_err(|(_, message)| AppError::Internal(Box::new(std::io::Error::other(message))))?;
41
42 guard::user::<U>(&session)
43 .await?
44 .map(Auth)
45 .ok_or_else(|| AppError::Http {
46 status: StatusCode::UNAUTHORIZED,
47 message: "Unauthenticated.".to_string(),
48 })
49 }
50}