user_profile_dto.rs 207 lignes · 6663 octets
/// User Profile Data Transfer Object
/// Feature 004: UI Post-Auth
///
/// This DTO is used to transfer user profile information to the UI layer,
/// including avatar generation (initials and color).
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use uuid::Uuid;

/// User Profile DTO with avatar information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserProfileDTO {
    /// User ID
    pub id: Uuid,

    /// Username
    pub username: String,

    /// User email
    pub email: String,

    /// User roles (e.g., ["admin", "user"])
    pub roles: Vec<String>,

    /// Account creation date
    pub created_at: DateTime<Utc>,

    /// Generated initials for avatar (e.g., "JD" for "John Doe")
    pub initials: String,

    /// Generated avatar color (hex code)
    pub avatar_color: String,
}

impl UserProfileDTO {
    /// Generate initials from username
    ///
    /// # Examples
    ///
    /// ```
    /// use rustwarden_ui::dto::user_profile_dto::UserProfileDTO;
    ///
    /// assert_eq!(UserProfileDTO::generate_initials("john"), "J");
    /// assert_eq!(UserProfileDTO::generate_initials("john_doe"), "JD");
    /// assert_eq!(UserProfileDTO::generate_initials("john.doe.smith"), "JD");
    /// assert_eq!(UserProfileDTO::generate_initials("a"), "A");
    /// ```
    pub fn generate_initials(username: &str) -> String {
        let parts: Vec<&str> = username
            .split(['_', '.', '-', ' '])
            .filter(|s| !s.is_empty())
            .collect();

        match parts.len() {
            0 => String::from("?"),
            1 => parts[0]
                .chars()
                .next()
                .unwrap_or('?')
                .to_uppercase()
                .to_string(),
            _ => {
                let first = parts[0].chars().next().unwrap_or('?');
                let second = parts[1].chars().next().unwrap_or('?');
                format!("{}{}", first.to_uppercase(), second.to_uppercase())
            }
        }
    }

    /// Generate avatar color based on username hash
    ///
    /// Returns a consistent color for the same username.
    /// Uses a predefined palette of pleasant colors.
    ///
    /// # Examples
    ///
    /// ```
    /// use rustwarden_ui::dto::user_profile_dto::UserProfileDTO;
    ///
    /// let color1 = UserProfileDTO::generate_avatar_color("john");
    /// let color2 = UserProfileDTO::generate_avatar_color("john");
    /// assert_eq!(color1, color2); // Same username = same color
    ///
    /// let color3 = UserProfileDTO::generate_avatar_color("jane");
    /// assert_ne!(color1, color3); // Different usernames = different colors (most likely)
    /// ```
    pub fn generate_avatar_color(username: &str) -> String {
        // Palette of pleasant colors
        const COLORS: &[&str] = &[
            "#0082c9", // RustWarden blue
            "#ce422b", // Rust red
            "#3b82f6", // Blue
            "#10b981", // Green
            "#f59e0b", // Amber
            "#8b5cf6", // Purple
            "#ec4899", // Pink
            "#06b6d4", // Cyan
            "#84cc16", // Lime
            "#f97316", // Orange
        ];

        // Hash the username
        let mut hasher = DefaultHasher::new();
        username.hash(&mut hasher);
        let hash = hasher.finish();

        // Select color from palette
        let index = (hash % COLORS.len() as u64) as usize;
        COLORS[index].to_string()
    }

    /// Create a new UserProfileDTO from user model and roles
    ///
    /// # Arguments
    ///
    /// * `user` - The user model from database
    /// * `roles` - List of role names for this user
    pub fn new(user: rustwarden_core::models::user::Model, roles: Vec<String>) -> Self {
        let username = user.username.clone();

        Self {
            id: user.id,
            username: user.username,
            email: user.email,
            roles,
            created_at: user.created_at,
            initials: Self::generate_initials(&username),
            avatar_color: Self::generate_avatar_color(&username),
        }
    }
}

/// Implement From trait for conversion from User model
impl From<rustwarden_core::models::user::Model> for UserProfileDTO {
    fn from(user: rustwarden_core::models::user::Model) -> Self {
        Self::new(user, Vec::new())
    }
}

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

    #[test]
    fn test_generate_initials_single_part() {
        assert_eq!(UserProfileDTO::generate_initials("john"), "J");
        assert_eq!(UserProfileDTO::generate_initials("alice"), "A");
    }

    #[test]
    fn test_generate_initials_two_parts() {
        assert_eq!(UserProfileDTO::generate_initials("john_doe"), "JD");
        assert_eq!(UserProfileDTO::generate_initials("alice.smith"), "AS");
        assert_eq!(UserProfileDTO::generate_initials("bob-jones"), "BJ");
    }

    #[test]
    fn test_generate_initials_multiple_parts() {
        // Should only take first two parts
        assert_eq!(UserProfileDTO::generate_initials("john.doe.smith"), "JD");
        assert_eq!(UserProfileDTO::generate_initials("a_b_c_d"), "AB");
    }

    #[test]
    fn test_generate_initials_empty() {
        assert_eq!(UserProfileDTO::generate_initials(""), "?");
    }

    #[test]
    fn test_generate_initials_single_char() {
        assert_eq!(UserProfileDTO::generate_initials("a"), "A");
        assert_eq!(UserProfileDTO::generate_initials("z"), "Z");
    }

    #[test]
    fn test_generate_avatar_color_consistency() {
        // Same username should always give same color
        let color1 = UserProfileDTO::generate_avatar_color("john");
        let color2 = UserProfileDTO::generate_avatar_color("john");
        assert_eq!(color1, color2);
    }

    #[test]
    fn test_generate_avatar_color_format() {
        // Color should be in hex format
        let color = UserProfileDTO::generate_avatar_color("test");
        assert!(color.starts_with('#'));
        assert_eq!(color.len(), 7); // #RRGGBB
    }

    #[test]
    fn test_generate_avatar_color_different_users() {
        let color1 = UserProfileDTO::generate_avatar_color("alice");
        let color2 = UserProfileDTO::generate_avatar_color("bob");
        let color3 = UserProfileDTO::generate_avatar_color("charlie");

        // All colors should be valid
        assert!(color1.starts_with('#'));
        assert!(color2.starts_with('#'));
        assert!(color3.starts_with('#'));

        // It's possible (but unlikely) that two different usernames
        // produce the same color, so we don't assert inequality
    }
}