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}