feat(config): add system configuration module (Phase 3)
Implement the complete erp-config crate with: - Data dictionaries (CRUD + items management) - Dynamic menus (tree structure with role filtering) - System settings (hierarchical: platform > tenant > org > user) - Numbering rules (concurrency-safe via PostgreSQL advisory_lock) - Theme and language configuration (via settings store) - 6 database migrations (dictionaries, menus, settings, numbering_rules) - Frontend Settings page with 5 tabs (dictionary, menu, numbering, settings, theme) Refactor: move RBAC functions (require_permission) from erp-auth to erp-core to avoid cross-module dependencies. Add 20 new seed permissions for config module operations.
This commit is contained in:
@@ -12,7 +12,7 @@ use crate::dto::{
|
||||
CreateDepartmentReq, CreateOrganizationReq, CreatePositionReq, DepartmentResp,
|
||||
OrganizationResp, PositionResp, UpdateDepartmentReq, UpdateOrganizationReq, UpdatePositionReq,
|
||||
};
|
||||
use crate::middleware::rbac::require_permission;
|
||||
use erp_core::rbac::require_permission;
|
||||
use crate::service::dept_service::DeptService;
|
||||
use crate::service::org_service::OrgService;
|
||||
use crate::service::position_service::PositionService;
|
||||
|
||||
@@ -9,7 +9,7 @@ use uuid::Uuid;
|
||||
|
||||
use crate::auth_state::AuthState;
|
||||
use crate::dto::{AssignPermissionsReq, CreateRoleReq, PermissionResp, RoleResp, UpdateRoleReq};
|
||||
use crate::middleware::rbac::require_permission;
|
||||
use erp_core::rbac::require_permission;
|
||||
use crate::service::permission_service::PermissionService;
|
||||
use crate::service::role_service::RoleService;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use uuid::Uuid;
|
||||
|
||||
use crate::auth_state::AuthState;
|
||||
use crate::dto::{CreateUserReq, UpdateUserReq, UserResp};
|
||||
use crate::middleware::rbac::require_permission;
|
||||
use erp_core::rbac::require_permission;
|
||||
use crate::service::user_service::UserService;
|
||||
|
||||
/// GET /api/v1/users
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod jwt_auth;
|
||||
pub mod rbac;
|
||||
|
||||
pub use jwt_auth::jwt_auth_middleware_fn;
|
||||
pub use rbac::{require_any_permission, require_permission, require_role};
|
||||
pub use erp_core::rbac::{require_any_permission, require_permission, require_role};
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
use erp_core::error::AppError;
|
||||
use erp_core::types::TenantContext;
|
||||
|
||||
/// Check whether the `TenantContext` includes the specified permission code.
|
||||
///
|
||||
/// Returns `Ok(())` if the permission is present, or `AppError::Forbidden` otherwise.
|
||||
pub fn require_permission(ctx: &TenantContext, permission: &str) -> Result<(), AppError> {
|
||||
if ctx.permissions.iter().any(|p| p == permission) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AppError::Forbidden(format!("需要权限: {}", permission)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether the `TenantContext` includes at least one of the specified permission codes.
|
||||
///
|
||||
/// Useful when multiple permissions can grant access to the same resource.
|
||||
pub fn require_any_permission(
|
||||
ctx: &TenantContext,
|
||||
permissions: &[&str],
|
||||
) -> Result<(), AppError> {
|
||||
let has_any = permissions
|
||||
.iter()
|
||||
.any(|p| ctx.permissions.iter().any(|up| up == *p));
|
||||
|
||||
if has_any {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AppError::Forbidden(format!(
|
||||
"需要以下权限之一: {}",
|
||||
permissions.join(", ")
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether the `TenantContext` includes the specified role code.
|
||||
///
|
||||
/// Returns `Ok(())` if the role is present, or `AppError::Forbidden` otherwise.
|
||||
pub fn require_role(ctx: &TenantContext, role: &str) -> Result<(), AppError> {
|
||||
if ctx.roles.iter().any(|r| r == role) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AppError::Forbidden(format!("需要角色: {}", role)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn test_ctx(roles: Vec<&str>, permissions: Vec<&str>) -> TenantContext {
|
||||
TenantContext {
|
||||
tenant_id: Uuid::now_v7(),
|
||||
user_id: Uuid::now_v7(),
|
||||
roles: roles.into_iter().map(String::from).collect(),
|
||||
permissions: permissions.into_iter().map(String::from).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn require_permission_succeeds_when_present() {
|
||||
let ctx = test_ctx(vec![], vec!["user.read", "user.write"]);
|
||||
assert!(require_permission(&ctx, "user.read").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn require_permission_fails_when_missing() {
|
||||
let ctx = test_ctx(vec![], vec!["user.read"]);
|
||||
assert!(require_permission(&ctx, "user.delete").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn require_any_permission_succeeds_with_match() {
|
||||
let ctx = test_ctx(vec![], vec!["user.read"]);
|
||||
assert!(require_any_permission(&ctx, &["user.delete", "user.read"]).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn require_any_permission_fails_with_no_match() {
|
||||
let ctx = test_ctx(vec![], vec!["user.read"]);
|
||||
assert!(require_any_permission(&ctx, &["user.delete", "user.admin"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn require_role_succeeds_when_present() {
|
||||
let ctx = test_ctx(vec!["admin", "user"], vec![]);
|
||||
assert!(require_role(&ctx, "admin").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn require_role_fails_when_missing() {
|
||||
let ctx = test_ctx(vec!["user"], vec![]);
|
||||
assert!(require_role(&ctx, "admin").is_err());
|
||||
}
|
||||
}
|
||||
@@ -84,10 +84,27 @@ const DEFAULT_PERMISSIONS: &[(&str, &str, &str, &str, &str)] = &[
|
||||
("position:read", "查看岗位", "position", "read", "查看岗位"),
|
||||
("position:update", "编辑岗位", "position", "update", "编辑岗位"),
|
||||
("position:delete", "删除岗位", "position", "delete", "删除岗位"),
|
||||
// Config module permissions
|
||||
("dictionary:create", "创建字典", "dictionary", "create", "创建数据字典"),
|
||||
("dictionary:list", "查看字典", "dictionary", "list", "查看数据字典"),
|
||||
("dictionary:update", "编辑字典", "dictionary", "update", "编辑数据字典"),
|
||||
("dictionary:delete", "删除字典", "dictionary", "delete", "删除数据字典"),
|
||||
("menu:list", "查看菜单", "menu", "list", "查看菜单配置"),
|
||||
("menu:update", "编辑菜单", "menu", "update", "编辑菜单配置"),
|
||||
("setting:read", "查看配置", "setting", "read", "查看系统参数"),
|
||||
("setting:update", "编辑配置", "setting", "update", "编辑系统参数"),
|
||||
("numbering:create", "创建编号规则", "numbering", "create", "创建编号规则"),
|
||||
("numbering:list", "查看编号规则", "numbering", "list", "查看编号规则"),
|
||||
("numbering:update", "编辑编号规则", "numbering", "update", "编辑编号规则"),
|
||||
("numbering:generate", "生成编号", "numbering", "generate", "生成文档编号"),
|
||||
("theme:read", "查看主题", "theme", "read", "查看主题设置"),
|
||||
("theme:update", "编辑主题", "theme", "update", "编辑主题设置"),
|
||||
("language:list", "查看语言", "language", "list", "查看语言配置"),
|
||||
("language:update", "编辑语言", "language", "update", "编辑语言配置"),
|
||||
];
|
||||
|
||||
/// Indices of read-only permissions within DEFAULT_PERMISSIONS.
|
||||
const READ_PERM_INDICES: &[usize] = &[1, 5, 9, 11, 15, 19];
|
||||
const READ_PERM_INDICES: &[usize] = &[1, 5, 9, 11, 15, 19, 23, 24, 28, 29, 34, 38];
|
||||
|
||||
/// Seed default auth data for a new tenant.
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user