Skip to main content

ferro_rs/auth/
guard.rs

1//! Authentication guard (facade)
2
3use std::sync::Arc;
4
5use crate::container::App;
6use crate::session::{
7    auth_user_id, clear_auth_user, generate_csrf_token, regenerate_session_id, session,
8    session_mut, set_auth_user, DatabaseSessionDriver, SessionStore,
9};
10
11use super::authenticatable::Authenticatable;
12use super::provider::UserProvider;
13
14/// Authentication facade
15///
16/// Provides Laravel-like static methods for authentication operations.
17///
18/// # Example
19///
20/// ```rust,ignore
21/// use ferro_rs::Auth;
22///
23/// // Check if authenticated
24/// if Auth::check() {
25///     let user_id = Auth::id().unwrap();
26/// }
27///
28/// // Log in
29/// Auth::login(user_id);
30///
31/// // Log out
32/// Auth::logout();
33/// ```
34pub struct Auth;
35
36impl Auth {
37    /// Get the authenticated user's ID
38    ///
39    /// Returns None if not authenticated.
40    pub fn id() -> Option<i64> {
41        auth_user_id()
42    }
43
44    /// Get the authenticated user's ID as a specific type
45    ///
46    /// Useful when your database uses i32 primary keys but Auth stores i64.
47    ///
48    /// # Example
49    ///
50    /// ```rust,ignore
51    /// // SeaORM entities typically use i32 for primary keys
52    /// let user_id: i32 = Auth::id_as().expect("User must be authenticated");
53    /// ```
54    pub fn id_as<T>() -> Option<T>
55    where
56        T: TryFrom<i64>,
57    {
58        Self::id().and_then(|id| T::try_from(id).ok())
59    }
60
61    /// Check if a user is currently authenticated
62    pub fn check() -> bool {
63        Self::id().is_some()
64    }
65
66    /// Check if the current user is a guest (not authenticated)
67    pub fn guest() -> bool {
68        !Self::check()
69    }
70
71    /// Log in a user by their ID
72    ///
73    /// This sets the user ID in the session, making them authenticated.
74    ///
75    /// # Security
76    ///
77    /// This method regenerates the session ID to prevent session fixation attacks.
78    pub fn login(user_id: i64) {
79        // Regenerate session ID to prevent session fixation
80        regenerate_session_id();
81
82        // Set the authenticated user
83        set_auth_user(user_id);
84
85        // Regenerate CSRF token for extra security
86        session_mut(|session| {
87            session.csrf_token = generate_csrf_token();
88        });
89    }
90
91    /// Log in a user with "remember me" functionality
92    ///
93    /// This extends the session lifetime for persistent login.
94    ///
95    /// # Arguments
96    ///
97    /// * `user_id` - The user's ID
98    /// * `remember_token` - A secure token for remember me cookie
99    pub fn login_remember(user_id: i64, _remember_token: &str) {
100        // For now, just do a regular login
101        // Remember me cookie handling is done in the controller
102        Self::login(user_id);
103    }
104
105    /// Log out the current user
106    ///
107    /// Clears the authenticated user from the session.
108    ///
109    /// # Security
110    ///
111    /// This regenerates the CSRF token to prevent any cached tokens from being reused.
112    pub fn logout() {
113        // Clear the authenticated user
114        clear_auth_user();
115
116        // Regenerate CSRF token for security
117        session_mut(|session| {
118            session.csrf_token = generate_csrf_token();
119        });
120    }
121
122    /// Log out and invalidate the entire session
123    ///
124    /// Use this for complete session destruction (e.g., "logout everywhere").
125    pub fn logout_and_invalidate() {
126        session_mut(|session| {
127            session.flush();
128            session.csrf_token = generate_csrf_token();
129        });
130    }
131
132    /// Log out all other sessions for the current user.
133    ///
134    /// Destroys all sessions for the authenticated user except the current one.
135    /// Use after password changes or security-sensitive operations.
136    ///
137    /// Returns the number of destroyed sessions, or None if not authenticated.
138    pub async fn logout_other_devices() -> Option<Result<u64, crate::error::FrameworkError>> {
139        let user_id = Self::id()?;
140        let current_session_id = session().map(|s| s.id);
141        // Lifetime values are irrelevant here — destroy_for_user only deletes by user_id.
142        let store = DatabaseSessionDriver::new(
143            std::time::Duration::from_secs(0),
144            std::time::Duration::from_secs(0),
145        );
146        Some(
147            store
148                .destroy_for_user(user_id, current_session_id.as_deref())
149                .await,
150        )
151    }
152
153    /// Attempt to authenticate with a validator function
154    ///
155    /// The validator function should return the user ID if credentials are valid.
156    ///
157    /// # Example
158    ///
159    /// ```rust,ignore
160    /// let user_id = Auth::attempt(async {
161    ///     // Validate credentials
162    ///     let user = User::find_by_email(&email).await?;
163    ///     if user.verify_password(&password)? {
164    ///         Ok(Some(user.id))
165    ///     } else {
166    ///         Ok(None)
167    ///     }
168    /// }).await?;
169    ///
170    /// if let Some(id) = user_id {
171    ///     // Authentication successful
172    /// }
173    /// ```
174    pub async fn attempt<F, Fut>(validator: F) -> Result<Option<i64>, crate::error::FrameworkError>
175    where
176        F: FnOnce() -> Fut,
177        Fut: std::future::Future<Output = Result<Option<i64>, crate::error::FrameworkError>>,
178    {
179        let result = validator().await?;
180        if let Some(user_id) = result {
181            Self::login(user_id);
182        }
183        Ok(result)
184    }
185
186    /// Validate credentials without logging in
187    ///
188    /// Useful for password confirmation dialogs.
189    pub async fn validate<F, Fut>(validator: F) -> Result<bool, crate::error::FrameworkError>
190    where
191        F: FnOnce() -> Fut,
192        Fut: std::future::Future<Output = Result<bool, crate::error::FrameworkError>>,
193    {
194        validator().await
195    }
196
197    /// Get the currently authenticated user
198    ///
199    /// Returns `None` if not authenticated or if no `UserProvider` is registered.
200    ///
201    /// # Example
202    ///
203    /// ```rust,ignore
204    /// use ferro_rs::Auth;
205    ///
206    /// if let Some(user) = Auth::user().await? {
207    ///     println!("Logged in as user {}", user.auth_identifier());
208    /// }
209    /// ```
210    ///
211    /// # Errors
212    ///
213    /// Returns an error if no `UserProvider` is registered in the container.
214    /// Make sure to register a `UserProvider` in your `bootstrap.rs`:
215    ///
216    /// ```rust,ignore
217    /// bind!(dyn UserProvider, DatabaseUserProvider);
218    /// ```
219    pub async fn user() -> Result<Option<Arc<dyn Authenticatable>>, crate::error::FrameworkError> {
220        let user_id = match Self::id() {
221            Some(id) => id,
222            None => return Ok(None),
223        };
224
225        let provider = App::make::<dyn UserProvider>().ok_or_else(|| {
226            crate::error::FrameworkError::internal(
227                "No UserProvider registered. Register one in bootstrap.rs with: \
228                 bind!(dyn UserProvider, YourUserProvider)"
229                    .to_string(),
230            )
231        })?;
232
233        provider.retrieve_by_id(user_id).await
234    }
235
236    /// Get the authenticated user, cast to a concrete type
237    ///
238    /// This is a convenience method that retrieves the user and downcasts
239    /// it to your concrete User type.
240    ///
241    /// # Example
242    ///
243    /// ```rust,ignore
244    /// use ferro_rs::Auth;
245    /// use ferro_rs::models::users::User;
246    ///
247    /// if let Some(user) = Auth::user_as::<User>().await? {
248    ///     println!("Welcome, user #{}!", user.id);
249    /// }
250    /// ```
251    ///
252    /// # Type Parameters
253    ///
254    /// * `T` - The concrete user type that implements `Authenticatable` and `Clone`
255    pub async fn user_as<T: Authenticatable + Clone>(
256    ) -> Result<Option<T>, crate::error::FrameworkError> {
257        let user = Self::user().await?;
258        Ok(user.and_then(|u| u.as_any().downcast_ref::<T>().cloned()))
259    }
260}