use db::conn::sea_orm::*; use db::space::space_member_roles as member_role_entity; use db::space::space_role_permissions as role_perm_entity; use db::space::space_roles as role_entity; use db::space::spaces as space_entity; use time::OffsetDateTime; use uuid::Uuid; use crate::error::{SpaceError, SpaceResult}; use crate::features::authz; use crate::features::SpaceService; use super::types::{CreateRoleCmd, RolePage, RoleView, UpdateRoleCmd}; async fn owner_space( svc: &SpaceService, caller: Uuid, space_id: Uuid, ) -> SpaceResult { let db = svc.db.reader(); let space = space_entity::Entity::find_by_id(space_id) .one(&db) .await? .ok_or_else(|| SpaceError::NotFound(format!("space {space_id}")))?; if space.owner_id != caller { return Err(SpaceError::Forbidden( "only the owner can manage roles".to_string(), )); } Ok(space) } async fn role_exists(db: &C, role_id: Uuid) -> SpaceResult<()> where C: db::conn::sea_orm::ConnectionTrait, { role_entity::Entity::find_by_id(role_id) .one(db) .await? .map(|_| ()) .ok_or_else(|| SpaceError::NotFound(format!("role {role_id}"))) } async fn load_permissions(db: &C, role_id: Uuid) -> SpaceResult> where C: db::conn::sea_orm::ConnectionTrait, { let rows = role_perm_entity::Entity::find() .filter(role_perm_entity::Column::RoleId.eq(role_id)) .all(db) .await?; Ok(rows.into_iter().map(|r| r.permission).collect()) } async fn replace_permissions( txn: &db::conn::sea_orm::DatabaseTransaction, role_id: Uuid, permissions: &[String], ) -> SpaceResult<()> { role_perm_entity::Entity::delete_many() .filter(role_perm_entity::Column::RoleId.eq(role_id)) .exec(txn) .await?; for perm in permissions { let p: role_perm_entity::ActiveModel = role_perm_entity::ActiveModel::builder() .set_role_id(role_id) .set_permission(perm.clone()) .into(); p.insert(txn).await?; } Ok(()) } pub async fn create_role( svc: &SpaceService, caller: Uuid, space_id: Uuid, cmd: CreateRoleCmd, ) -> SpaceResult { let name = cmd.name.trim().to_string(); if name.is_empty() { return Err(SpaceError::BadRequest("role name cannot be empty".to_string())); } if name.chars().count() > svc.config.max_role_name_len { return Err(SpaceError::BadRequest(format!( "role name exceeds {} characters", svc.config.max_role_name_len ))); } owner_space(svc, caller, space_id).await?; let db = svc.db.writer(); let txn = db.begin().await?; let existing = role_entity::Entity::find() .filter(role_entity::Column::SpaceId.eq(space_id)) .filter(role_entity::Column::Name.eq(&name)) .one(&txn) .await?; if existing.is_some() { return Err(SpaceError::Conflict(format!( "role with name {name} already exists in space {space_id}" ))); } let now = OffsetDateTime::now_utc(); let role_id = Uuid::new_v4(); let color = cmd.color.clone().unwrap_or_default(); let unicode_emoji = cmd.unicode_emoji.clone(); let icon_hash = cmd.icon_hash.clone(); let position = cmd.position.unwrap_or(0); let role: role_entity::ActiveModel = role_entity::ActiveModel::builder() .set_id(role_id) .set_space_id(space_id) .set_name(name.clone()) .set_position(position) .set_color(color.clone()) .set_unicode_emoji(unicode_emoji.clone()) .set_icon_hash(icon_hash.clone()) .set_version(1) .set_created_at(now) .set_updated_at(Some(now)) .set_created_by(caller) .into(); role.insert(&txn).await?; replace_permissions(&txn, role_id, &cmd.permissions).await?; txn.commit().await?; let mut view = RoleView::from(role_entity::Model { id: role_id, space_id, name, position, color, unicode_emoji, icon_hash, version: 1, created_at: now, updated_at: Some(now), created_by: caller, }); view.permissions = cmd.permissions; Ok(view) } pub async fn get_role( svc: &SpaceService, caller: Option, space_id: Uuid, role_id: Uuid, ) -> SpaceResult { let db = svc.db.reader(); let role = role_entity::Entity::find_by_id(role_id) .one(&db) .await? .ok_or_else(|| SpaceError::NotFound(format!("role {role_id}")))?; if role.space_id != space_id { return Err(SpaceError::NotFound(format!("role {role_id}"))); } let space = space_entity::Entity::find_by_id(space_id) .one(&db) .await? .ok_or_else(|| SpaceError::NotFound(format!("space {space_id}")))?; if !space.is_active { return Err(SpaceError::NotFound(format!("role {role_id}"))); } if !space.public { let caller = caller.ok_or_else(|| SpaceError::NotFound(format!("role {role_id}")))?; match authz::require_perm(svc, caller, space_id, authz::MANAGE_ROLES).await { Ok(_) => {} Err(SpaceError::Forbidden(_)) => { return Err(SpaceError::NotFound(format!("role {role_id}"))); } Err(e) => return Err(e), } } let perms = load_permissions(&db, role_id).await?; let mut view = RoleView::from(role); view.permissions = perms; Ok(view) } pub async fn update_role( svc: &SpaceService, caller: Uuid, space_id: Uuid, role_id: Uuid, cmd: UpdateRoleCmd, ) -> SpaceResult { owner_space(svc, caller, space_id).await?; let db = svc.db.writer(); let txn = db.begin().await?; let role = role_entity::Entity::find_by_id(role_id) .one(&txn) .await? .ok_or_else(|| SpaceError::NotFound(format!("role {role_id}")))?; if role.space_id != space_id { return Err(SpaceError::NotFound(format!("role {role_id}"))); } let mut active: role_entity::ActiveModel = role.into(); if let Some(name) = cmd.name { let name = name.trim().to_string(); if name.is_empty() { return Err(SpaceError::BadRequest("role name cannot be empty".to_string())); } if name.chars().count() > svc.config.max_role_name_len { return Err(SpaceError::BadRequest(format!( "role name exceeds {} characters", svc.config.max_role_name_len ))); } let dup = role_entity::Entity::find() .filter(role_entity::Column::SpaceId.eq(space_id)) .filter(role_entity::Column::Name.eq(&name)) .filter(role_entity::Column::Id.ne(role_id)) .one(&txn) .await?; if dup.is_some() { return Err(SpaceError::Conflict(format!( "role with name {name} already exists in space {space_id}" ))); } active.name = Set(name); } if let Some(color) = cmd.color { active.color = Set(color); } if let Some(v) = cmd.unicode_emoji { active.unicode_emoji = Set(v); } if let Some(v) = cmd.icon_hash { active.icon_hash = Set(v); } if let Some(position) = cmd.position { active.position = Set(position); } active.version = Set(active.version.take().map_or(1, |v| v + 1)); active.updated_at = Set(Some(OffsetDateTime::now_utc())); let saved = active.update(&txn).await?; if let Some(perms) = cmd.permissions { replace_permissions(&txn, role_id, &perms).await?; } txn.commit().await?; let perms = load_permissions(&svc.db.reader(), role_id).await?; let mut view = RoleView::from(saved); view.permissions = perms; Ok(view) } pub async fn delete_role( svc: &SpaceService, caller: Uuid, space_id: Uuid, role_id: Uuid, ) -> SpaceResult<()> { owner_space(svc, caller, space_id).await?; let db = svc.db.writer(); let txn = db.begin().await?; role_exists(&txn, role_id).await?; member_role_entity::Entity::delete_many() .filter(member_role_entity::Column::RoleId.eq(role_id)) .exec(&txn) .await?; role_perm_entity::Entity::delete_many() .filter(role_perm_entity::Column::RoleId.eq(role_id)) .exec(&txn) .await?; role_entity::Entity::delete_by_id(role_id).exec(&txn).await?; txn.commit().await?; Ok(()) } pub async fn list_roles( svc: &SpaceService, caller: Uuid, space_id: Uuid, limit: u64, offset: u64, ) -> SpaceResult { authz::require_perm(svc, caller, space_id, authz::MANAGE_ROLES).await?; let db = svc.db.reader(); let limit = std::cmp::min(limit, svc.config.max_pagination); let roles = role_entity::Entity::find() .filter(role_entity::Column::SpaceId.eq(space_id)) .order_by_asc(role_entity::Column::Position) .limit(Some(limit)) .offset(Some(offset)) .all(&db) .await?; let total = role_entity::Entity::find() .filter(role_entity::Column::SpaceId.eq(space_id)) .count(&db) .await?; let mut views = Vec::with_capacity(roles.len()); for role in roles { let perms = load_permissions(&db, role.id).await?; let mut view = RoleView::from(role); view.permissions = perms; views.push(view); } Ok(RolePage { items: views, total: total as i64, }) } pub async fn assign_role( svc: &SpaceService, caller: Uuid, space_id: Uuid, user_id: Uuid, role_id: Uuid, ) -> SpaceResult<()> { owner_space(svc, caller, space_id).await?; let db = svc.db.writer(); let txn = db.begin().await?; role_exists(&txn, role_id).await?; let mr: member_role_entity::ActiveModel = member_role_entity::ActiveModel::builder() .set_space_id(space_id) .set_user_id(user_id) .set_role_id(role_id) .set_created_at(OffsetDateTime::now_utc()) .into(); mr.insert(&txn).await?; txn.commit().await?; Ok(()) } pub async fn unassign_role( svc: &SpaceService, caller: Uuid, _space_id: Uuid, user_id: Uuid, role_id: Uuid, ) -> SpaceResult<()> { owner_space(svc, caller, _space_id).await?; let db = svc.db.writer(); let txn = db.begin().await?; member_role_entity::Entity::delete_many() .filter(member_role_entity::Column::UserId.eq(user_id)) .filter(member_role_entity::Column::RoleId.eq(role_id)) .exec(&txn) .await?; txn.commit().await?; Ok(()) }