1use crate::core::ModelAdmin;
7use crate::core::model_admin::AdminUser;
8use crate::server::admin_auth::{AdminLoginAuthenticator, AdminUserLoader};
9use crate::types::{AdminError, AdminResult};
10use async_trait::async_trait;
11use dashmap::DashMap;
12use parking_lot::RwLock;
13use reinhardt_core::macros::injectable;
14use reinhardt_di::{DiResult, FactoryOutput, Injectable, InjectionContext};
15use std::sync::Arc;
16
17#[injectable(scope = Singleton, prebuilt = true)]
28#[derive(Clone)]
29pub struct AdminSite {
30 name: String,
32
33 url_prefix: String,
35
36 registry: Arc<DashMap<String, Arc<dyn ModelAdmin>>>,
38
39 config: Arc<RwLock<AdminSiteConfig>>,
41
42 favicon_data: Arc<RwLock<Option<Vec<u8>>>>,
44
45 user_loader: Option<Arc<AdminUserLoader>>,
51
52 login_authenticator: Option<Arc<AdminLoginAuthenticator>>,
58
59 jwt_secret: Option<Vec<u8>>,
63}
64
65#[reinhardt_di::injectable_key]
67pub struct AdminSiteKey;
68
69#[derive(Debug, Clone)]
71pub struct AdminSiteConfig {
72 pub site_title: String,
74
75 pub site_header: String,
77
78 pub index_title: String,
80
81 pub list_per_page: usize,
83
84 pub enable_search: bool,
86
87 pub enable_filters: bool,
89}
90
91impl Default for AdminSiteConfig {
92 fn default() -> Self {
93 Self {
94 site_title: "Admin Panel".into(),
95 site_header: "Administration".into(),
96 index_title: "Dashboard".into(),
97 list_per_page: 100,
98 enable_search: true,
99 enable_filters: true,
100 }
101 }
102}
103
104impl AdminSite {
105 pub fn new(name: impl Into<String>) -> Self {
115 Self {
116 name: name.into(),
117 url_prefix: "/admin".into(),
118 registry: Arc::new(DashMap::new()),
119 config: Arc::new(RwLock::new(AdminSiteConfig::default())),
120 favicon_data: Arc::new(RwLock::new(None)),
121 user_loader: None,
122 login_authenticator: None,
123 jwt_secret: None,
124 }
125 }
126
127 pub fn name(&self) -> &str {
138 &self.name
139 }
140
141 pub fn set_url_prefix(&mut self, prefix: impl Into<String>) {
153 self.url_prefix = prefix.into();
154 }
155
156 pub fn url_prefix(&self) -> &str {
158 &self.url_prefix
159 }
160
161 pub fn set_favicon(&self, data: Vec<u8>) {
173 *self.favicon_data.write() = Some(data);
174 }
175
176 pub fn favicon_data(&self) -> Option<Vec<u8>> {
180 self.favicon_data.read().clone()
181 }
182
183 pub fn configure<F>(&self, f: F)
198 where
199 F: FnOnce(&mut AdminSiteConfig),
200 {
201 let mut config = self.config.write();
202 f(&mut config);
203 }
204
205 pub fn config(&self) -> AdminSiteConfig {
207 self.config.read().clone()
208 }
209
210 pub fn set_user_type<U>(&mut self) -> &mut Self
234 where
235 U: reinhardt_auth::BaseUser
236 + AdminUser
237 + reinhardt_db::orm::Model
238 + Clone
239 + Send
240 + Sync
241 + 'static,
242 <U as reinhardt_auth::BaseUser>::PrimaryKey: std::str::FromStr + ToString + Send + Sync,
243 <<U as reinhardt_auth::BaseUser>::PrimaryKey as std::str::FromStr>::Err: std::fmt::Debug,
244 <U as reinhardt_db::orm::Model>::PrimaryKey:
245 From<<U as reinhardt_auth::BaseUser>::PrimaryKey>,
246 {
247 self.user_loader = Some(Arc::new(
248 crate::server::admin_auth::create_admin_user_loader::<U>(),
249 ));
250 self.login_authenticator = Some(Arc::new(
251 crate::server::admin_auth::create_admin_login_authenticator::<U>(),
252 ));
253 self
254 }
255
256 pub(crate) fn user_loader(&self) -> Option<Arc<AdminUserLoader>> {
258 self.user_loader.clone()
259 }
260
261 pub(crate) fn login_authenticator(&self) -> Option<Arc<AdminLoginAuthenticator>> {
263 self.login_authenticator.clone()
264 }
265
266 pub fn set_jwt_secret(&mut self, secret: &[u8]) -> &mut Self {
280 self.jwt_secret = Some(secret.to_vec());
281 self
282 }
283
284 pub(crate) fn jwt_secret(&self) -> Option<&[u8]> {
286 self.jwt_secret.as_deref()
287 }
288
289 pub fn register(
306 &self,
307 model_name: impl Into<String>,
308 admin: impl ModelAdmin + 'static,
309 ) -> AdminResult<()> {
310 let model_name = model_name.into();
311 let needle = model_name.to_lowercase();
314 if let Some(existing) = self
315 .registry
316 .iter()
317 .find(|e| e.key().to_lowercase() == needle)
318 {
319 return Err(AdminError::ValidationError(format!(
320 "Model '{}' is already registered (as '{}')",
321 model_name,
322 existing.key()
323 )));
324 }
325 self.registry.insert(model_name, Arc::new(admin));
326 Ok(())
327 }
328
329 pub fn unregister(&self, model_name: &str) -> AdminResult<()> {
341 let needle = model_name.to_lowercase();
342 let key = self
343 .registry
344 .iter()
345 .find(|entry| entry.key().to_lowercase() == needle)
346 .map(|entry| entry.key().clone())
347 .ok_or_else(|| AdminError::ModelNotRegistered(model_name.into()))?;
348 self.registry.remove(&key);
349 Ok(())
350 }
351
352 pub fn is_registered(&self, model_name: &str) -> bool {
363 let needle = model_name.to_lowercase();
364 self.registry
365 .iter()
366 .any(|entry| entry.key().to_lowercase() == needle)
367 }
368
369 pub fn get_model_admin(&self, model_name: &str) -> AdminResult<Arc<dyn ModelAdmin>> {
381 let needle = model_name.to_lowercase();
382 self.registry
383 .iter()
384 .find(|entry| entry.key().to_lowercase() == needle)
385 .map(|entry| Arc::clone(entry.value()))
386 .ok_or_else(|| AdminError::ModelNotRegistered(model_name.into()))
387 }
388
389 pub fn registered_models(&self) -> Vec<String> {
400 self.registry
401 .iter()
402 .map(|entry| entry.key().clone())
403 .collect()
404 }
405
406 pub fn model_count(&self) -> usize {
417 self.registry.len()
418 }
419
420 pub fn clear(&self) {
432 self.registry.clear();
433 }
434}
435
436#[async_trait]
442impl Injectable for AdminSite {
443 async fn inject(ctx: &InjectionContext) -> DiResult<Self> {
444 ctx.get_singleton::<Self>()
445 .map(|arc| (*arc).clone())
446 .ok_or_else(|| reinhardt_di::DiError::NotRegistered {
447 type_name: "AdminSite".into(),
448 hint: "AdminSite must be registered as a singleton. \
449 Use admin_routes_with_di(site) and attach the returned \
450 DiRegistrationList via .with_di_registrations() on UnifiedRouter."
451 .into(),
452 })
453 }
454}
455
456#[reinhardt_di::injectable(scope = "singleton")]
457async fn admin_site_provider(#[inject] site: AdminSite) -> FactoryOutput<AdminSiteKey, AdminSite> {
458 FactoryOutput::new(site)
459}
460
461#[cfg(all(test, server))]
462mod tests {
463 use super::*;
464 use crate::core::ModelAdminConfig;
465 use reinhardt_di::SingletonScope;
466 use rstest::rstest;
467
468 #[rstest]
469 fn test_admin_site_creation() {
470 let admin = AdminSite::new("Test Admin");
471 assert_eq!(admin.name(), "Test Admin");
472 assert_eq!(admin.url_prefix(), "/admin");
473 assert_eq!(admin.model_count(), 0);
474 }
475
476 #[rstest]
477 #[tokio::test]
478 async fn test_admin_site_resolves_through_keyed_provider() {
479 let singleton = Arc::new(SingletonScope::new());
480 let site = Arc::new(AdminSite::new("Registry Admin"));
481 singleton.set_arc(site);
482 let ctx = reinhardt_di::InjectionContext::builder(singleton).build();
483
484 let result =
485 reinhardt_di::Depends::<AdminSiteKey, AdminSite>::resolve_from_registry(&ctx, true)
486 .await;
487
488 assert!(result.is_ok());
489 assert_eq!(result.unwrap().name(), "Registry Admin");
490 }
491
492 #[rstest]
493 fn test_url_prefix() {
494 let mut admin = AdminSite::new("Admin");
495 admin.set_url_prefix("/manage");
496 assert_eq!(admin.url_prefix(), "/manage");
497 }
498
499 #[rstest]
500 fn test_configuration() {
501 let admin = AdminSite::new("Admin");
502 admin.configure(|config| {
503 config.site_title = "Custom Title".into();
504 config.list_per_page = 25;
505 });
506
507 let config = admin.config();
508 assert_eq!(config.site_title, "Custom Title");
509 assert_eq!(config.list_per_page, 25);
510 }
511
512 #[rstest]
513 fn test_register_and_unregister() {
514 let admin = AdminSite::new("Admin");
515 let model_admin = ModelAdminConfig::new("User");
516
517 assert!(!admin.is_registered("User"));
518
519 admin.register("User", model_admin).unwrap();
520 assert!(admin.is_registered("User"));
521 assert_eq!(admin.model_count(), 1);
522
523 admin.unregister("User").unwrap();
524 assert!(!admin.is_registered("User"));
525 assert_eq!(admin.model_count(), 0);
526 }
527
528 #[rstest]
529 fn test_unregister_nonexistent() {
530 let admin = AdminSite::new("Admin");
531 let result = admin.unregister("NonExistent");
532 assert!(result.is_err());
533 }
534
535 #[rstest]
536 fn test_get_model_admin() {
537 let admin = AdminSite::new("Admin");
538 let model_admin = ModelAdminConfig::new("User");
539
540 admin.register("User", model_admin).unwrap();
541
542 let retrieved = admin.get_model_admin("User");
543 assert!(retrieved.is_ok());
544 }
545
546 #[rstest]
547 fn test_get_nonexistent_model_admin() {
548 let admin = AdminSite::new("Admin");
549 let result = admin.get_model_admin("NonExistent");
550 assert!(result.is_err());
551 }
552
553 #[rstest]
554 fn test_registered_models() {
555 let admin = AdminSite::new("Admin");
556
557 admin
558 .register("User", ModelAdminConfig::new("User"))
559 .unwrap();
560 admin
561 .register("Post", ModelAdminConfig::new("Post"))
562 .unwrap();
563
564 let models = admin.registered_models();
565 assert_eq!(models.len(), 2);
566 assert!(models.contains(&"User".into()));
567 assert!(models.contains(&"Post".into()));
568 }
569
570 #[rstest]
571 fn test_clear() {
572 let admin = AdminSite::new("Admin");
573
574 admin
575 .register("User", ModelAdminConfig::new("User"))
576 .unwrap();
577 admin
578 .register("Post", ModelAdminConfig::new("Post"))
579 .unwrap();
580
581 assert_eq!(admin.model_count(), 2);
582
583 admin.clear();
584 assert_eq!(admin.model_count(), 0);
585 }
586
587 #[rstest]
588 fn test_duplicate_registration_returns_error() {
589 let admin = AdminSite::new("Admin");
591 admin
592 .register("User", ModelAdminConfig::new("User"))
593 .unwrap();
594
595 let result = admin.register("User", ModelAdminConfig::new("User"));
597
598 assert!(result.is_err());
600 let err = result.unwrap_err();
601 assert!(err.to_string().contains("already registered"));
602 }
603
604 #[rstest]
605 fn test_default_config() {
606 let config = AdminSiteConfig::default();
607 assert_eq!(config.site_title, "Admin Panel");
608 assert_eq!(config.site_header, "Administration");
609 assert_eq!(config.list_per_page, 100);
610 assert!(config.enable_search);
611 assert!(config.enable_filters);
612 }
613
614 #[rstest]
615 fn test_set_arc_stores_admin_site_with_correct_type_id() {
616 let singleton = SingletonScope::new();
618 let site = Arc::new(AdminSite::new("Test Admin"));
619
620 singleton.set_arc(site);
622
623 assert!(
625 singleton.get::<AdminSite>().is_some(),
626 "AdminSite should be retrievable via get::<AdminSite>()"
627 );
628 }
629
630 #[rstest]
631 fn test_set_arc_preserves_favicon_data() {
632 let singleton = SingletonScope::new();
634 let site = Arc::new(AdminSite::new("Test Admin"));
635 let favicon = vec![0x89, 0x50, 0x4E, 0x47]; site.set_favicon(favicon.clone());
639 singleton.set_arc(site);
640
641 let retrieved = singleton.get::<AdminSite>().unwrap();
643 assert_eq!(retrieved.favicon_data(), Some(favicon));
644 }
645
646 #[rstest]
647 #[tokio::test]
648 async fn test_admin_site_inject_resolves_from_singleton() {
649 let singleton = Arc::new(SingletonScope::new());
651 let site = Arc::new(AdminSite::new("Injectable Admin"));
652 singleton.set_arc(site);
653 let ctx = reinhardt_di::InjectionContext::builder(singleton).build();
654
655 let result = AdminSite::inject(&ctx).await;
657
658 assert!(result.is_ok());
660 assert_eq!(result.unwrap().name(), "Injectable Admin");
661 }
662
663 #[rstest]
664 #[tokio::test]
665 async fn test_admin_site_inject_returns_error_when_not_registered() {
666 let singleton = Arc::new(SingletonScope::new());
668 let ctx = reinhardt_di::InjectionContext::builder(singleton).build();
669
670 let result = AdminSite::inject(&ctx).await;
672
673 assert!(result.is_err());
675 let err = result.err().unwrap();
676 assert!(
677 err.to_string().contains("AdminSite"),
678 "Error should mention AdminSite, got: {}",
679 err
680 );
681 }
682
683 #[rstest]
684 #[tokio::test]
685 async fn test_admin_site_inject_error_hint_mentions_routes_with_di() {
686 let singleton = Arc::new(SingletonScope::new());
688 let ctx = reinhardt_di::InjectionContext::builder(singleton).build();
689
690 let result = AdminSite::inject(&ctx).await;
692
693 assert!(result.is_err());
695 let err = result.err().unwrap();
696 assert!(
697 err.to_string().contains("admin_routes_with_di"),
698 "Error hint should mention admin_routes_with_di, got: {}",
699 err
700 );
701 }
702
703 #[rstest]
706 fn test_get_model_admin_case_insensitive() {
707 let admin = AdminSite::new("Admin");
708 admin
709 .register("User", ModelAdminConfig::new("User"))
710 .unwrap();
711
712 assert!(admin.get_model_admin("User").is_ok());
713 assert!(admin.get_model_admin("user").is_ok());
714 assert!(admin.get_model_admin("USER").is_ok());
715 assert!(admin.get_model_admin("uSeR").is_ok());
716 assert!(admin.get_model_admin("nonexistent").is_err());
717 }
718
719 #[rstest]
720 fn test_is_registered_case_insensitive() {
721 let admin = AdminSite::new("Admin");
722 admin
723 .register("User", ModelAdminConfig::new("User"))
724 .unwrap();
725
726 assert!(admin.is_registered("User"));
727 assert!(admin.is_registered("user"));
728 assert!(admin.is_registered("USER"));
729 assert!(!admin.is_registered("Post"));
730 }
731
732 #[rstest]
733 fn test_register_rejects_case_insensitive_duplicate() {
734 let admin = AdminSite::new("Admin");
735 admin
736 .register("User", ModelAdminConfig::new("User"))
737 .unwrap();
738
739 let result = admin.register("user", ModelAdminConfig::new("user"));
740 assert!(result.is_err());
741 assert!(
742 result
743 .unwrap_err()
744 .to_string()
745 .contains("already registered")
746 );
747 }
748
749 #[rstest]
750 fn test_unregister_case_insensitive() {
751 let admin = AdminSite::new("Admin");
752 admin
753 .register("User", ModelAdminConfig::new("User"))
754 .unwrap();
755
756 admin.unregister("user").unwrap();
757 assert!(!admin.is_registered("User"));
758 }
759}