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