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:
iven
2026-04-11 08:09:19 +08:00
parent 8a012f6c6a
commit 0baaf5f7ee
55 changed files with 5295 additions and 12 deletions

View File

@@ -1,4 +1,5 @@
pub mod error;
pub mod events;
pub mod module;
pub mod rbac;
pub mod types;

View File

@@ -0,0 +1,96 @@
use crate::error::AppError;
use crate::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());
}
}