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.
357 lines
12 KiB
Rust
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())
|
|
}
|
|
}
|