systemprompt-users 0.2.2

User management for systemprompt.io AI governance infrastructure. 6-tier RBAC, sessions, IP bans, and role-scoped access control for the MCP governance pipeline.
Documentation
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use systemprompt_identifiers::{SessionId, UserId};

pub use systemprompt_models::auth::{UserRole, UserStatus};

#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct User {
    #[sqlx(try_from = "String")]
    pub id: UserId,
    pub name: String,
    pub email: String,
    pub full_name: Option<String>,
    pub display_name: Option<String>,
    pub status: Option<String>,
    pub email_verified: Option<bool>,
    pub roles: Vec<String>,
    pub avatar_url: Option<String>,
    pub is_bot: bool,
    pub is_scanner: bool,
    pub created_at: Option<DateTime<Utc>>,
    pub updated_at: Option<DateTime<Utc>>,
}

impl User {
    pub fn is_active(&self) -> bool {
        self.status.as_deref() == Some(UserStatus::Active.as_str())
    }

    pub fn is_admin(&self) -> bool {
        self.roles.contains(&UserRole::Admin.as_str().to_string())
    }

    pub fn has_role(&self, role: UserRole) -> bool {
        self.roles.contains(&role.as_str().to_string())
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct UserActivity {
    #[sqlx(try_from = "String")]
    pub user_id: UserId,
    pub last_active: Option<DateTime<Utc>>,
    pub session_count: i64,
    pub task_count: i64,
    pub message_count: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct UserWithSessions {
    #[sqlx(try_from = "String")]
    pub id: UserId,
    pub name: String,
    pub email: String,
    pub full_name: Option<String>,
    pub status: Option<String>,
    pub roles: Vec<String>,
    pub created_at: Option<DateTime<Utc>>,
    pub active_sessions: i64,
    pub last_session_at: Option<DateTime<Utc>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserSession {
    pub session_id: SessionId,
    pub user_id: Option<UserId>,
    pub ip_address: Option<String>,
    pub user_agent: Option<String>,
    pub device_type: Option<String>,
    pub started_at: Option<DateTime<Utc>>,
    pub last_activity_at: Option<DateTime<Utc>>,
    pub ended_at: Option<DateTime<Utc>>,
}

#[derive(Debug, Clone, FromRow)]
pub struct UserSessionRow {
    #[sqlx(try_from = "String")]
    pub session_id: SessionId,
    pub user_id: Option<UserId>,
    pub ip_address: Option<String>,
    pub user_agent: Option<String>,
    pub device_type: Option<String>,
    pub started_at: Option<DateTime<Utc>>,
    pub last_activity_at: Option<DateTime<Utc>>,
    pub ended_at: Option<DateTime<Utc>>,
}

impl From<UserSessionRow> for UserSession {
    fn from(row: UserSessionRow) -> Self {
        Self {
            session_id: row.session_id,
            user_id: row.user_id,
            ip_address: row.ip_address,
            user_agent: row.user_agent,
            device_type: row.device_type,
            started_at: row.started_at,
            last_activity_at: row.last_activity_at,
            ended_at: row.ended_at,
        }
    }
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct UserStats {
    pub total: i64,
    pub created_24h: i64,
    pub created_7d: i64,
    pub created_30d: i64,
    pub active: i64,
    pub suspended: i64,
    pub admins: i64,
    pub anonymous: i64,
    pub bots: i64,
    pub oldest_user: Option<DateTime<Utc>>,
    pub newest_user: Option<DateTime<Utc>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserCountBreakdown {
    pub total: i64,
    pub by_status: std::collections::HashMap<String, i64>,
    pub by_role: std::collections::HashMap<String, i64>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserExport {
    pub id: UserId,
    pub name: String,
    pub email: String,
    pub full_name: Option<String>,
    pub display_name: Option<String>,
    pub status: Option<String>,
    pub email_verified: Option<bool>,
    pub roles: Vec<String>,
    pub is_bot: bool,
    pub is_scanner: bool,
    pub created_at: Option<DateTime<Utc>>,
    pub updated_at: Option<DateTime<Utc>>,
}

impl From<User> for UserExport {
    fn from(user: User) -> Self {
        Self {
            id: user.id,
            name: user.name,
            email: user.email,
            full_name: user.full_name,
            display_name: user.display_name,
            status: user.status,
            email_verified: user.email_verified,
            roles: user.roles,
            is_bot: user.is_bot,
            is_scanner: user.is_scanner,
            created_at: user.created_at,
            updated_at: user.updated_at,
        }
    }
}