use crate::core::ModelAdmin;
use crate::core::model_admin::AdminUser;
use crate::server::admin_auth::{AdminLoginAuthenticator, AdminUserLoader};
use crate::types::{AdminError, AdminResult};
use async_trait::async_trait;
use dashmap::DashMap;
use parking_lot::RwLock;
use reinhardt_core::macros::injectable;
use reinhardt_di::{DiResult, Injectable, InjectionContext};
use std::sync::Arc;
#[injectable(scope = Singleton, prebuilt = true)]
#[derive(Clone)]
pub struct AdminSite {
name: String,
url_prefix: String,
registry: Arc<DashMap<String, Arc<dyn ModelAdmin>>>,
config: Arc<RwLock<AdminSiteConfig>>,
favicon_data: Arc<RwLock<Option<Vec<u8>>>>,
user_loader: Option<Arc<AdminUserLoader>>,
login_authenticator: Option<Arc<AdminLoginAuthenticator>>,
jwt_secret: Option<Vec<u8>>,
}
#[derive(Debug, Clone)]
pub struct AdminSiteConfig {
pub site_title: String,
pub site_header: String,
pub index_title: String,
pub list_per_page: usize,
pub enable_search: bool,
pub enable_filters: bool,
}
impl Default for AdminSiteConfig {
fn default() -> Self {
Self {
site_title: "Admin Panel".into(),
site_header: "Administration".into(),
index_title: "Dashboard".into(),
list_per_page: 100,
enable_search: true,
enable_filters: true,
}
}
}
impl AdminSite {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
url_prefix: "/admin".into(),
registry: Arc::new(DashMap::new()),
config: Arc::new(RwLock::new(AdminSiteConfig::default())),
favicon_data: Arc::new(RwLock::new(None)),
user_loader: None,
login_authenticator: None,
jwt_secret: None,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn set_url_prefix(&mut self, prefix: impl Into<String>) {
self.url_prefix = prefix.into();
}
pub fn url_prefix(&self) -> &str {
&self.url_prefix
}
pub fn set_favicon(&self, data: Vec<u8>) {
*self.favicon_data.write() = Some(data);
}
pub fn favicon_data(&self) -> Option<Vec<u8>> {
self.favicon_data.read().clone()
}
pub fn configure<F>(&self, f: F)
where
F: FnOnce(&mut AdminSiteConfig),
{
let mut config = self.config.write();
f(&mut config);
}
pub fn config(&self) -> AdminSiteConfig {
self.config.read().clone()
}
pub fn set_user_type<U>(&mut self) -> &mut Self
where
U: reinhardt_auth::BaseUser
+ AdminUser
+ reinhardt_db::orm::Model
+ Clone
+ Send
+ Sync
+ 'static,
<U as reinhardt_auth::BaseUser>::PrimaryKey: std::str::FromStr + ToString + Send + Sync,
<<U as reinhardt_auth::BaseUser>::PrimaryKey as std::str::FromStr>::Err: std::fmt::Debug,
<U as reinhardt_db::orm::Model>::PrimaryKey:
From<<U as reinhardt_auth::BaseUser>::PrimaryKey>,
{
self.user_loader = Some(Arc::new(
crate::server::admin_auth::create_admin_user_loader::<U>(),
));
self.login_authenticator = Some(Arc::new(
crate::server::admin_auth::create_admin_login_authenticator::<U>(),
));
self
}
pub(crate) fn user_loader(&self) -> Option<Arc<AdminUserLoader>> {
self.user_loader.clone()
}
pub(crate) fn login_authenticator(&self) -> Option<Arc<AdminLoginAuthenticator>> {
self.login_authenticator.clone()
}
pub fn set_jwt_secret(&mut self, secret: &[u8]) -> &mut Self {
self.jwt_secret = Some(secret.to_vec());
self
}
pub(crate) fn jwt_secret(&self) -> Option<&[u8]> {
self.jwt_secret.as_deref()
}
pub fn register(
&self,
model_name: impl Into<String>,
admin: impl ModelAdmin + 'static,
) -> AdminResult<()> {
let model_name = model_name.into();
let needle = model_name.to_lowercase();
if let Some(existing) = self
.registry
.iter()
.find(|e| e.key().to_lowercase() == needle)
{
return Err(AdminError::ValidationError(format!(
"Model '{}' is already registered (as '{}')",
model_name,
existing.key()
)));
}
self.registry.insert(model_name, Arc::new(admin));
Ok(())
}
pub fn unregister(&self, model_name: &str) -> AdminResult<()> {
let needle = model_name.to_lowercase();
let key = self
.registry
.iter()
.find(|entry| entry.key().to_lowercase() == needle)
.map(|entry| entry.key().clone())
.ok_or_else(|| AdminError::ModelNotRegistered(model_name.into()))?;
self.registry.remove(&key);
Ok(())
}
pub fn is_registered(&self, model_name: &str) -> bool {
let needle = model_name.to_lowercase();
self.registry
.iter()
.any(|entry| entry.key().to_lowercase() == needle)
}
pub fn get_model_admin(&self, model_name: &str) -> AdminResult<Arc<dyn ModelAdmin>> {
let needle = model_name.to_lowercase();
self.registry
.iter()
.find(|entry| entry.key().to_lowercase() == needle)
.map(|entry| Arc::clone(entry.value()))
.ok_or_else(|| AdminError::ModelNotRegistered(model_name.into()))
}
pub fn registered_models(&self) -> Vec<String> {
self.registry
.iter()
.map(|entry| entry.key().clone())
.collect()
}
pub fn model_count(&self) -> usize {
self.registry.len()
}
pub fn clear(&self) {
self.registry.clear();
}
}
#[async_trait]
impl Injectable for AdminSite {
async fn inject(ctx: &InjectionContext) -> DiResult<Self> {
ctx.get_singleton::<Self>()
.map(|arc| (*arc).clone())
.ok_or_else(|| reinhardt_di::DiError::NotRegistered {
type_name: "AdminSite".into(),
hint: "AdminSite must be registered as a singleton. \
Use admin_routes_with_di(site) and attach the returned \
DiRegistrationList via .with_di_registrations() on UnifiedRouter."
.into(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ModelAdminConfig;
use reinhardt_di::SingletonScope;
use rstest::rstest;
#[rstest]
fn test_admin_site_creation() {
let admin = AdminSite::new("Test Admin");
assert_eq!(admin.name(), "Test Admin");
assert_eq!(admin.url_prefix(), "/admin");
assert_eq!(admin.model_count(), 0);
}
#[rstest]
fn test_url_prefix() {
let mut admin = AdminSite::new("Admin");
admin.set_url_prefix("/manage");
assert_eq!(admin.url_prefix(), "/manage");
}
#[rstest]
fn test_configuration() {
let admin = AdminSite::new("Admin");
admin.configure(|config| {
config.site_title = "Custom Title".into();
config.list_per_page = 25;
});
let config = admin.config();
assert_eq!(config.site_title, "Custom Title");
assert_eq!(config.list_per_page, 25);
}
#[rstest]
fn test_register_and_unregister() {
let admin = AdminSite::new("Admin");
let model_admin = ModelAdminConfig::new("User");
assert!(!admin.is_registered("User"));
admin.register("User", model_admin).unwrap();
assert!(admin.is_registered("User"));
assert_eq!(admin.model_count(), 1);
admin.unregister("User").unwrap();
assert!(!admin.is_registered("User"));
assert_eq!(admin.model_count(), 0);
}
#[rstest]
fn test_unregister_nonexistent() {
let admin = AdminSite::new("Admin");
let result = admin.unregister("NonExistent");
assert!(result.is_err());
}
#[rstest]
fn test_get_model_admin() {
let admin = AdminSite::new("Admin");
let model_admin = ModelAdminConfig::new("User");
admin.register("User", model_admin).unwrap();
let retrieved = admin.get_model_admin("User");
assert!(retrieved.is_ok());
}
#[rstest]
fn test_get_nonexistent_model_admin() {
let admin = AdminSite::new("Admin");
let result = admin.get_model_admin("NonExistent");
assert!(result.is_err());
}
#[rstest]
fn test_registered_models() {
let admin = AdminSite::new("Admin");
admin
.register("User", ModelAdminConfig::new("User"))
.unwrap();
admin
.register("Post", ModelAdminConfig::new("Post"))
.unwrap();
let models = admin.registered_models();
assert_eq!(models.len(), 2);
assert!(models.contains(&"User".into()));
assert!(models.contains(&"Post".into()));
}
#[rstest]
fn test_clear() {
let admin = AdminSite::new("Admin");
admin
.register("User", ModelAdminConfig::new("User"))
.unwrap();
admin
.register("Post", ModelAdminConfig::new("Post"))
.unwrap();
assert_eq!(admin.model_count(), 2);
admin.clear();
assert_eq!(admin.model_count(), 0);
}
#[rstest]
fn test_duplicate_registration_returns_error() {
let admin = AdminSite::new("Admin");
admin
.register("User", ModelAdminConfig::new("User"))
.unwrap();
let result = admin.register("User", ModelAdminConfig::new("User"));
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("already registered"));
}
#[rstest]
fn test_default_config() {
let config = AdminSiteConfig::default();
assert_eq!(config.site_title, "Admin Panel");
assert_eq!(config.site_header, "Administration");
assert_eq!(config.list_per_page, 100);
assert!(config.enable_search);
assert!(config.enable_filters);
}
#[rstest]
fn test_set_arc_stores_admin_site_with_correct_type_id() {
let singleton = SingletonScope::new();
let site = Arc::new(AdminSite::new("Test Admin"));
singleton.set_arc(site);
assert!(
singleton.get::<AdminSite>().is_some(),
"AdminSite should be retrievable via get::<AdminSite>()"
);
}
#[rstest]
fn test_set_arc_preserves_favicon_data() {
let singleton = SingletonScope::new();
let site = Arc::new(AdminSite::new("Test Admin"));
let favicon = vec![0x89, 0x50, 0x4E, 0x47];
site.set_favicon(favicon.clone());
singleton.set_arc(site);
let retrieved = singleton.get::<AdminSite>().unwrap();
assert_eq!(retrieved.favicon_data(), Some(favicon));
}
#[rstest]
#[tokio::test]
async fn test_admin_site_inject_resolves_from_singleton() {
let singleton = Arc::new(SingletonScope::new());
let site = Arc::new(AdminSite::new("Injectable Admin"));
singleton.set_arc(site);
let ctx = reinhardt_di::InjectionContext::builder(singleton).build();
let result = AdminSite::inject(&ctx).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().name(), "Injectable Admin");
}
#[rstest]
#[tokio::test]
async fn test_admin_site_inject_returns_error_when_not_registered() {
let singleton = Arc::new(SingletonScope::new());
let ctx = reinhardt_di::InjectionContext::builder(singleton).build();
let result = AdminSite::inject(&ctx).await;
assert!(result.is_err());
let err = result.err().unwrap();
assert!(
err.to_string().contains("AdminSite"),
"Error should mention AdminSite, got: {}",
err
);
}
#[rstest]
#[tokio::test]
async fn test_admin_site_inject_error_hint_mentions_routes_with_di() {
let singleton = Arc::new(SingletonScope::new());
let ctx = reinhardt_di::InjectionContext::builder(singleton).build();
let result = AdminSite::inject(&ctx).await;
assert!(result.is_err());
let err = result.err().unwrap();
assert!(
err.to_string().contains("admin_routes_with_di"),
"Error hint should mention admin_routes_with_di, got: {}",
err
);
}
#[rstest]
fn test_get_model_admin_case_insensitive() {
let admin = AdminSite::new("Admin");
admin
.register("User", ModelAdminConfig::new("User"))
.unwrap();
assert!(admin.get_model_admin("User").is_ok());
assert!(admin.get_model_admin("user").is_ok());
assert!(admin.get_model_admin("USER").is_ok());
assert!(admin.get_model_admin("uSeR").is_ok());
assert!(admin.get_model_admin("nonexistent").is_err());
}
#[rstest]
fn test_is_registered_case_insensitive() {
let admin = AdminSite::new("Admin");
admin
.register("User", ModelAdminConfig::new("User"))
.unwrap();
assert!(admin.is_registered("User"));
assert!(admin.is_registered("user"));
assert!(admin.is_registered("USER"));
assert!(!admin.is_registered("Post"));
}
#[rstest]
fn test_register_rejects_case_insensitive_duplicate() {
let admin = AdminSite::new("Admin");
admin
.register("User", ModelAdminConfig::new("User"))
.unwrap();
let result = admin.register("user", ModelAdminConfig::new("user"));
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("already registered")
);
}
#[rstest]
fn test_unregister_case_insensitive() {
let admin = AdminSite::new("Admin");
admin
.register("User", ModelAdminConfig::new("User"))
.unwrap();
admin.unregister("user").unwrap();
assert!(!admin.is_registered("User"));
}
}