navigation_dto.rs 371 lignes · 10719 octets
use rustwarden_core::i18n::extractor::Locale;
use rustwarden_core::i18n::service::I18nService;
/// Navigation Data Transfer Objects
/// Feature 004: UI Post-Auth
///
/// Defines the navigation menu structure for the application UI.
use serde::{Deserialize, Serialize};
use std::sync::Arc;

/// A single navigation item
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NavigationItemDTO {
    /// Item identifier (e.g., "home", "settings")
    pub id: String,

    /// Display label
    pub label: String,

    /// Icon name (e.g., "home", "settings")
    pub icon: String,

    /// Target URL
    pub url: String,

    /// Whether this item is active (current page)
    pub active: bool,

    /// Whether this item is enabled
    pub enabled: bool,

    /// Badge text (e.g., "New") - empty string if None (for Askama compatibility)
    pub badge: String,

    /// Helper flag for Askama compatibility (true if badge is not empty)
    #[serde(skip_serializing, skip_deserializing)]
    pub has_badge: bool,

    /// Message when disabled - empty string if None (for Askama compatibility)
    pub disabled_message: String,

    /// Helper flag for Askama compatibility (true if disabled_message is not empty)
    #[serde(skip_serializing, skip_deserializing)]
    pub has_disabled_message: bool,

    /// Order for sorting (lower = first, used internally)
    #[serde(skip_serializing, skip_deserializing)]
    pub order: i32,
}

impl NavigationItemDTO {
    /// Create an active navigation item
    ///
    /// # Examples
    ///
    /// ```
    /// use rustwarden_ui::dto::navigation_dto::NavigationItemDTO;
    ///
    /// let item = NavigationItemDTO::active(
    ///     "home",
    ///     "Home",
    ///     "home",
    ///     "/home"
    /// );
    ///
    /// assert!(item.active);
    /// assert!(item.enabled);
    /// ```
    pub fn active(id: &str, label: &str, icon: &str, url: &str) -> Self {
        Self {
            id: id.to_string(),
            label: label.to_string(),
            icon: icon.to_string(),
            url: url.to_string(),
            active: true,
            enabled: true,
            badge: String::new(),
            has_badge: false,
            disabled_message: String::new(),
            has_disabled_message: false,
            order: 0,
        }
    }

    /// Create a disabled navigation item
    ///
    /// # Examples
    ///
    /// ```
    /// use rustwarden_ui::dto::navigation_dto::NavigationItemDTO;
    ///
    /// let item = NavigationItemDTO::disabled(
    ///     "module",
    ///     "Module",
    ///     "box",
    ///     "/module",
    ///     "Soon",
    ///     Some("Feature in development")
    /// );
    ///
    /// assert!(!item.active);
    /// assert!(!item.enabled);
    /// assert_eq!(item.badge, "Soon".to_string());
    /// ```
    pub fn disabled(
        id: &str,
        label: &str,
        icon: &str,
        url: &str,
        badge: &str,
        message: Option<&str>,
    ) -> Self {
        let badge_str = badge.to_string();
        let disabled_msg = message.map(|s| s.to_string()).unwrap_or_default();
        Self {
            id: id.to_string(),
            label: label.to_string(),
            icon: icon.to_string(),
            url: url.to_string(),
            active: false,
            enabled: false,
            badge: badge_str.clone(),
            has_badge: !badge_str.is_empty(),
            disabled_message: disabled_msg.clone(),
            has_disabled_message: !disabled_msg.is_empty(),
            order: 0,
        }
    }

    /// Create a regular (enabled but not active) navigation item
    ///
    /// # Examples
    ///
    /// ```
    /// use rustwarden_ui::dto::navigation_dto::NavigationItemDTO;
    ///
    /// let item = NavigationItemDTO::regular(
    ///     "settings",
    ///     "Settings",
    ///     "settings",
    ///     "/settings"
    /// );
    ///
    /// assert!(!item.active);
    /// assert!(item.enabled);
    /// ```
    pub fn regular(id: &str, label: &str, icon: &str, url: &str) -> Self {
        Self {
            id: id.to_string(),
            label: label.to_string(),
            icon: icon.to_string(),
            url: url.to_string(),
            active: false,
            enabled: true,
            badge: String::new(),
            has_badge: false,
            disabled_message: String::new(),
            has_disabled_message: false,
            order: 0,
        }
    }
}

/// Complete navigation menu
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NavigationDTO {
    /// Primary navigation items
    pub items: Vec<NavigationItemDTO>,

    /// Secondary navigation items (e.g., user menu)
    pub secondary_items: Vec<NavigationItemDTO>,

    /// Feature 026: Flag indicating if user has a pending password change request
    pub has_pending_password_change: bool,
}

impl NavigationDTO {
    /// Create a new navigation with given items
    pub fn new(items: Vec<NavigationItemDTO>, secondary_items: Vec<NavigationItemDTO>) -> Self {
        Self {
            items,
            secondary_items,
            has_pending_password_change: false,
        }
    }
}

