admin_dto.rs 269 lignes · 9065 octets
//! Admin DTOs
//! Feature 009: Admin & Settings
//!
//! Data Transfer Objects for admin endpoints

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

/// Dashboard statistics DTO
///
/// Contains statistics displayed on the admin dashboard.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DashboardStats {
    /// Total number of users in the system
    pub total_users: u64,
    /// Number of active sessions (non-revoked, non-expired refresh tokens)
    pub active_sessions: u64,
    /// Number of users with admin role
    pub total_admins: u64,
    /// Last activity timestamp (most recent last_login_at)
    pub last_activity: Option<DateTime<Utc>>,
    /// Audit logging statistics (if audit_logs table exists)
    pub audit_log_stats: Option<AuditLogStats>,
}

/// Audit logging statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditLogStats {
    /// Total number of audit log entries
    pub total_logs: u64,
    /// Number of logs in the last 24 hours
    pub logs_last_24h: u64,
    /// Number of logs by level
    pub logs_by_level: LogsByLevel,
}

/// Logs count by level
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogsByLevel {
    /// Number of DEBUG-level log entries.
    pub debug: u64,
    /// Number of INFO-level log entries.
    pub info: u64,
    /// Number of WARN-level log entries.
    pub warn: u64,
    /// Number of ERROR-level log entries.
    pub error: u64,
}

/// User information for admin list (admin context)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserInfo {
    /// User ID
    pub id: uuid::Uuid,
    /// Username
    pub username: String,
    /// Email
    pub email: String,
    /// User roles (as strings)
    pub roles: Vec<String>,
    /// Account creation date
    pub created_at: DateTime<Utc>,
    /// Last login timestamp
    pub last_login_at: Option<DateTime<Utc>>,
    /// Last login IP address
    pub last_login_ip: Option<String>,
}

/// User list response with pagination
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserListResponse {
    /// List of users
    pub users: Vec<UserInfo>,
    /// Total number of users (for pagination)
    pub total: u64,
    /// Current page number (1-indexed)
    pub page: u64,
    /// Number of users per page
    pub per_page: u64,
    /// Total number of pages
    pub total_pages: u64,
}

impl UserListResponse {
    /// Create a new UserListResponse
    pub fn new(users: Vec<UserInfo>, total: u64, page: u64, per_page: u64) -> Self {
        let total_pages = (total as f64 / per_page as f64).ceil() as u64;
        Self {
            users,
            total,
            page,
            per_page,
            total_pages,
        }
    }
}

/// Request DTO for password reset by admin (Feature 009 - Phase 6)
#[derive(Debug, Deserialize, Serialize)]
pub struct ResetPasswordRequest {
    /// Optional new password
    /// If None, a secure password will be generated automatically
    pub new_password: Option<String>,
}

/// Response DTO for password reset (Feature 009 - Phase 6)
#[derive(Debug, Deserialize, Serialize)]
pub struct ResetPasswordResponse {
    /// The new password (only returned if auto-generated)
    pub password: Option<String>,
    /// Success message
    pub message: String,
}

/// Request DTO for creating a new user
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateUserRequest {
    /// Username (must be unique)
    pub username: String,
    /// Email address (must be unique)
    pub email: String,
    /// Password (optional - will be generated if not provided)
    pub password: Option<String>,
    /// Roles to assign to the user
    /// Accepts either a single string or a vector of strings
    #[serde(deserialize_with = "deserialize_string_or_vec")]
    pub roles: Vec<String>,
}

/// Custom deserializer that accepts either a `String` or `Vec<String>`
///
/// This is needed because HTML forms send:
/// - `roles=user` when one checkbox is checked (`String`)
/// - `roles=user&roles=admin` when multiple checkboxes are checked (`Vec<String>`)
///
/// serde_urlencoded handles duplicate fields by creating a map where values are either
/// a single string or a vector. We need to handle both cases.
pub fn deserialize_string_or_vec<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    use serde::de::{self, Visitor};
    use std::fmt;

    struct StringOrVec;

    impl<'de> Visitor<'de> for StringOrVec {
        type Value = Vec<String>;

        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
            formatter.write_str("a string, a sequence of strings, or a map with string values")
        }

        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
        where
            E: de::Error,
        {
            Ok(vec![value.to_string()])
        }

        fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
        where
            E: de::Error,
        {
            Ok(vec![value])
        }

        fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
        where
            A: de::SeqAccess<'de>,
        {
            let mut vec = Vec::new();
            while let Some(value) = seq.next_element()? {
                vec.push(value);
            }
            Ok(vec)
        }

        // Handle the case where serde_urlencoded creates a map with duplicate keys
        // This happens when multiple checkboxes with the same name are checked
        fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
        where
            A: de::MapAccess<'de>,
        {
            let mut vec = Vec::new();
            while let Some((_key, value)) = map.next_entry::<String, String>()? {
                vec.push(value);
            }
            Ok(vec)
        }
    }

    // Try deserialize_any first, but also handle the map case
    deserializer.deserialize_any(StringOrVec)
}

/// Request DTO for updating a user
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateUserRequest {
    /// Email address (optional - only if modifying)
    pub email: Option<String>,
    /// Roles to assign (optional - replaces all existing roles if provided)
    pub roles: Option<Vec<String>>,
    /// Whether user must change password on next login
    pub must_change_password: Option<bool>,
}

/// Response DTO for user creation (includes generated password if applicable)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateUserResponse {
    /// Created user information
    pub user: UserInfo,
    /// Generated password (only if password was auto-generated, None if user provided password)
    /// This should be displayed once and never stored
    pub generated_password: Option<String>,
}

/// Request DTO for updating app settings (Phase 11 - T166)
#[derive(Debug, Deserialize, Serialize)]
pub struct UpdateAppSettingsRequest {
    // General settings
    /// Application display name.
    pub app_name: Option<String>,
    /// Short description of the application.
    pub app_description: Option<String>,
    /// Public domain of the application.
    pub app_domain: Option<String>,
    /// Whether public self-registration is allowed.
    pub allow_registration: Option<bool>,
    /// Whether email validation is required on registration.
    pub validation_email_required: Option<bool>,
    /// Alias for `validation_email_required` (legacy French field name).
    pub validation_email_requise: Option<bool>,

    // Security - Password complexity
    /// Minimum password length.
    pub password_min_length: Option<u32>,
    /// Whether passwords must contain at least one uppercase letter.
    pub password_require_uppercase: Option<bool>,
    /// Whether passwords must contain at least one lowercase letter.
    pub password_require_lowercase: Option<bool>,
    /// Whether passwords must contain at least one digit.
    pub password_require_digits: Option<bool>,
    /// Whether passwords must contain at least one special character.
    pub password_require_special: Option<bool>,
    /// Whether periodic password expiration is enabled.
    pub password_expiration_enabled: Option<bool>,
    /// Number of days before a password expires.
    pub password_expiration_days: Option<u32>,
    /// Whether an alert email is sent before password expiry.
    pub password_expiration_alert_enabled: Option<bool>,
    /// Number of days before expiry at which the alert is sent.
    pub password_expiration_alert_days: Option<u32>,

    // Audit logging
    /// Minimum severity level for audit log entries.
    pub audit_log_level: Option<String>,
    /// Number of days to retain audit log entries.
    pub audit_log_retention_days: Option<u32>,
}

/// Response DTO for app settings (Phase 11)
#[derive(Debug, Serialize)]
pub struct AppSettingsResponse {
    /// All current settings as key-value pairs
    pub settings: std::collections::HashMap<String, String>,
    /// Success message if just updated
    pub message: Option<String>,
}