use crate::storage::database::Database;
use crate::utils::error::{GatewayError, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tracing::{debug, info, warn};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub user_id: String,
pub email: String,
pub display_name: Option<String>,
pub first_name: Option<String>,
pub last_name: Option<String>,
pub role: UserRole,
pub teams: Vec<String>,
pub permissions: Vec<String>,
pub metadata: HashMap<String, String>,
pub max_budget: Option<f64>,
pub spend: f64,
pub budget_duration: Option<String>,
pub budget_reset_at: Option<DateTime<Utc>>,
pub is_active: bool,
pub created_at: DateTime<Utc>,
pub last_login_at: Option<DateTime<Utc>>,
pub preferences: UserPreferences,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Team {
pub team_id: String,
pub team_name: String,
pub description: Option<String>,
pub organization_id: Option<String>,
pub members: Vec<TeamMember>,
pub permissions: Vec<String>,
pub models: Vec<String>,
pub max_budget: Option<f64>,
pub spend: f64,
pub budget_duration: Option<String>,
pub budget_reset_at: Option<DateTime<Utc>>,
pub metadata: HashMap<String, String>,
pub is_active: bool,
pub created_at: DateTime<Utc>,
pub settings: TeamSettings,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Organization {
pub organization_id: String,
pub organization_name: String,
pub description: Option<String>,
pub domain: Option<String>,
pub teams: Vec<String>,
pub admins: Vec<String>,
pub max_budget: Option<f64>,
pub spend: f64,
pub budget_duration: Option<String>,
pub budget_reset_at: Option<DateTime<Utc>>,
pub metadata: HashMap<String, String>,
pub is_active: bool,
pub created_at: DateTime<Utc>,
pub settings: OrganizationSettings,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum UserRole {
SuperAdmin,
OrgAdmin,
TeamAdmin,
User,
ReadOnly,
ServiceAccount,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TeamMember {
pub user_id: String,
pub role: TeamRole,
pub joined_at: DateTime<Utc>,
pub is_active: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum TeamRole {
Owner,
Admin,
Member,
ReadOnly,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserPreferences {
pub language: Option<String>,
pub timezone: Option<String>,
pub email_notifications: bool,
pub slack_notifications: bool,
pub dashboard_config: HashMap<String, serde_json::Value>,
}
impl Default for UserPreferences {
fn default() -> Self {
Self {
language: Some("en".to_string()),
timezone: Some("UTC".to_string()),
email_notifications: true,
slack_notifications: false,
dashboard_config: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TeamSettings {
pub default_model: Option<String>,
pub auto_approve_members: bool,
pub require_approval_for_high_cost: bool,
pub high_cost_threshold: Option<f64>,
pub rate_limits: Option<crate::core::virtual_keys::RateLimits>,
}
impl Default for TeamSettings {
fn default() -> Self {
Self {
default_model: None,
auto_approve_members: true,
require_approval_for_high_cost: false,
high_cost_threshold: Some(10.0),
rate_limits: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrganizationSettings {
pub sso_config: Option<SSOConfig>,
pub default_team: Option<String>,
pub require_email_verification: bool,
pub password_policy: PasswordPolicy,
pub session_timeout_minutes: u32,
pub allowed_email_domains: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SSOConfig {
pub provider: SSOProvider,
pub client_id: String,
pub client_secret: String,
pub auth_endpoint: String,
pub token_endpoint: String,
pub userinfo_endpoint: String,
pub scopes: Vec<String>,
pub attribute_mappings: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SSOProvider {
Google,
Microsoft,
Okta,
Auth0,
Generic,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PasswordPolicy {
pub min_length: u32,
pub require_uppercase: bool,
pub require_lowercase: bool,
pub require_numbers: bool,
pub require_special_chars: bool,
pub expiry_days: Option<u32>,
}
impl Default for PasswordPolicy {
fn default() -> Self {
Self {
min_length: 8,
require_uppercase: true,
require_lowercase: true,
require_numbers: true,
require_special_chars: false,
expiry_days: None,
}
}
}
impl Default for OrganizationSettings {
fn default() -> Self {
Self {
sso_config: None,
default_team: None,
require_email_verification: true,
password_policy: PasswordPolicy::default(),
session_timeout_minutes: 480, allowed_email_domains: vec![],
}
}
}
pub struct UserManager {
database: Arc<Database>,
}
impl UserManager {
pub fn new(database: Arc<Database>) -> Self {
Self { database }
}
pub async fn create_user(&self, email: String, display_name: Option<String>) -> Result<User> {
info!("Creating user: {}", email);
if self.database.get_user_by_email(&email).await?.is_some() {
return Err(GatewayError::Conflict("User already exists".to_string()));
}
let user = User {
user_id: Uuid::new_v4().to_string(),
email,
display_name,
first_name: None,
last_name: None,
role: UserRole::User,
teams: vec![],
permissions: vec![],
metadata: HashMap::new(),
max_budget: Some(100.0), spend: 0.0,
budget_duration: Some("1m".to_string()),
budget_reset_at: Some(Utc::now() + chrono::Duration::days(30)),
is_active: true,
created_at: Utc::now(),
last_login_at: None,
preferences: UserPreferences::default(),
};
self.database.create_user(&user).await?;
info!("User created successfully: {}", user.user_id);
Ok(user)
}
pub async fn get_user(&self, user_id: &str) -> Result<Option<User>> {
self.database.get_user(user_id).await
}
pub async fn get_user_by_email(&self, email: &str) -> Result<Option<User>> {
self.database.get_user_by_email(email).await
}
pub async fn update_user(&self, user: &User) -> Result<()> {
self.database.update_user(user).await
}
pub async fn delete_user(&self, user_id: &str) -> Result<()> {
info!("Deleting user: {}", user_id);
self.database.delete_user(user_id).await
}
pub async fn create_team(
&self,
team_name: String,
description: Option<String>,
organization_id: Option<String>,
creator_id: String,
) -> Result<Team> {
info!("Creating team: {}", team_name);
let team = Team {
team_id: Uuid::new_v4().to_string(),
team_name,
description,
organization_id,
members: vec![TeamMember {
user_id: creator_id,
role: TeamRole::Owner,
joined_at: Utc::now(),
is_active: true,
}],
permissions: vec![],
models: vec![],
max_budget: Some(1000.0), spend: 0.0,
budget_duration: Some("1m".to_string()),
budget_reset_at: Some(Utc::now() + chrono::Duration::days(30)),
metadata: HashMap::new(),
is_active: true,
created_at: Utc::now(),
settings: TeamSettings::default(),
};
self.database.create_team(&team).await?;
info!("Team created successfully: {}", team.team_id);
Ok(team)
}
pub async fn get_team(&self, team_id: &str) -> Result<Option<Team>> {
self.database.get_team(team_id).await
}
pub async fn add_user_to_team(
&self,
team_id: &str,
user_id: &str,
role: TeamRole,
) -> Result<()> {
info!("Adding user {} to team {} with role {:?}", user_id, team_id, role);
let mut team = self.database.get_team(team_id).await?
.ok_or_else(|| GatewayError::NotFound("Team not found".to_string()))?;
if team.members.iter().any(|m| m.user_id == user_id) {
return Err(GatewayError::Conflict("User is already a team member".to_string()));
}
team.members.push(TeamMember {
user_id: user_id.to_string(),
role,
joined_at: Utc::now(),
is_active: true,
});
self.database.update_team(&team).await?;
if let Some(mut user) = self.database.get_user(user_id).await? {
user.teams.push(team_id.to_string());
self.database.update_user(&user).await?;
}
Ok(())
}
pub async fn remove_user_from_team(&self, team_id: &str, user_id: &str) -> Result<()> {
info!("Removing user {} from team {}", user_id, team_id);
let mut team = self.database.get_team(team_id).await?
.ok_or_else(|| GatewayError::NotFound("Team not found".to_string()))?;
team.members.retain(|m| m.user_id != user_id);
self.database.update_team(&team).await?;
if let Some(mut user) = self.database.get_user(user_id).await? {
user.teams.retain(|t| t != team_id);
self.database.update_user(&user).await?;
}
Ok(())
}
pub async fn create_organization(
&self,
organization_name: String,
description: Option<String>,
creator_id: String,
) -> Result<Organization> {
info!("Creating organization: {}", organization_name);
let organization = Organization {
organization_id: Uuid::new_v4().to_string(),
organization_name,
description,
domain: None,
teams: vec![],
admins: vec![creator_id],
max_budget: Some(10000.0), spend: 0.0,
budget_duration: Some("1m".to_string()),
budget_reset_at: Some(Utc::now() + chrono::Duration::days(30)),
metadata: HashMap::new(),
is_active: true,
created_at: Utc::now(),
settings: OrganizationSettings::default(),
};
self.database.create_organization(&organization).await?;
info!("Organization created successfully: {}", organization.organization_id);
Ok(organization)
}
pub async fn get_organization(&self, organization_id: &str) -> Result<Option<Organization>> {
self.database.get_organization(organization_id).await
}
pub async fn check_permission(&self, user_id: &str, permission: &str) -> Result<bool> {
let user = self.database.get_user(user_id).await?
.ok_or_else(|| GatewayError::NotFound("User not found".to_string()))?;
if user.role == UserRole::SuperAdmin {
return Ok(true);
}
if user.permissions.contains(&permission.to_string()) {
return Ok(true);
}
for team_id in &user.teams {
if let Some(team) = self.database.get_team(team_id).await? {
if team.permissions.contains(&permission.to_string()) {
return Ok(true);
}
}
}
Ok(false)
}
pub async fn update_user_spend(&self, user_id: &str, cost: f64) -> Result<()> {
self.database.update_user_spend(user_id, cost).await
}
pub async fn update_team_spend(&self, team_id: &str, cost: f64) -> Result<()> {
self.database.update_team_spend(team_id, cost).await
}
pub async fn list_users(&self, offset: u32, limit: u32) -> Result<Vec<User>> {
self.database.list_users(offset, limit).await
}
pub async fn list_teams(&self, offset: u32, limit: u32) -> Result<Vec<Team>> {
self.database.list_teams(offset, limit).await
}
pub async fn get_user_teams(&self, user_id: &str) -> Result<Vec<Team>> {
let user = self.database.get_user(user_id).await?
.ok_or_else(|| GatewayError::NotFound("User not found".to_string()))?;
let mut teams = Vec::new();
for team_id in &user.teams {
if let Some(team) = self.database.get_team(team_id).await? {
teams.push(team);
}
}
Ok(teams)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_user_creation() {
let db = Arc::new(Database::new_mock());
let manager = UserManager::new(db);
let user = manager.create_user(
"test@example.com".to_string(),
Some("Test User".to_string()),
).await.unwrap();
assert_eq!(user.email, "test@example.com");
assert_eq!(user.display_name, Some("Test User".to_string()));
assert_eq!(user.role, UserRole::User);
assert!(user.is_active);
}
#[tokio::test]
async fn test_team_creation() {
let db = Arc::new(Database::new_mock());
let manager = UserManager::new(db);
let team = manager.create_team(
"Test Team".to_string(),
Some("A test team".to_string()),
None,
"user123".to_string(),
).await.unwrap();
assert_eq!(team.team_name, "Test Team");
assert_eq!(team.description, Some("A test team".to_string()));
assert_eq!(team.members.len(), 1);
assert_eq!(team.members[0].user_id, "user123");
assert_eq!(team.members[0].role, TeamRole::Owner);
}
#[test]
fn test_user_roles() {
assert_eq!(UserRole::SuperAdmin, UserRole::SuperAdmin);
assert_ne!(UserRole::User, UserRole::Admin);
}
#[test]
fn test_team_roles() {
assert_eq!(TeamRole::Owner, TeamRole::Owner);
assert_ne!(TeamRole::Member, TeamRole::Admin);
}
}