systemprompt-users 0.2.1

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 systemprompt_identifiers::UserId;

use crate::UserService;
use crate::error::Result;
use crate::models::{User, UserRole};

#[derive(Debug)]
pub struct UserAdminService {
    user_service: UserService,
}

impl UserAdminService {
    pub const fn new(user_service: UserService) -> Self {
        Self { user_service }
    }

    pub async fn find_user(&self, identifier: &str) -> Result<Option<User>> {
        if uuid::Uuid::parse_str(identifier).is_ok() {
            let user_id = UserId::new(identifier);
            if let Some(user) = self.user_service.find_by_id(&user_id).await? {
                return Ok(Some(user));
            }
        }

        if identifier.contains('@') {
            return self.user_service.find_by_email(identifier).await;
        }

        self.user_service.find_by_name(identifier).await
    }

    pub async fn promote_to_admin(&self, user_identifier: &str) -> Result<PromoteResult> {
        let user = self.find_user(user_identifier).await?;
        let admin_role = UserRole::Admin.as_str().to_string();
        let user_role = UserRole::User.as_str().to_string();

        match user {
            Some(u) => {
                let current_roles = u.roles.clone();

                if current_roles.contains(&admin_role) {
                    return Ok(PromoteResult::AlreadyAdmin(u));
                }

                let mut new_roles = current_roles;
                if !new_roles.contains(&admin_role) {
                    new_roles.push(admin_role);
                }
                if !new_roles.contains(&user_role) {
                    new_roles.push(user_role);
                }

                let updated = self.user_service.assign_roles(&u.id, &new_roles).await?;
                Ok(PromoteResult::Promoted(updated, new_roles))
            },
            None => Ok(PromoteResult::UserNotFound),
        }
    }

    pub async fn demote_from_admin(&self, user_identifier: &str) -> Result<DemoteResult> {
        let user = self.find_user(user_identifier).await?;
        let admin_role = UserRole::Admin.as_str();
        let user_role = UserRole::User.as_str().to_string();

        match user {
            Some(u) => {
                let current_roles = u.roles.clone();

                if !current_roles.contains(&admin_role.to_string()) {
                    return Ok(DemoteResult::NotAdmin(u));
                }

                let new_roles: Vec<String> = current_roles
                    .into_iter()
                    .filter(|r| r != admin_role)
                    .collect();

                let mut final_roles = new_roles;
                if !final_roles.contains(&user_role) {
                    final_roles.push(user_role);
                }

                let updated = self.user_service.assign_roles(&u.id, &final_roles).await?;
                Ok(DemoteResult::Demoted(updated, final_roles))
            },
            None => Ok(DemoteResult::UserNotFound),
        }
    }
}

#[derive(Debug)]
pub enum PromoteResult {
    Promoted(User, Vec<String>),
    AlreadyAdmin(User),
    UserNotFound,
}

#[derive(Debug)]
pub enum DemoteResult {
    Demoted(User, Vec<String>),
    NotAdmin(User),
    UserNotFound,
}