1use std::future::Future;
2use std::pin::Pin;
3
4use crate::error::AuthError;
5use crate::handle::AllowThem;
6use crate::types::{PermissionName, RoleName, SessionToken, User, UserId};
7
8pub type AuthFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, AuthError>> + Send + 'a>>;
10
11pub trait AuthClient: Send + Sync {
21 fn validate_session<'a>(&'a self, token: &'a SessionToken) -> AuthFuture<'a, Option<User>>;
26
27 fn check_role<'a>(&'a self, user_id: &'a UserId, role: &'a RoleName) -> AuthFuture<'a, bool>;
29
30 fn check_permission<'a>(
32 &'a self,
33 user_id: &'a UserId,
34 permission: &'a PermissionName,
35 ) -> AuthFuture<'a, bool>;
36
37 fn logout<'a>(&'a self, token: &'a SessionToken) -> AuthFuture<'a, ()>;
39
40 fn login_url(&self) -> &str;
42
43 fn session_cookie_name(&self) -> &str;
45}
46
47pub struct EmbeddedAuthClient {
53 ath: AllowThem,
54 login_url: String,
55}
56
57impl EmbeddedAuthClient {
58 pub fn new(ath: AllowThem, login_url: impl Into<String>) -> Self {
63 Self {
64 ath,
65 login_url: login_url.into(),
66 }
67 }
68}
69
70impl AuthClient for EmbeddedAuthClient {
71 fn validate_session<'a>(&'a self, token: &'a SessionToken) -> AuthFuture<'a, Option<User>> {
72 Box::pin(async move {
73 let ttl = self.ath.session_config().ttl;
74 let session = match self.ath.db().validate_session(token, ttl).await? {
75 Some(s) => s,
76 None => return Ok(None),
77 };
78 match self.ath.db().get_user(session.user_id).await {
79 Ok(user) if user.is_active => Ok(Some(user)),
80 Ok(_) => Ok(None), Err(AuthError::NotFound) => Ok(None), Err(e) => Err(e),
83 }
84 })
85 }
86
87 fn check_role<'a>(&'a self, user_id: &'a UserId, role: &'a RoleName) -> AuthFuture<'a, bool> {
88 Box::pin(async move { self.ath.db().has_role(user_id, role).await })
89 }
90
91 fn check_permission<'a>(
92 &'a self,
93 user_id: &'a UserId,
94 permission: &'a PermissionName,
95 ) -> AuthFuture<'a, bool> {
96 Box::pin(async move { self.ath.db().has_permission(user_id, permission).await })
97 }
98
99 fn logout<'a>(&'a self, token: &'a SessionToken) -> AuthFuture<'a, ()> {
100 Box::pin(async move {
101 let _ = self.ath.db().delete_session(token).await?;
102 Ok(())
103 })
104 }
105
106 fn login_url(&self) -> &str {
107 &self.login_url
108 }
109
110 fn session_cookie_name(&self) -> &str {
111 self.ath.session_config().cookie_name
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use std::sync::Arc;
118
119 use chrono::{Duration, Utc};
120
121 use super::*;
122 use crate::handle::AllowThemBuilder;
123 use crate::sessions::{generate_token, hash_token};
124 use crate::types::{Email, PermissionName, RoleName};
125
126 async fn setup() -> EmbeddedAuthClient {
127 let ath = AllowThemBuilder::new("sqlite::memory:")
128 .cookie_secure(false)
129 .build()
130 .await
131 .unwrap();
132 EmbeddedAuthClient::new(ath, "/login")
133 }
134
135 #[tokio::test]
136 async fn validate_session_valid_token_returns_user() {
137 let client = setup().await;
138 let email = Email::new("valid@example.com".into()).unwrap();
139 let user = client
140 .ath
141 .db()
142 .create_user(email, "password123", None)
143 .await
144 .unwrap();
145
146 let token = generate_token();
147 let token_hash = hash_token(&token);
148 let expires = Utc::now() + Duration::hours(24);
149 client
150 .ath
151 .db()
152 .create_session(user.id, token_hash, None, None, expires)
153 .await
154 .unwrap();
155
156 let result = client.validate_session(&token).await.unwrap();
157 assert!(result.is_some());
158 assert_eq!(result.unwrap().email.as_str(), "valid@example.com");
159 }
160
161 #[tokio::test]
162 async fn validate_session_expired_token_returns_none() {
163 let client = setup().await;
164 let email = Email::new("expired@example.com".into()).unwrap();
165 let user = client
166 .ath
167 .db()
168 .create_user(email, "password123", None)
169 .await
170 .unwrap();
171
172 let token = generate_token();
173 let token_hash = hash_token(&token);
174 let expires = Utc::now() - Duration::hours(1);
175 client
176 .ath
177 .db()
178 .create_session(user.id, token_hash, None, None, expires)
179 .await
180 .unwrap();
181
182 let result = client.validate_session(&token).await.unwrap();
183 assert!(result.is_none());
184 }
185
186 #[tokio::test]
187 async fn validate_session_invalid_token_returns_none() {
188 let client = setup().await;
189 let token = generate_token();
190 let result = client.validate_session(&token).await.unwrap();
191 assert!(result.is_none());
192 }
193
194 #[tokio::test]
195 async fn validate_session_inactive_user_returns_none() {
196 let client = setup().await;
197 let email = Email::new("inactive@example.com".into()).unwrap();
198 let user = client
199 .ath
200 .db()
201 .create_user(email, "password123", None)
202 .await
203 .unwrap();
204
205 let token = generate_token();
206 let token_hash = hash_token(&token);
207 let expires = Utc::now() + Duration::hours(24);
208 client
209 .ath
210 .db()
211 .create_session(user.id, token_hash, None, None, expires)
212 .await
213 .unwrap();
214
215 client
216 .ath
217 .db()
218 .update_user_active(user.id, false)
219 .await
220 .unwrap();
221
222 let result = client.validate_session(&token).await.unwrap();
223 assert!(result.is_none());
224 }
225
226 #[tokio::test]
227 async fn validate_session_deleted_user_returns_none() {
228 let client = setup().await;
229 let email = Email::new("deleted@example.com".into()).unwrap();
230 let user = client
231 .ath
232 .db()
233 .create_user(email, "password123", None)
234 .await
235 .unwrap();
236
237 let token = generate_token();
238 let token_hash = hash_token(&token);
239 let expires = Utc::now() + Duration::hours(24);
240 client
241 .ath
242 .db()
243 .create_session(user.id, token_hash, None, None, expires)
244 .await
245 .unwrap();
246
247 client.ath.db().delete_user(user.id).await.unwrap();
248
249 let result = client.validate_session(&token).await.unwrap();
250 assert!(result.is_none());
251 }
252
253 #[tokio::test]
254 async fn check_role_returns_true_when_assigned() {
255 let client = setup().await;
256 let email = Email::new("roleuser@example.com".into()).unwrap();
257 let user = client
258 .ath
259 .db()
260 .create_user(email, "password123", None)
261 .await
262 .unwrap();
263
264 let rn = RoleName::new("admin");
265 let role = client.ath.db().create_role(&rn, None).await.unwrap();
266 client
267 .ath
268 .db()
269 .assign_role(&user.id, &role.id)
270 .await
271 .unwrap();
272
273 let result = client.check_role(&user.id, &rn).await.unwrap();
274 assert!(result);
275 }
276
277 #[tokio::test]
278 async fn check_role_returns_false_when_not_assigned() {
279 let client = setup().await;
280 let email = Email::new("norole@example.com".into()).unwrap();
281 let user = client
282 .ath
283 .db()
284 .create_user(email, "password123", None)
285 .await
286 .unwrap();
287
288 let rn = RoleName::new("admin");
289 let result = client.check_role(&user.id, &rn).await.unwrap();
290 assert!(!result);
291 }
292
293 #[tokio::test]
294 async fn check_permission_returns_true_direct() {
295 let client = setup().await;
296 let email = Email::new("permdirect@example.com".into()).unwrap();
297 let user = client
298 .ath
299 .db()
300 .create_user(email, "password123", None)
301 .await
302 .unwrap();
303
304 let pn = PermissionName::new("posts:write");
305 let perm = client.ath.db().create_permission(&pn, None).await.unwrap();
306 client
307 .ath
308 .db()
309 .assign_permission_to_user(&user.id, &perm.id)
310 .await
311 .unwrap();
312
313 let result = client.check_permission(&user.id, &pn).await.unwrap();
314 assert!(result);
315 }
316
317 #[tokio::test]
318 async fn check_permission_returns_true_via_role() {
319 let client = setup().await;
320 let email = Email::new("permviarole@example.com".into()).unwrap();
321 let user = client
322 .ath
323 .db()
324 .create_user(email, "password123", None)
325 .await
326 .unwrap();
327
328 let rn = RoleName::new("editor");
329 let role = client.ath.db().create_role(&rn, None).await.unwrap();
330
331 let pn = PermissionName::new("posts:read");
332 let perm = client.ath.db().create_permission(&pn, None).await.unwrap();
333 client
334 .ath
335 .db()
336 .assign_permission_to_role(&role.id, &perm.id)
337 .await
338 .unwrap();
339
340 client
341 .ath
342 .db()
343 .assign_role(&user.id, &role.id)
344 .await
345 .unwrap();
346
347 let result = client.check_permission(&user.id, &pn).await.unwrap();
348 assert!(result);
349 }
350
351 #[tokio::test]
352 async fn check_permission_returns_false_when_missing() {
353 let client = setup().await;
354 let email = Email::new("noperm@example.com".into()).unwrap();
355 let user = client
356 .ath
357 .db()
358 .create_user(email, "password123", None)
359 .await
360 .unwrap();
361
362 let pn = PermissionName::new("posts:delete");
363 let result = client.check_permission(&user.id, &pn).await.unwrap();
364 assert!(!result);
365 }
366
367 #[tokio::test]
368 async fn logout_deletes_session() {
369 let client = setup().await;
370 let email = Email::new("logout@example.com".into()).unwrap();
371 let user = client
372 .ath
373 .db()
374 .create_user(email, "password123", None)
375 .await
376 .unwrap();
377
378 let token = generate_token();
379 let token_hash = hash_token(&token);
380 let expires = Utc::now() + Duration::hours(24);
381 client
382 .ath
383 .db()
384 .create_session(user.id, token_hash, None, None, expires)
385 .await
386 .unwrap();
387
388 client.logout(&token).await.unwrap();
389
390 let result = client.validate_session(&token).await.unwrap();
391 assert!(result.is_none());
392 }
393
394 #[tokio::test]
395 async fn logout_nonexistent_token_succeeds() {
396 let client = setup().await;
397 let token = generate_token();
398 let result = client.logout(&token).await;
399 assert!(result.is_ok());
400 }
401
402 #[tokio::test]
403 async fn login_url_returns_configured_path() {
404 let ath = AllowThemBuilder::new("sqlite::memory:")
405 .build()
406 .await
407 .unwrap();
408 let client = EmbeddedAuthClient::new(ath, "/login");
409 assert_eq!(client.login_url(), "/login");
410 }
411
412 #[tokio::test]
413 async fn session_cookie_name_returns_config_name() {
414 let ath = AllowThemBuilder::new("sqlite::memory:")
415 .build()
416 .await
417 .unwrap();
418 let client = EmbeddedAuthClient::new(ath, "/login");
419 assert_eq!(client.session_cookie_name(), "allowthem_session");
420
421 let ath_custom = AllowThemBuilder::new("sqlite::memory:")
422 .cookie_name("my_session")
423 .build()
424 .await
425 .unwrap();
426 let client_custom = EmbeddedAuthClient::new(ath_custom, "/login");
427 assert_eq!(client_custom.session_cookie_name(), "my_session");
428 }
429
430 #[tokio::test]
432 async fn works_as_arc_dyn_auth_client() {
433 let ath = AllowThemBuilder::new("sqlite::memory:")
434 .build()
435 .await
436 .unwrap();
437 let _client: Arc<dyn AuthClient> = Arc::new(EmbeddedAuthClient::new(ath, "/login"));
438 }
439}