/// Default navigation for RustWarden
///
/// Provides the standard navigation menu with:
/// - Home (enabled)
/// - Settings (enabled)
impl Default for NavigationDTO {
    fn default() -> Self {
        Self {
            items: vec![NavigationItemDTO::regular(
                "home", "Accueil", "home", "/home",
            )],
            secondary_items: vec![],
            has_pending_password_change: false,
        }
    }
}

impl NavigationDTO {
    /// Generate navigation menu based on user roles
    ///
    /// Creates a contextualized navigation menu that includes:
    /// - Home (enabled)
    /// - Settings (enabled)
    /// - Administration (enabled, only if user has "admin" role)
    ///
    /// # Arguments
    ///
    /// * `roles` - Slice of role strings for the current user
    /// * `i18n_service` - I18nService for translations
    /// * `locale` - Locale for translations
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use rustwarden_ui::dto::navigation_dto::NavigationDTO;
    /// use rustwarden_core::i18n::service::I18nService;
    /// use rustwarden_core::i18n::extractor::Locale;
    /// use std::sync::Arc;
    ///
    /// # async fn example() {
    /// let i18n_service = Arc::new(I18nService::new());
    /// let locale = Locale("fr".to_string());
    ///
    /// // Non-admin user
    /// let nav = NavigationDTO::for_user(&[], &i18n_service, &locale, None).await;
    /// assert_eq!(nav.items.len(), 1);
    ///
    /// // Admin user
    /// let nav = NavigationDTO::for_user(&["admin".to_string()], &i18n_service, &locale, None).await;
    /// assert_eq!(nav.items.len(), 2);
    /// assert_eq!(nav.items[1].id, "admin");
    /// # }
    /// ```
    pub async fn for_user(
        roles: &[String],
        i18n_service: &Arc<I18nService>,
        locale: &Locale,
        _db: Option<&sea_orm::DatabaseConnection>,
    ) -> Self {
        let locale_str = locale.as_str();

        let home_label = i18n_service.translate("nav.home", locale_str).await;
        let admin_label = i18n_service.translate("nav.admin", locale_str).await;

        let mut items = vec![NavigationItemDTO {
            id: "home".to_string(),
            label: home_label,
            icon: "home".to_string(),
            url: "/home".to_string(),
            active: false,
            enabled: true,
            badge: String::new(),
            has_badge: false,
            disabled_message: String::new(),
            has_disabled_message: false,
            order: 0,
        }];

        if roles.contains(&"admin".to_string()) {
            items.push(NavigationItemDTO {
                id: "admin".to_string(),
                label: admin_label,
                icon: "shield".to_string(),
                url: "/admin".to_string(),
                active: false,
                enabled: true,
                badge: String::new(),
                has_badge: false,
                disabled_message: String::new(),
                has_disabled_message: false,
                order: 100,
            });
        }

        items.sort_by_key(|item| item.order);

        let has_pending_password_change = false;

        Self {
            items,
            secondary_items: vec![],
            has_pending_password_change,
        }
    }

    /// Set the pending password change flag (Feature 026)
    pub fn set_pending_password_change(&mut self, has_pending: bool) {
        self.has_pending_password_change = has_pending;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_navigation_item_active() {
        let item = NavigationItemDTO::active("home", "Home", "home", "/home");

        assert_eq!(item.id, "home");
        assert_eq!(item.label, "Home");
        assert_eq!(item.icon, "home");
        assert_eq!(item.url, "/home");
        assert!(item.active);
        assert!(item.enabled);
        assert!(item.badge.is_empty());
        assert!(item.disabled_message.is_empty());
    }

    #[test]
    fn test_navigation_item_disabled() {
        let item = NavigationItemDTO::disabled(
            "module",
            "Module",
            "box",
            "/module",
            "Soon",
            Some("Coming soon"),
        );

        assert_eq!(item.id, "module");
        assert_eq!(item.label, "Module");
        assert_eq!(item.icon, "box");
        assert_eq!(item.url, "/module");
        assert!(!item.active);
        assert!(!item.enabled);
        assert_eq!(item.badge, "Soon");
        assert_eq!(item.disabled_message, "Coming soon");
    }

    #[test]
    fn test_navigation_item_regular() {
        let item = NavigationItemDTO::regular("settings", "Settings", "settings", "/settings");

        assert_eq!(item.id, "settings");
        assert!(!item.active);
        assert!(item.enabled);
        assert!(item.badge.is_empty());
    }

    #[test]
    fn test_navigation_dto_default() {
        let nav = NavigationDTO::default();

        assert_eq!(nav.items.len(), 1);
        assert_eq!(nav.secondary_items.len(), 0);

        let home = &nav.items[0];
        assert_eq!(home.id, "home");
        assert!(home.enabled);
    }

    #[test]
    fn test_navigation_dto_new() {
        let items = vec![NavigationItemDTO::active("home", "Home", "home", "/home")];
        let secondary = vec![NavigationItemDTO::regular(
            "logout", "Logout", "logout", "/logout",
        )];

        let nav = NavigationDTO::new(items, secondary);

        assert_eq!(nav.items.len(), 1);
        assert_eq!(nav.secondary_items.len(), 1);
    }
}