Files
hms/crates/erp-auth/src/service/role_service.rs
iven db2cd24259 feat(core): add audit logging to all mutation operations
Create audit_log SeaORM entity and audit_service::record() helper.
Integrate audit recording into 35 mutation endpoints across all modules:
- erp-auth: user/role/organization/department/position CRUD (15 actions)
- erp-config: dictionary/menu/setting/numbering_rule CRUD (15 actions)
- erp-workflow: definition/instance/task operations (8 actions)
- erp-message: send/system/mark_read/delete (5 actions)

Uses fire-and-forget pattern — audit failures logged but non-blocking.
2026-04-11 23:48:45 +08:00

357 lines
12 KiB
Rust

use chrono::Utc;
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set,
};
use uuid::Uuid;
use crate::dto::{PermissionResp, RoleResp};
use crate::entity::{permission, role, role_permission};
use crate::error::AuthError;
use crate::error::AuthResult;
use erp_core::audit::AuditLog;
use erp_core::audit_service;
use erp_core::error::check_version;
use erp_core::events::EventBus;
use erp_core::types::Pagination;
/// Role CRUD service — create, read, update, soft-delete roles within a tenant,
/// and manage role-permission assignments.
pub struct RoleService;
impl RoleService {
/// List roles within a tenant with pagination.
///
/// Returns `(roles, total_count)`.
pub async fn list(
tenant_id: Uuid,
pagination: &Pagination,
db: &sea_orm::DatabaseConnection,
) -> AuthResult<(Vec<RoleResp>, u64)> {
let paginator = role::Entity::find()
.filter(role::Column::TenantId.eq(tenant_id))
.filter(role::Column::DeletedAt.is_null())
.paginate(db, pagination.limit());
let total = paginator
.num_items()
.await
.map_err(|e| AuthError::Validation(e.to_string()))?;
let page_index = pagination.page.unwrap_or(1).saturating_sub(1);
let models = paginator
.fetch_page(page_index)
.await
.map_err(|e| AuthError::Validation(e.to_string()))?;
let resps: Vec<RoleResp> = models
.iter()
.map(|m| RoleResp {
id: m.id,
name: m.name.clone(),
code: m.code.clone(),
description: m.description.clone(),
is_system: m.is_system,
version: m.version,
})
.collect();
Ok((resps, total))
}
/// Fetch a single role by ID, scoped to the given tenant.
pub async fn get_by_id(
id: Uuid,
tenant_id: Uuid,
db: &sea_orm::DatabaseConnection,
) -> AuthResult<RoleResp> {
let model = role::Entity::find_by_id(id)
.one(db)
.await
.map_err(|e| AuthError::Validation(e.to_string()))?
.filter(|r| r.tenant_id == tenant_id && r.deleted_at.is_none())
.ok_or_else(|| AuthError::Validation("角色不存在".to_string()))?;
Ok(RoleResp {
id: model.id,
name: model.name.clone(),
code: model.code.clone(),
description: model.description.clone(),
is_system: model.is_system,
version: model.version,
})
}
/// Create a new role within the current tenant.
///
/// Validates code uniqueness, then inserts the record and publishes
/// a `role.created` domain event.
pub async fn create(
tenant_id: Uuid,
operator_id: Uuid,
name: &str,
code: &str,
description: &Option<String>,
db: &sea_orm::DatabaseConnection,
event_bus: &EventBus,
) -> AuthResult<RoleResp> {
// Check code uniqueness within tenant
let existing = role::Entity::find()
.filter(role::Column::TenantId.eq(tenant_id))
.filter(role::Column::Code.eq(code))
.filter(role::Column::DeletedAt.is_null())
.one(db)
.await
.map_err(|e| AuthError::Validation(e.to_string()))?;
if existing.is_some() {
return Err(AuthError::Validation("角色编码已存在".to_string()));
}
let now = Utc::now();
let id = Uuid::now_v7();
let model = role::ActiveModel {
id: Set(id),
tenant_id: Set(tenant_id),
name: Set(name.to_string()),
code: Set(code.to_string()),
description: Set(description.clone()),
is_system: Set(false),
created_at: Set(now),
updated_at: Set(now),
created_by: Set(operator_id),
updated_by: Set(operator_id),
deleted_at: Set(None),
version: Set(1),
};
model
.insert(db)
.await
.map_err(|e| AuthError::Validation(e.to_string()))?;
event_bus.publish(erp_core::events::DomainEvent::new(
"role.created",
tenant_id,
serde_json::json!({ "role_id": id, "code": code }),
));
audit_service::record(
AuditLog::new(tenant_id, Some(operator_id), "role.create", "role")
.with_resource_id(id),
db,
)
.await;
Ok(RoleResp {
id,
name: name.to_string(),
code: code.to_string(),
description: description.clone(),
is_system: false,
version: 1,
})
}
/// Update editable role fields (name and description).
///
/// Code and is_system cannot be changed after creation.
pub async fn update(
id: Uuid,
tenant_id: Uuid,
operator_id: Uuid,
name: &Option<String>,
description: &Option<String>,
version: i32,
db: &sea_orm::DatabaseConnection,
) -> AuthResult<RoleResp> {
let model = role::Entity::find_by_id(id)
.one(db)
.await
.map_err(|e| AuthError::Validation(e.to_string()))?
.filter(|r| r.tenant_id == tenant_id && r.deleted_at.is_none())
.ok_or_else(|| AuthError::Validation("角色不存在".to_string()))?;
let next_ver = check_version(version, model.version)?;
let mut active: role::ActiveModel = model.into();
if let Some(name) = name {
active.name = Set(name.clone());
}
if let Some(desc) = description {
active.description = Set(Some(desc.clone()));
}
active.updated_at = Set(Utc::now());
active.updated_by = Set(operator_id);
active.version = Set(next_ver);
let updated = active
.update(db)
.await
.map_err(|e| AuthError::Validation(e.to_string()))?;
audit_service::record(
AuditLog::new(tenant_id, Some(operator_id), "role.update", "role")
.with_resource_id(id),
db,
)
.await;
Ok(RoleResp {
id: updated.id,
name: updated.name.clone(),
code: updated.code.clone(),
description: updated.description.clone(),
is_system: updated.is_system,
version: updated.version,
})
}
/// Soft-delete a role by setting the `deleted_at` timestamp.
///
/// System roles cannot be deleted.
pub async fn delete(
id: Uuid,
tenant_id: Uuid,
operator_id: Uuid,
db: &sea_orm::DatabaseConnection,
event_bus: &EventBus,
) -> AuthResult<()> {
let model = role::Entity::find_by_id(id)
.one(db)
.await
.map_err(|e| AuthError::Validation(e.to_string()))?
.filter(|r| r.tenant_id == tenant_id && r.deleted_at.is_none())
.ok_or_else(|| AuthError::Validation("角色不存在".to_string()))?;
if model.is_system {
return Err(AuthError::Validation("系统角色不可删除".to_string()));
}
let current_version = model.version;
let mut active: role::ActiveModel = model.into();
active.deleted_at = Set(Some(Utc::now()));
active.updated_at = Set(Utc::now());
active.updated_by = Set(operator_id);
active.version = Set(current_version + 1);
active
.update(db)
.await
.map_err(|e| AuthError::Validation(e.to_string()))?;
event_bus.publish(erp_core::events::DomainEvent::new(
"role.deleted",
tenant_id,
serde_json::json!({ "role_id": id }),
));
audit_service::record(
AuditLog::new(tenant_id, Some(operator_id), "role.delete", "role")
.with_resource_id(id),
db,
)
.await;
Ok(())
}
/// Replace all permission assignments for a role.
///
/// Soft-deletes existing assignments and creates new ones.
pub async fn assign_permissions(
role_id: Uuid,
tenant_id: Uuid,
operator_id: Uuid,
permission_ids: &[Uuid],
db: &sea_orm::DatabaseConnection,
) -> AuthResult<()> {
// Verify the role exists and belongs to this tenant
let _role = role::Entity::find_by_id(role_id)
.one(db)
.await
.map_err(|e| AuthError::Validation(e.to_string()))?
.filter(|r| r.tenant_id == tenant_id && r.deleted_at.is_none())
.ok_or_else(|| AuthError::Validation("角色不存在".to_string()))?;
// Soft-delete existing role_permission rows
let existing = role_permission::Entity::find()
.filter(role_permission::Column::RoleId.eq(role_id))
.filter(role_permission::Column::TenantId.eq(tenant_id))
.filter(role_permission::Column::DeletedAt.is_null())
.all(db)
.await
.map_err(|e| AuthError::Validation(e.to_string()))?;
let now = Utc::now();
for rp in existing {
let mut active: role_permission::ActiveModel = rp.into();
active.deleted_at = Set(Some(now));
active.updated_at = Set(now);
active.updated_by = Set(operator_id);
active
.update(db)
.await
.map_err(|e| AuthError::Validation(e.to_string()))?;
}
// Insert new role_permission rows
for perm_id in permission_ids {
let rp = role_permission::ActiveModel {
role_id: Set(role_id),
permission_id: Set(*perm_id),
tenant_id: Set(tenant_id),
created_at: Set(now),
updated_at: Set(now),
created_by: Set(operator_id),
updated_by: Set(operator_id),
deleted_at: Set(None),
version: Set(1),
};
rp.insert(db)
.await
.map_err(|e| AuthError::Validation(e.to_string()))?;
}
Ok(())
}
/// Fetch all permissions assigned to a role.
///
/// Resolves through the role_permission join table.
pub async fn get_role_permissions(
role_id: Uuid,
tenant_id: Uuid,
db: &sea_orm::DatabaseConnection,
) -> AuthResult<Vec<PermissionResp>> {
let rp_rows = role_permission::Entity::find()
.filter(role_permission::Column::RoleId.eq(role_id))
.filter(role_permission::Column::TenantId.eq(tenant_id))
.filter(role_permission::Column::DeletedAt.is_null())
.all(db)
.await
.map_err(|e| AuthError::Validation(e.to_string()))?;
let perm_ids: Vec<Uuid> = rp_rows.iter().map(|rp| rp.permission_id).collect();
if perm_ids.is_empty() {
return Ok(vec![]);
}
let perms = permission::Entity::find()
.filter(permission::Column::Id.is_in(perm_ids))
.filter(permission::Column::TenantId.eq(tenant_id))
.all(db)
.await
.map_err(|e| AuthError::Validation(e.to_string()))?;
Ok(perms
.iter()
.map(|p| PermissionResp {
id: p.id,
code: p.code.clone(),
name: p.name.clone(),
resource: p.resource.clone(),
action: p.action.clone(),
description: p.description.clone(),
})
.collect())
}
}