ferro_rs/auth/extract.rs
1//! Typed extractors for authenticated users
2//!
3//! Provides `AuthUser<T>` and `OptionalUser<T>` for handler parameter injection,
4//! eliminating boilerplate `Auth::user_as::<T>()` calls.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use ferro_rs::{handler, AuthUser, OptionalUser, Response};
10//!
11//! #[handler]
12//! pub async fn dashboard(user: AuthUser<User>) -> Response {
13//! // `user` is guaranteed to be authenticated
14//! // Returns 401 automatically if not
15//! json_response!({ "name": user.name })
16//! }
17//!
18//! #[handler]
19//! pub async fn home(user: OptionalUser<User>) -> Response {
20//! match user.as_ref() {
21//! Some(u) => json_response!({ "greeting": format!("Hello, {}!", u.name) }),
22//! None => json_response!({ "greeting": "Hello, guest!" }),
23//! }
24//! }
25//! ```
26//!
27//! # Limitations
28//!
29//! These extractors use the `FromRequest` trait which takes ownership of the
30//! request. This means they cannot be combined with `FormRequest` types or
31//! `Request` in the same handler signature. If you need both auth and request
32//! body, use `Auth::user_as::<T>()` manually in the handler body.
33
34use std::ops::Deref;
35
36use async_trait::async_trait;
37
38use super::authenticatable::Authenticatable;
39use super::guard::Auth;
40use crate::error::FrameworkError;
41use crate::http::FromRequest;
42use crate::Request;
43
44/// Extracts the authenticated user, returning 401 if not authenticated.
45///
46/// Use this in handler signatures where authentication is required.
47/// The handler macro will automatically extract the user from the session.
48///
49/// # Type Parameters
50///
51/// * `T` - The concrete user type implementing `Authenticatable + Clone`
52///
53/// # Example
54///
55/// ```rust,ignore
56/// #[handler]
57/// pub async fn profile(user: AuthUser<User>) -> Response {
58/// json_response!({ "id": user.id, "email": user.email })
59/// }
60/// ```
61pub struct AuthUser<T>(pub T);
62
63#[async_trait]
64impl<T> FromRequest for AuthUser<T>
65where
66 T: Authenticatable + Clone + 'static,
67{
68 async fn from_request(_req: Request) -> Result<Self, FrameworkError> {
69 let user = Auth::user().await?;
70
71 match user {
72 None => Err(FrameworkError::domain("Unauthenticated.", 401)),
73 Some(arc_user) => {
74 let concrete = arc_user
75 .as_any()
76 .downcast_ref::<T>()
77 .cloned()
78 .ok_or_else(|| {
79 FrameworkError::internal(format!(
80 "AuthUser downcast failed: user is not of type {}",
81 std::any::type_name::<T>()
82 ))
83 })?;
84 Ok(AuthUser(concrete))
85 }
86 }
87 }
88}
89
90impl<T> Deref for AuthUser<T> {
91 type Target = T;
92
93 fn deref(&self) -> &T {
94 &self.0
95 }
96}
97
98/// Extracts the authenticated user as `Option<T>`, never failing on auth.
99///
100/// Use this in handler signatures where authentication is optional.
101/// Returns `None` for guests instead of a 401 error.
102///
103/// # Type Parameters
104///
105/// * `T` - The concrete user type implementing `Authenticatable + Clone`
106///
107/// # Example
108///
109/// ```rust,ignore
110/// #[handler]
111/// pub async fn home(user: OptionalUser<User>) -> Response {
112/// if let Some(u) = user.as_ref() {
113/// json_response!({ "message": format!("Welcome back, {}!", u.name) })
114/// } else {
115/// json_response!({ "message": "Welcome, guest!" })
116/// }
117/// }
118/// ```
119pub struct OptionalUser<T>(pub Option<T>);
120
121#[async_trait]
122impl<T> FromRequest for OptionalUser<T>
123where
124 T: Authenticatable + Clone + 'static,
125{
126 async fn from_request(_req: Request) -> Result<Self, FrameworkError> {
127 let user = Auth::user().await?;
128
129 match user {
130 None => Ok(OptionalUser(None)),
131 Some(arc_user) => {
132 let concrete = arc_user.as_any().downcast_ref::<T>().cloned();
133 Ok(OptionalUser(concrete))
134 }
135 }
136 }
137}
138
139impl<T> Deref for OptionalUser<T> {
140 type Target = Option<T>;
141
142 fn deref(&self) -> &Option<T> {
143 &self.0
144 }
145}