slave 0.13.1

CLI for slave, a Laravel-inspired Rust framework for MCP-first apps: scaffold an MCP backend with auth, roles, teams and SurrealDB migrations, add server-rendered web apps, and route them by domain or path
use axum::extract::FromRef;
use slave_framework::database::{Db, Executor};

use crate::base::auth::SUPER_ADMIN_ROLE;
use crate::base::error::{AppError, AppResult};
use crate::base::state::AppState;
use crate::modules::role::models::role::Role;
use crate::modules::user::User;

/// Business rules for roles and who holds them.
#[derive(Debug, Clone)]
pub struct RoleService {
    db: Db,
}

impl FromRef<AppState> for RoleService {
    fn from_ref(state: &AppState) -> Self {
        Self {
            db: state.db.clone(),
        }
    }
}

impl RoleService {
    pub async fn list(&self) -> AppResult<Vec<Role>> {
        let roles: Vec<Role> = self
            .db
            .query(format!("SELECT {} FROM role ORDER BY name", Role::FIELDS))
            .await?
            .take(0)?;
        Ok(roles)
    }

    pub async fn roles_of(&self, user_id: i64) -> AppResult<Vec<Role>> {
        find_user(&self.db, user_id).await?;
        roles_of(&self.db, user_id).await
    }

    /// Grants `role_name` to a user and returns their roles. Granting a role
    /// the user already has changes nothing.
    pub async fn assign(&self, user_id: i64, role_name: &str) -> AppResult<Vec<Role>> {
        find_user(&self.db, user_id).await?;
        let role = find_role(&self.db, role_name).await?;
        self.db
            .query(
                "UPSERT type::record('user_role', [$user_id, $role_id])
                 SET user_id = $user_id, role_id = $role_id",
            )
            .bind(("user_id", user_id))
            .bind(("role_id", role.id))
            .await?
            .check()?;
        roles_of(&self.db, user_id).await
    }

    /// Removes `role_name` from a user and returns their remaining roles.
    ///
    /// Refuses to remove the last super admin, so the app can never lock
    /// itself out. Every revoke writes the role's record, so two concurrent
    /// revokes conflict and one is retried: they cannot both see a second
    /// admin and remove the last two.
    pub async fn revoke(&self, user_id: i64, role_name: &str) -> AppResult<Vec<Role>> {
        slave_framework::transaction!(&self.db, |tx| {
            find_user(&tx, user_id).await?;
            let role = find_role(&tx, role_name).await?;
            let held: Option<i64> = tx
                .query("SELECT VALUE user_id FROM ONLY type::record('user_role', [$user_id, $role_id])")
                .bind(("user_id", user_id))
                .bind(("role_id", role.id))
                .await?
                .take(0)?;
            if held.is_none() {
                return Err(AppError::NotFound(format!(
                    "role `{role_name}` for user {user_id}"
                )));
            }
            tx.query("UPDATE type::record('role', $role_id) SET revision += 1")
                .bind(("role_id", role.id))
                .await?
                .check()?;
            if role.name == SUPER_ADMIN_ROLE {
                let admins: Option<i64> = tx
                    .query("RETURN count(SELECT * FROM user_role WHERE role_id = $role_id)")
                    .bind(("role_id", role.id))
                    .await?
                    .take(0)?;
                if admins.unwrap_or(0) <= 1 {
                    return Err(AppError::Conflict(
                        "cannot remove the last super admin".into(),
                    ));
                }
            }
            tx.query("DELETE type::record('user_role', [$user_id, $role_id])")
                .bind(("user_id", user_id))
                .bind(("role_id", role.id))
                .await?
                .check()?;
            roles_of(&tx, user_id).await
        })
    }
}

/// The roles of user `user_id`, by name.
pub(crate) async fn roles_of(db: &impl Executor, user_id: i64) -> AppResult<Vec<Role>> {
    let roles: Vec<Role> = db
        .query(format!(
            "SELECT {} FROM role
             WHERE record::id(id) IN (SELECT VALUE role_id FROM user_role WHERE user_id = $user_id)
             ORDER BY name",
            Role::FIELDS
        ))
        .bind(("user_id", user_id))
        .await?
        .take(0)?;
    Ok(roles)
}

async fn find_user(db: &impl Executor, user_id: i64) -> AppResult<User> {
    let user: Option<User> = db
        .query(format!(
            "SELECT {} FROM type::record('user', $id)",
            User::FIELDS
        ))
        .bind(("id", user_id))
        .await?
        .take(0)?;
    user.ok_or_else(|| AppError::NotFound(format!("user {user_id}")))
}

async fn find_role(db: &impl Executor, name: &str) -> AppResult<Role> {
    let role: Option<Role> = db
        .query(format!(
            "SELECT {} FROM role WHERE name = $name",
            Role::FIELDS
        ))
        .bind(("name", name.to_owned()))
        .await?
        .take(0)?;
    role.ok_or_else(|| AppError::Validation(format!("unknown role `{name}`")))
}