1use crate::core::AdminUser;
11use async_trait::async_trait;
12use reinhardt_auth::BaseUser;
13use reinhardt_db::orm::{CustomManager, DatabaseConnection, Model};
14use reinhardt_di::{DiError, DiResult, Injectable, InjectionContext};
15use reinhardt_http::AuthState;
16use std::future::Future;
17use std::pin::Pin;
18use std::sync::Arc;
19
20pub(crate) type AdminUserLoaderFn = Arc<
26 dyn Fn(
27 String,
28 Arc<DatabaseConnection>,
29 ) -> Pin<Box<dyn Future<Output = Result<Arc<dyn AdminUser>, DiError>> + Send>>
30 + Send
31 + Sync,
32>;
33
34#[derive(Clone)]
39pub(crate) struct AdminUserLoader(pub(crate) AdminUserLoaderFn);
40
41#[derive(Clone)]
73pub struct AdminAuthenticatedUser(pub Arc<dyn AdminUser>);
74
75impl std::fmt::Debug for AdminAuthenticatedUser {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 f.debug_struct("AdminAuthenticatedUser")
78 .field("username", &self.0.get_username())
79 .finish()
80 }
81}
82
83#[async_trait]
84impl Injectable for AdminAuthenticatedUser {
85 async fn inject(ctx: &InjectionContext) -> DiResult<Self> {
86 let request = ctx.get_http_request().ok_or_else(|| {
88 DiError::Authentication(
89 "AdminAuthenticatedUser: No HTTP request available in InjectionContext".to_string(),
90 )
91 })?;
92
93 let auth_state: AuthState = request.extensions.get().ok_or_else(|| {
95 DiError::Authentication(
96 "AdminAuthenticatedUser: No AuthState found in request extensions".to_string(),
97 )
98 })?;
99
100 if !auth_state.is_authenticated() {
101 return Err(DiError::Authentication(
102 "AdminAuthenticatedUser: User is not authenticated".to_string(),
103 ));
104 }
105
106 let user_id = auth_state.user_id().to_string();
107
108 let loader: Arc<AdminUserLoader> =
111 ctx.get_singleton::<AdminUserLoader>()
112 .ok_or_else(|| DiError::NotRegistered {
113 type_name: "AdminUserLoader".into(),
114 hint: "Call AdminSite::set_user_type::<U>() before building admin routes, \
115 or use the default by calling admin_routes_with_di() which \
116 registers AdminDefaultUser as a fallback."
117 .into(),
118 })?;
119
120 let db: Arc<DatabaseConnection> = ctx
122 .get_singleton::<DatabaseConnection>()
123 .or_else(|| ctx.get_request::<DatabaseConnection>())
124 .ok_or_else(|| {
125 ::tracing::warn!(
126 "AdminAuthenticatedUser: DatabaseConnection not available for user resolution"
127 );
128 DiError::Internal {
129 message:
130 "AdminAuthenticatedUser: DatabaseConnection not registered in DI context"
131 .to_string(),
132 }
133 })?;
134
135 let user = (loader.0)(user_id, db).await?;
137
138 if !user.is_active() {
140 return Err(DiError::Authentication(
141 "User account is not active".to_string(),
142 ));
143 }
144
145 if !user.is_staff() {
147 return Err(DiError::Authentication(
148 "User does not have staff privileges".to_string(),
149 ));
150 }
151
152 Ok(AdminAuthenticatedUser(user))
153 }
154}
155
156pub(crate) fn create_admin_user_loader<U>() -> AdminUserLoader
170where
171 U: BaseUser + AdminUser + Model + Clone + Send + Sync + 'static,
172 <U as BaseUser>::PrimaryKey: std::str::FromStr + ToString + Send + Sync,
173 <<U as BaseUser>::PrimaryKey as std::str::FromStr>::Err: std::fmt::Debug,
174 <U as Model>::PrimaryKey: From<<U as BaseUser>::PrimaryKey>,
175{
176 let loader: AdminUserLoaderFn = Arc::new(move |user_id, db| {
177 Box::pin(async move {
178 let pk = user_id
180 .parse::<<U as BaseUser>::PrimaryKey>()
181 .map_err(|e| {
182 ::tracing::warn!(
183 user_id = %user_id,
184 error = ?e,
185 "AdminUserLoader: failed to parse user_id"
186 );
187 DiError::Authentication("AdminUserLoader: Invalid user_id format".to_string())
188 })?;
189
190 let model_pk = <U as Model>::PrimaryKey::from(pk);
191
192 let user = U::objects()
194 .get(model_pk)
195 .first_with_db(&db)
196 .await
197 .map_err(|e| {
198 ::tracing::warn!(error = ?e, "AdminUserLoader: Database query failed");
199 DiError::Internal {
200 message: "AdminUserLoader: Database query failed".to_string(),
201 }
202 })?
203 .ok_or_else(|| {
204 ::tracing::warn!(
205 user_id = %user_id,
206 "AdminUserLoader: User not found in database"
207 );
208 DiError::NotFound("AdminUserLoader: User not found".to_string())
209 })?;
210
211 Ok(Arc::new(user) as Arc<dyn AdminUser>)
212 })
213 });
214
215 AdminUserLoader(loader)
216}
217
218pub(crate) struct AuthenticatedUserInfo {
223 pub(crate) user_id: String,
225 pub(crate) username: String,
227 pub(crate) is_staff: bool,
229 pub(crate) is_superuser: bool,
231}
232
233pub(crate) type AdminLoginAuthenticatorFn = Arc<
238 dyn Fn(
239 String,
240 String,
241 Arc<DatabaseConnection>,
242 )
243 -> Pin<Box<dyn Future<Output = Result<Option<AuthenticatedUserInfo>, DiError>> + Send>>
244 + Send
245 + Sync,
246>;
247
248#[derive(Clone)]
254pub struct AdminLoginAuthenticator(pub(crate) AdminLoginAuthenticatorFn);
255
256#[async_trait]
257impl Injectable for AdminLoginAuthenticator {
258 async fn inject(ctx: &InjectionContext) -> DiResult<Self> {
259 ctx.get_singleton::<AdminLoginAuthenticator>()
260 .map(|arc| (*arc).clone())
261 .ok_or_else(|| DiError::NotRegistered {
262 type_name: "AdminLoginAuthenticator".into(),
263 hint: "Call AdminSite::set_user_type::<U>() or use admin_routes_with_di() \
264 which registers AdminDefaultUser as a fallback."
265 .into(),
266 })
267 }
268}
269
270pub(crate) fn create_admin_login_authenticator<U>() -> AdminLoginAuthenticator
278where
279 U: BaseUser + AdminUser + Model + Clone + Send + Sync + 'static,
280 <U as BaseUser>::PrimaryKey: ToString + Send + Sync,
281{
282 use reinhardt_db::orm::{CustomManager, Filter, FilterOperator, FilterValue};
283
284 let authenticator: AdminLoginAuthenticatorFn = Arc::new(move |username, password, db| {
285 Box::pin(async move {
286 let user: Option<U> = U::objects()
288 .filter(Filter::new(
289 "username",
290 FilterOperator::Eq,
291 FilterValue::String(username.clone()),
292 ))
293 .first_with_db(&db)
294 .await
295 .map_err(|e| {
296 ::tracing::warn!(error = ?e, "AdminLoginAuthenticator: Database query failed");
297 DiError::Internal {
298 message: "AdminLoginAuthenticator: Database query failed".to_string(),
299 }
300 })?;
301
302 let Some(user) = user else {
303 ::tracing::debug!(username = %username, "AdminLoginAuthenticator: User not found");
304 return Ok(None);
305 };
306
307 let password_valid = user.check_password(&password).map_err(|e| {
309 ::tracing::warn!(error = ?e, "AdminLoginAuthenticator: Password check failed");
310 DiError::Internal {
311 message: "AdminLoginAuthenticator: Password verification error".to_string(),
312 }
313 })?;
314
315 if !password_valid {
316 ::tracing::debug!(username = %username, "AdminLoginAuthenticator: Invalid password");
317 return Ok(None);
318 }
319
320 if !AdminUser::is_active(&user) {
322 ::tracing::debug!(username = %username, "AdminLoginAuthenticator: User is not active");
323 return Ok(None);
324 }
325
326 if !user.is_staff() {
327 ::tracing::debug!(username = %username, "AdminLoginAuthenticator: User is not staff");
328 return Ok(None);
329 }
330
331 let user_id = user
332 .primary_key()
333 .map(|pk| pk.to_string())
334 .unwrap_or_default();
335
336 Ok(Some(AuthenticatedUserInfo {
337 user_id,
338 username: AdminUser::get_username(&user).to_string(),
339 is_staff: user.is_staff(),
340 is_superuser: user.is_superuser(),
341 }))
342 })
343 });
344
345 AdminLoginAuthenticator(authenticator)
346}
347
348#[cfg(all(test, server))]
349mod tests {
350 use super::*;
351 use reinhardt_di::SingletonScope;
352 use rstest::rstest;
353
354 #[rstest]
355 #[tokio::test]
356 async fn test_inject_returns_error_when_no_http_request() {
357 let singleton = Arc::new(SingletonScope::new());
359 let ctx = InjectionContext::builder(singleton).build();
360
361 let result = AdminAuthenticatedUser::inject(&ctx).await;
363
364 assert!(result.is_err());
366 let err = result.unwrap_err();
367 assert!(
368 err.to_string().contains("No HTTP request"),
369 "Expected 'No HTTP request' error, got: {}",
370 err
371 );
372 }
373
374 #[rstest]
375 #[tokio::test]
376 async fn test_inject_returns_error_when_no_auth_state() {
377 let singleton = Arc::new(SingletonScope::new());
379 let request = reinhardt_http::Request::builder()
380 .uri("/admin/test")
381 .build()
382 .expect("Failed to build test request");
383 let ctx = InjectionContext::builder(singleton)
384 .with_request(request)
385 .build();
386
387 let result = AdminAuthenticatedUser::inject(&ctx).await;
389
390 assert!(result.is_err());
392 let err = result.unwrap_err();
393 assert!(
394 err.to_string().contains("No AuthState"),
395 "Expected 'No AuthState' error, got: {}",
396 err
397 );
398 }
399
400 #[rstest]
401 #[tokio::test]
402 async fn test_inject_returns_error_when_not_authenticated() {
403 let singleton = Arc::new(SingletonScope::new());
405 let request = reinhardt_http::Request::builder()
406 .uri("/admin/test")
407 .build()
408 .expect("Failed to build test request");
409 request.extensions.insert(AuthState::anonymous());
411 let ctx = InjectionContext::builder(singleton)
412 .with_request(request)
413 .build();
414
415 let result = AdminAuthenticatedUser::inject(&ctx).await;
417
418 assert!(result.is_err());
420 let err = result.unwrap_err();
421 assert!(
422 err.to_string().contains("not authenticated"),
423 "Expected 'not authenticated' error, got: {}",
424 err
425 );
426 }
427
428 #[rstest]
429 #[tokio::test]
430 async fn test_inject_returns_error_when_no_loader_registered() {
431 let singleton = Arc::new(SingletonScope::new());
433 let request = reinhardt_http::Request::builder()
434 .uri("/admin/test")
435 .build()
436 .expect("Failed to build test request");
437 request
438 .extensions
439 .insert(AuthState::authenticated("user-123", true, true));
440 let ctx = InjectionContext::builder(singleton)
441 .with_request(request)
442 .build();
443
444 let result = AdminAuthenticatedUser::inject(&ctx).await;
446
447 assert!(result.is_err());
449 let err = result.unwrap_err();
450 assert!(
451 err.to_string().contains("AdminUserLoader"),
452 "Expected 'AdminUserLoader' error, got: {}",
453 err
454 );
455 }
456
457 #[rstest]
458 #[tokio::test]
459 async fn test_inject_returns_error_when_no_database_connection() {
460 let singleton = Arc::new(SingletonScope::new());
462 let loader = AdminUserLoader(Arc::new(|_user_id, _db| {
463 Box::pin(async { Err(DiError::NotFound("should not be called".to_string())) })
464 }));
465 singleton.set_arc(Arc::new(loader));
466 let request = reinhardt_http::Request::builder()
467 .uri("/admin/test")
468 .build()
469 .expect("Failed to build test request");
470 request
471 .extensions
472 .insert(AuthState::authenticated("user-123", true, true));
473 let ctx = InjectionContext::builder(singleton)
474 .with_request(request)
475 .build();
476
477 let result = AdminAuthenticatedUser::inject(&ctx).await;
479
480 assert!(result.is_err());
482 let err = result.unwrap_err();
483 assert!(
484 err.to_string().contains("DatabaseConnection"),
485 "Expected error mentioning DatabaseConnection, got: {}",
486 err
487 );
488 }
489
490 #[rstest]
491 fn test_admin_user_loader_can_be_stored_in_singleton_scope() {
492 let singleton = SingletonScope::new();
494 let loader = AdminUserLoader(Arc::new(|_user_id, _db| {
495 Box::pin(async { Err(DiError::NotFound("test loader".to_string())) })
496 }));
497
498 singleton.set_arc(Arc::new(loader));
500
501 let retrieved = singleton.get::<AdminUserLoader>();
503 assert!(
504 retrieved.is_some(),
505 "AdminUserLoader should be retrievable from singleton scope"
506 );
507 }
508}