use super::{Metadata, UsageStats};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
#[serde(flatten)]
pub metadata: Metadata,
pub username: String,
pub email: String,
pub display_name: Option<String>,
#[serde(skip_serializing)]
pub password_hash: String,
pub role: UserRole,
pub status: UserStatus,
pub team_ids: Vec<Uuid>,
pub preferences: UserPreferences,
pub usage_stats: UsageStats,
pub rate_limits: Option<UserRateLimits>,
pub last_login_at: Option<chrono::DateTime<chrono::Utc>>,
pub email_verified: bool,
pub two_factor_enabled: bool,
pub profile: UserProfile,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UserRole {
SuperAdmin,
Admin,
Manager,
User,
Viewer,
ApiUser,
}
impl std::fmt::Display for UserRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UserRole::SuperAdmin => write!(f, "super_admin"),
UserRole::Admin => write!(f, "admin"),
UserRole::Manager => write!(f, "manager"),
UserRole::User => write!(f, "user"),
UserRole::Viewer => write!(f, "viewer"),
UserRole::ApiUser => write!(f, "api_user"),
}
}
}
impl std::str::FromStr for UserRole {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"super_admin" => Ok(UserRole::SuperAdmin),
"admin" => Ok(UserRole::Admin),
"manager" => Ok(UserRole::Manager),
"user" => Ok(UserRole::User),
"viewer" => Ok(UserRole::Viewer),
"api_user" => Ok(UserRole::ApiUser),
_ => Err(format!("Invalid user role: {}", s)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UserStatus {
Active,
Inactive,
Suspended,
Pending,
Deleted,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct UserPreferences {
pub language: Option<String>,
pub timezone: Option<String>,
pub theme: Option<String>,
pub notifications: NotificationSettings,
pub dashboard: DashboardSettings,
pub api: ApiPreferences,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct NotificationSettings {
pub email_enabled: bool,
pub slack_enabled: bool,
pub webhook_enabled: bool,
pub types: Vec<NotificationType>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NotificationType {
RateLimitWarning,
QuotaWarning,
ServiceAlert,
SecurityAlert,
UsageReport,
SystemMaintenance,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DashboardSettings {
pub default_time_range: Option<String>,
pub favorite_charts: Vec<String>,
pub layout: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ApiPreferences {
pub default_model: Option<String>,
pub default_temperature: Option<f32>,
pub default_max_tokens: Option<u32>,
pub preferred_providers: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserRateLimits {
pub rpm: Option<u32>,
pub tpm: Option<u32>,
pub rpd: Option<u32>,
pub tpd: Option<u32>,
pub concurrent: Option<u32>,
pub monthly_budget: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct UserProfile {
pub first_name: Option<String>,
pub last_name: Option<String>,
pub company: Option<String>,
pub title: Option<String>,
pub phone: Option<String>,
pub avatar_url: Option<String>,
pub bio: Option<String>,
pub location: Option<String>,
pub website: Option<String>,
pub social_links: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserSession {
#[serde(flatten)]
pub metadata: Metadata,
pub user_id: Uuid,
#[serde(skip_serializing)]
pub token: String,
pub session_type: SessionType,
pub ip_address: Option<String>,
pub user_agent: Option<String>,
pub expires_at: chrono::DateTime<chrono::Utc>,
pub last_activity: chrono::DateTime<chrono::Utc>,
pub data: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SessionType {
Web,
Api,
Mobile,
Cli,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserActivity {
#[serde(flatten)]
pub metadata: Metadata,
pub user_id: Uuid,
pub activity_type: ActivityType,
pub description: String,
pub ip_address: Option<String>,
pub user_agent: Option<String>,
pub data: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ActivityType {
Login,
Logout,
PasswordChange,
ProfileUpdate,
ApiKeyCreated,
ApiKeyDeleted,
TeamJoined,
TeamLeft,
SettingsChanged,
SecurityEvent,
}
impl User {
pub fn new(username: String, email: String, password_hash: String) -> Self {
Self {
metadata: Metadata::new(),
username,
email,
display_name: None,
password_hash,
role: UserRole::User,
status: UserStatus::Pending,
team_ids: vec![],
preferences: UserPreferences::default(),
usage_stats: UsageStats::default(),
rate_limits: None,
last_login_at: None,
email_verified: false,
two_factor_enabled: false,
profile: UserProfile::default(),
}
}
pub fn id(&self) -> Uuid {
self.metadata.id
}
pub fn is_active(&self) -> bool {
matches!(self.status, UserStatus::Active)
}
pub fn has_role(&self, role: &UserRole) -> bool {
match (&self.role, role) {
(UserRole::SuperAdmin, _) => true,
(
UserRole::Admin,
UserRole::Admin
| UserRole::Manager
| UserRole::User
| UserRole::Viewer
| UserRole::ApiUser,
) => true,
(
UserRole::Manager,
UserRole::Manager | UserRole::User | UserRole::Viewer | UserRole::ApiUser,
) => true,
(current, target) => current == target,
}
}
pub fn is_in_team(&self, team_id: Uuid) -> bool {
self.team_ids.contains(&team_id)
}
pub fn add_to_team(&mut self, team_id: Uuid) {
if !self.team_ids.contains(&team_id) {
self.team_ids.push(team_id);
self.metadata.touch();
}
}
pub fn remove_from_team(&mut self, team_id: Uuid) {
if let Some(pos) = self.team_ids.iter().position(|&id| id == team_id) {
self.team_ids.remove(pos);
self.metadata.touch();
}
}
pub fn update_last_login(&mut self) {
self.last_login_at = Some(chrono::Utc::now());
self.metadata.touch();
}
pub fn verify_email(&mut self) {
self.email_verified = true;
if matches!(self.status, UserStatus::Pending) {
self.status = UserStatus::Active;
}
self.metadata.touch();
}
pub fn enable_two_factor(&mut self) {
self.two_factor_enabled = true;
self.metadata.touch();
}
pub fn disable_two_factor(&mut self) {
self.two_factor_enabled = false;
self.metadata.touch();
}
pub fn update_usage(&mut self, requests: u64, tokens: u64, cost: f64) {
self.usage_stats.total_requests += requests;
self.usage_stats.total_tokens += tokens;
self.usage_stats.total_cost += cost;
let today = chrono::Utc::now().date_naive();
let last_reset = self.usage_stats.last_reset.date_naive();
if today != last_reset {
self.usage_stats.requests_today = 0;
self.usage_stats.tokens_today = 0;
self.usage_stats.cost_today = 0.0;
self.usage_stats.last_reset = chrono::Utc::now();
}
self.usage_stats.requests_today += requests as u32;
self.usage_stats.tokens_today += tokens as u32;
self.usage_stats.cost_today += cost;
self.metadata.touch();
}
}
impl UserSession {
pub fn new(
user_id: Uuid,
token: String,
session_type: SessionType,
expires_at: chrono::DateTime<chrono::Utc>,
) -> Self {
Self {
metadata: Metadata::new(),
user_id,
token,
session_type,
ip_address: None,
user_agent: None,
expires_at,
last_activity: chrono::Utc::now(),
data: HashMap::new(),
}
}
pub fn is_expired(&self) -> bool {
chrono::Utc::now() > self.expires_at
}
pub fn update_activity(&mut self) {
self.last_activity = chrono::Utc::now();
}
pub fn set_data<K: Into<String>, V: Into<serde_json::Value>>(&mut self, key: K, value: V) {
self.data.insert(key.into(), value.into());
}
pub fn get_data(&self, key: &str) -> Option<&serde_json::Value> {
self.data.get(key)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_user_creation() {
let user = User::new(
"testuser".to_string(),
"test@example.com".to_string(),
"hashed_password".to_string(),
);
assert_eq!(user.username, "testuser");
assert_eq!(user.email, "test@example.com");
assert!(matches!(user.role, UserRole::User));
assert!(matches!(user.status, UserStatus::Pending));
assert!(!user.is_active());
}
#[test]
fn test_user_role_hierarchy() {
let mut user = User::new(
"admin".to_string(),
"admin@example.com".to_string(),
"hashed_password".to_string(),
);
user.role = UserRole::Admin;
assert!(user.has_role(&UserRole::Admin));
assert!(user.has_role(&UserRole::User));
assert!(user.has_role(&UserRole::Viewer));
assert!(!user.has_role(&UserRole::SuperAdmin));
}
#[test]
fn test_team_management() {
let mut user = User::new(
"testuser".to_string(),
"test@example.com".to_string(),
"hashed_password".to_string(),
);
let team_id = Uuid::new_v4();
assert!(!user.is_in_team(team_id));
user.add_to_team(team_id);
assert!(user.is_in_team(team_id));
user.remove_from_team(team_id);
assert!(!user.is_in_team(team_id));
}
#[test]
fn test_session_expiry() {
let user_id = Uuid::new_v4();
let token = "test_token".to_string();
let expires_at = chrono::Utc::now() - chrono::Duration::hours(1);
let session = UserSession::new(user_id, token, SessionType::Web, expires_at);
assert!(session.is_expired());
}
}