systemprompt-users 0.4.0

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::{ApiKeyId, DeviceCertId, 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>>,
}

#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct UserApiKey {
    #[sqlx(try_from = "String")]
    pub id: ApiKeyId,
    #[sqlx(try_from = "String")]
    pub user_id: UserId,
    pub name: String,
    pub key_prefix: String,
    pub key_hash: String,
    pub created_at: Option<DateTime<Utc>>,
    pub last_used_at: Option<DateTime<Utc>>,
    pub expires_at: Option<DateTime<Utc>>,
    pub revoked_at: Option<DateTime<Utc>>,
}

impl UserApiKey {
    pub fn is_active(&self, now: DateTime<Utc>) -> bool {
        if self.revoked_at.is_some() {
            return false;
        }
        if let Some(expires_at) = self.expires_at {
            if now >= expires_at {
                return false;
            }
        }
        true
    }
}

#[derive(Debug, Clone)]
pub struct NewApiKey {
    pub record: UserApiKey,
    pub secret: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct UserDeviceCert {
    #[sqlx(try_from = "String")]
    pub id: DeviceCertId,
    #[sqlx(try_from = "String")]
    pub user_id: UserId,
    pub fingerprint: String,
    pub label: String,
    pub enrolled_at: Option<DateTime<Utc>>,
    pub revoked_at: Option<DateTime<Utc>>,
}

impl UserDeviceCert {
    pub const fn is_active(&self) -> bool {
        self.revoked_at.is_none()
    }
}

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,
        }
    }
}