fix: address Phase 1-2 audit findings

- CORS: replace permissive() with configurable whitelist (default.toml)
- Auth store: synchronously restore state at creation to eliminate
  flash-of-login-page on refresh
- MainLayout: menu highlight now tracks current route via useLocation
- Add extractErrorMessage() utility to reduce repeated error parsing
- Fix all clippy warnings across 4 crates (erp-auth, erp-config,
  erp-workflow, erp-message): remove unnecessary casts, use div_ceil,
  collapse nested ifs, reduce function arguments with DTOs
This commit is contained in:
iven
2026-04-11 12:36:34 +08:00
parent 5c899e6f4a
commit 3a05523d23
35 changed files with 283 additions and 187 deletions

View File

@@ -143,6 +143,14 @@ pub struct UpdateSettingReq {
pub setting_value: serde_json::Value,
}
/// 内部参数结构体,用于减少 SettingService::set 的参数数量。
pub struct SetSettingParams {
pub key: String,
pub scope: String,
pub scope_id: Option<Uuid>,
pub value: serde_json::Value,
}
// --- Numbering Rule DTOs ---
#[derive(Debug, Serialize, ToSchema)]

View File

@@ -35,7 +35,7 @@ where
let page = pagination.page.unwrap_or(1);
let page_size = pagination.limit();
let total_pages = (total + page_size - 1) / page_size;
let total_pages = total.div_ceil(page_size);
Ok(Json(ApiResponse::ok(PaginatedResponse {
data: dictionaries,

View File

@@ -7,7 +7,7 @@ use erp_core::rbac::require_permission;
use erp_core::types::{ApiResponse, Pagination, TenantContext};
use crate::config_state::ConfigState;
use crate::dto::{LanguageResp, UpdateLanguageReq};
use crate::dto::{LanguageResp, SetSettingParams, UpdateLanguageReq};
use crate::service::setting_service::SettingService;
/// GET /api/v1/languages
@@ -82,10 +82,12 @@ where
let value = serde_json::json!({"is_active": req.is_active});
SettingService::set(
&key,
"platform",
&None,
value,
SetSettingParams {
key,
scope: "platform".to_string(),
scope_id: None,
value,
},
ctx.tenant_id,
ctx.user_id,
&state.db,

View File

@@ -33,7 +33,7 @@ where
let page = pagination.page.unwrap_or(1);
let page_size = pagination.limit();
let total_pages = (total + page_size - 1) / page_size;
let total_pages = total.div_ceil(page_size);
Ok(Json(ApiResponse::ok(PaginatedResponse {
data: rules,

View File

@@ -8,7 +8,7 @@ use erp_core::types::{ApiResponse, TenantContext};
use uuid::Uuid;
use crate::config_state::ConfigState;
use crate::dto::{SettingResp, UpdateSettingReq};
use crate::dto::{SetSettingParams, SettingResp, UpdateSettingReq};
use crate::service::setting_service::SettingService;
/// GET /api/v1/settings/:key?scope=tenant&scope_id=xxx
@@ -54,10 +54,12 @@ where
require_permission(&ctx, "setting.update")?;
let setting = SettingService::set(
&key,
"tenant",
&None,
req.setting_value,
SetSettingParams {
key,
scope: "tenant".to_string(),
scope_id: None,
value: req.setting_value,
},
ctx.tenant_id,
ctx.user_id,
&state.db,

View File

@@ -7,7 +7,7 @@ use erp_core::rbac::require_permission;
use erp_core::types::{ApiResponse, TenantContext};
use crate::config_state::ConfigState;
use crate::dto::ThemeResp;
use crate::dto::{SetSettingParams, ThemeResp};
use crate::service::setting_service::SettingService;
/// GET /api/v1/theme
@@ -54,10 +54,12 @@ where
.map_err(|e| AppError::Validation(format!("主题配置序列化失败: {e}")))?;
SettingService::set(
"theme",
"tenant",
&None,
value,
SetSettingParams {
key: "theme".to_string(),
scope: "tenant".to_string(),
scope_id: None,
value,
},
ctx.tenant_id,
ctx.user_id,
&state.db,

View File

@@ -36,7 +36,7 @@ impl DictionaryService {
.await
.map_err(|e| ConfigError::Validation(e.to_string()))?;
let page_index = pagination.page.unwrap_or(1).saturating_sub(1) as u64;
let page_index = pagination.page.unwrap_or(1).saturating_sub(1);
let models = paginator
.fetch_page(page_index)
.await
@@ -229,10 +229,7 @@ impl DictionaryService {
dictionary_id: Uuid,
tenant_id: Uuid,
operator_id: Uuid,
label: &str,
value: &str,
sort_order: i32,
color: &Option<String>,
req: &crate::dto::CreateDictionaryItemReq,
db: &sea_orm::DatabaseConnection,
) -> ConfigResult<DictionaryItemResp> {
// Verify the dictionary exists and belongs to this tenant
@@ -247,7 +244,7 @@ impl DictionaryService {
let existing = dictionary_item::Entity::find()
.filter(dictionary_item::Column::DictionaryId.eq(dictionary_id))
.filter(dictionary_item::Column::TenantId.eq(tenant_id))
.filter(dictionary_item::Column::Value.eq(value))
.filter(dictionary_item::Column::Value.eq(&req.value))
.filter(dictionary_item::Column::DeletedAt.is_null())
.one(db)
.await
@@ -258,14 +255,15 @@ impl DictionaryService {
let now = Utc::now();
let id = Uuid::now_v7();
let sort_order = req.sort_order.unwrap_or(0);
let model = dictionary_item::ActiveModel {
id: Set(id),
tenant_id: Set(tenant_id),
dictionary_id: Set(dictionary_id),
label: Set(label.to_string()),
value: Set(value.to_string()),
label: Set(req.label.clone()),
value: Set(req.value.clone()),
sort_order: Set(sort_order),
color: Set(color.clone()),
color: Set(req.color.clone()),
created_at: Set(now),
updated_at: Set(now),
created_by: Set(operator_id),
@@ -281,10 +279,10 @@ impl DictionaryService {
Ok(DictionaryItemResp {
id,
dictionary_id,
label: label.to_string(),
value: value.to_string(),
label: req.label.clone(),
value: req.value.clone(),
sort_order,
color: color.clone(),
color: req.color.clone(),
})
}
@@ -293,10 +291,7 @@ impl DictionaryService {
item_id: Uuid,
tenant_id: Uuid,
operator_id: Uuid,
label: &Option<String>,
value: &Option<String>,
sort_order: &Option<i32>,
color: &Option<String>,
req: &crate::dto::UpdateDictionaryItemReq,
db: &sea_orm::DatabaseConnection,
) -> ConfigResult<DictionaryItemResp> {
let model = dictionary_item::Entity::find_by_id(item_id)
@@ -308,16 +303,16 @@ impl DictionaryService {
let mut active: dictionary_item::ActiveModel = model.into();
if let Some(l) = label {
if let Some(l) = &req.label {
active.label = Set(l.clone());
}
if let Some(v) = value {
if let Some(v) = &req.value {
active.value = Set(v.clone());
}
if let Some(s) = sort_order {
active.sort_order = Set(*s);
if let Some(s) = req.sort_order {
active.sort_order = Set(s);
}
if let Some(c) = color {
if let Some(c) = &req.color {
active.color = Set(Some(c.clone()));
}

View File

@@ -142,10 +142,10 @@ impl MenuService {
.map_err(|e| ConfigError::Validation(e.to_string()))?;
// 关联角色(如果提供了 role_ids
if let Some(role_ids) = &req.role_ids {
if !role_ids.is_empty() {
Self::assign_roles(id, role_ids, tenant_id, operator_id, db).await?;
}
if let Some(role_ids) = &req.role_ids
&& !role_ids.is_empty()
{
Self::assign_roles(id, role_ids, tenant_id, operator_id, db).await?;
}
event_bus.publish(erp_core::events::DomainEvent::new(

View File

@@ -32,7 +32,7 @@ impl NumberingService {
.await
.map_err(|e| ConfigError::Validation(e.to_string()))?;
let page_index = pagination.page.unwrap_or(1).saturating_sub(1) as u64;
let page_index = pagination.page.unwrap_or(1).saturating_sub(1);
let models = paginator
.fetch_page(page_index)
.await
@@ -40,7 +40,7 @@ impl NumberingService {
let resps: Vec<NumberingRuleResp> = models
.iter()
.map(|m| Self::model_to_resp(m))
.map(Self::model_to_resp)
.collect();
Ok((resps, total))

View File

@@ -71,10 +71,7 @@ impl SettingService {
/// If a record with the same (scope, scope_id, key) exists and is not
/// soft-deleted, it will be updated. Otherwise a new record is inserted.
pub async fn set(
key: &str,
scope: &str,
scope_id: &Option<Uuid>,
value: serde_json::Value,
params: crate::dto::SetSettingParams,
tenant_id: Uuid,
operator_id: Uuid,
db: &sea_orm::DatabaseConnection,
@@ -83,9 +80,9 @@ impl SettingService {
// Look for an existing non-deleted record
let existing = setting::Entity::find()
.filter(setting::Column::TenantId.eq(tenant_id))
.filter(setting::Column::Scope.eq(scope))
.filter(setting::Column::ScopeId.eq(*scope_id))
.filter(setting::Column::SettingKey.eq(key))
.filter(setting::Column::Scope.eq(&params.scope))
.filter(setting::Column::ScopeId.eq(params.scope_id))
.filter(setting::Column::SettingKey.eq(&params.key))
.filter(setting::Column::DeletedAt.is_null())
.one(db)
.await
@@ -94,7 +91,7 @@ impl SettingService {
if let Some(model) = existing {
// Update existing record
let mut active: setting::ActiveModel = model.into();
active.setting_value = Set(value.clone());
active.setting_value = Set(params.value.clone());
active.updated_at = Set(Utc::now());
active.updated_by = Set(operator_id);
@@ -108,8 +105,8 @@ impl SettingService {
tenant_id,
serde_json::json!({
"setting_id": updated.id,
"key": key,
"scope": scope,
"key": params.key,
"scope": params.scope,
}),
));
@@ -121,10 +118,10 @@ impl SettingService {
let model = setting::ActiveModel {
id: Set(id),
tenant_id: Set(tenant_id),
scope: Set(scope.to_string()),
scope_id: Set(*scope_id),
setting_key: Set(key.to_string()),
setting_value: Set(value),
scope: Set(params.scope.clone()),
scope_id: Set(params.scope_id),
setting_key: Set(params.key.clone()),
setting_value: Set(params.value),
created_at: Set(now),
updated_at: Set(now),
created_by: Set(operator_id),
@@ -142,8 +139,8 @@ impl SettingService {
tenant_id,
serde_json::json!({
"setting_id": id,
"key": key,
"scope": scope,
"key": params.key,
"scope": params.scope,
}),
));
@@ -171,7 +168,7 @@ impl SettingService {
.await
.map_err(|e| ConfigError::Validation(e.to_string()))?;
let page_index = pagination.page.unwrap_or(1).saturating_sub(1) as u64;
let page_index = pagination.page.unwrap_or(1).saturating_sub(1);
let models = paginator
.fetch_page(page_index)
.await
@@ -248,7 +245,7 @@ impl SettingService {
/// Returns a list of (scope, scope_id) tuples to try in order.
fn fallback_chain(
scope: &str,
scope_id: &Option<Uuid>,
_scope_id: &Option<Uuid>,
tenant_id: Uuid,
) -> ConfigResult<Vec<(String, Option<Uuid>)>> {
match scope {