slave 0.4.0

Laravel-inspired CLI for Rust MCP servers: scaffold apps whose business operations are MCP tools over Streamable HTTP, with built-in auth, users, roles and Turso migrations
use axum::extract::FromRef;
use slave_framework::database::Database;
use slave_framework::turso::Connection;
use slave_framework::turso::transaction::TransactionBehavior;

use crate::base::auth::ADMIN_ROLE;
use crate::base::error::{AppError, AppResult};
use crate::base::state::AppState;
use crate::modules::role::models::role::Role;
use crate::modules::role::repositories::role_repository;
use crate::modules::user::user_repository;

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

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

impl RoleService {
    pub async fn list(&self) -> AppResult<Vec<Role>> {
        let conn = self.database.connect().await?;
        Ok(role_repository::all(&conn).await?)
    }

    pub async fn roles_of(&self, user_id: i64) -> AppResult<Vec<Role>> {
        let conn = self.database.connect().await?;
        ensure_user_exists(&conn, user_id).await?;
        Ok(role_repository::for_user(&conn, user_id).await?)
    }

    /// Grants `role_name` to a user and returns their roles.
    pub async fn assign(&self, user_id: i64, role_name: &str) -> AppResult<Vec<Role>> {
        let conn = self.database.connect().await?;
        ensure_user_exists(&conn, user_id).await?;
        let role = find_role(&conn, role_name).await?;
        role_repository::assign(&conn, user_id, role.id).await?;
        Ok(role_repository::for_user(&conn, user_id).await?)
    }

    /// Removes `role_name` from a user and returns their remaining roles.
    ///
    /// Refuses to remove the last admin, so the app can never lock itself out.
    pub async fn revoke(&self, user_id: i64, role_name: &str) -> AppResult<Vec<Role>> {
        let mut conn = self.database.connect().await?;
        let tx = conn
            .transaction_with_behavior(TransactionBehavior::Immediate)
            .await?;

        ensure_user_exists(&tx, user_id).await?;
        let role = find_role(&tx, role_name).await?;
        if role.name == ADMIN_ROLE && role_repository::count_users_with(&tx, role.id).await? <= 1 {
            let holders = role_repository::for_user(&tx, user_id).await?;
            if holders.iter().any(|r| r.id == role.id) {
                return Err(AppError::Conflict("cannot remove the last admin".into()));
            }
        }
        if !role_repository::revoke(&tx, user_id, role.id).await? {
            return Err(AppError::NotFound(format!(
                "role `{role_name}` for user {user_id}"
            )));
        }
        let roles = role_repository::for_user(&tx, user_id).await?;
        tx.commit().await?;
        Ok(roles)
    }
}

async fn ensure_user_exists(conn: &Connection, user_id: i64) -> AppResult<()> {
    match user_repository::find(conn, user_id).await? {
        Some(_) => Ok(()),
        None => Err(AppError::NotFound(format!("user {user_id}"))),
    }
}

async fn find_role(conn: &Connection, name: &str) -> AppResult<Role> {
    role_repository::find_by_name(conn, name)
        .await?
        .ok_or_else(|| AppError::Validation(format!("unknown role `{name}`")))
}