systemprompt-users 0.6.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.
use systemprompt_identifiers::{DeviceCertId, UserId};

use crate::error::Result;
use crate::models::UserDeviceCert;
use crate::repository::UserRepository;

pub struct EnrollDeviceCertParams<'a> {
    pub id: &'a DeviceCertId,
    pub user_id: &'a UserId,
    pub fingerprint: &'a str,
    pub label: &'a str,
}

impl UserRepository {
    pub async fn enroll_device_cert(
        &self,
        params: EnrollDeviceCertParams<'_>,
    ) -> Result<UserDeviceCert> {
        let row = sqlx::query_as!(
            UserDeviceCert,
            r#"
            INSERT INTO user_device_certs (id, user_id, fingerprint, label)
            VALUES ($1, $2, $3, $4)
            RETURNING id, user_id, fingerprint, label, enrolled_at, revoked_at
            "#,
            params.id.as_str(),
            params.user_id.as_str(),
            params.fingerprint,
            params.label,
        )
        .fetch_one(&*self.write_pool)
        .await?;
        Ok(row)
    }

    pub async fn find_active_device_cert_by_fingerprint(
        &self,
        fingerprint: &str,
    ) -> Result<Option<UserDeviceCert>> {
        let row = sqlx::query_as!(
            UserDeviceCert,
            r#"
            SELECT id, user_id, fingerprint, label, enrolled_at, revoked_at
            FROM user_device_certs
            WHERE fingerprint = $1 AND revoked_at IS NULL
            "#,
            fingerprint,
        )
        .fetch_optional(&*self.pool)
        .await?;
        Ok(row)
    }

    pub async fn list_device_certs_for_user(
        &self,
        user_id: &UserId,
    ) -> Result<Vec<UserDeviceCert>> {
        let rows = sqlx::query_as!(
            UserDeviceCert,
            r#"
            SELECT id, user_id, fingerprint, label, enrolled_at, revoked_at
            FROM user_device_certs
            WHERE user_id = $1
            ORDER BY enrolled_at DESC
            "#,
            user_id.as_str(),
        )
        .fetch_all(&*self.pool)
        .await?;
        Ok(rows)
    }

    pub async fn revoke_device_cert(&self, id: &DeviceCertId, user_id: &UserId) -> Result<bool> {
        let result = sqlx::query!(
            r#"
            UPDATE user_device_certs
            SET revoked_at = CURRENT_TIMESTAMP
            WHERE id = $1 AND user_id = $2 AND revoked_at IS NULL
            "#,
            id.as_str(),
            user_id.as_str(),
        )
        .execute(&*self.write_pool)
        .await?;
        Ok(result.rows_affected() > 0)
    }
}