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}`")))
}