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

@@ -10,7 +10,7 @@ use uuid::Uuid;
use crate::dto::NodeType;
use crate::engine::expression::ExpressionEvaluator;
use crate::engine::model::FlowGraph;
use crate::entity::{token, process_instance};
use crate::entity::{token, process_instance, task};
use crate::error::{WorkflowError, WorkflowResult};
/// Token 驱动的流程执行引擎。
@@ -264,6 +264,36 @@ impl FlowExecutor {
.await
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
// UserTask: 同时创建 task 记录
if node.node_type == NodeType::UserTask {
let task_model = task::ActiveModel {
id: Set(Uuid::now_v7()),
tenant_id: Set(tenant_id),
instance_id: Set(instance_id),
token_id: Set(new_token_id),
node_id: Set(node_id.to_string()),
node_name: Set(Some(node.name.clone())),
assignee_id: Set(node.assignee_id),
candidate_groups: Set(node.candidate_groups.as_ref()
.map(|g| serde_json::to_value(g).unwrap_or_default())),
status: Set("pending".to_string()),
outcome: Set(None),
form_data: Set(None),
due_date: Set(None),
completed_at: Set(None),
created_at: Set(now),
updated_at: Set(now),
created_by: Set(Uuid::nil()),
updated_by: Set(Uuid::nil()),
deleted_at: Set(None),
version: Set(1),
};
task_model
.insert(txn)
.await
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
}
Ok(vec![new_token_id])
}
}

View File

@@ -164,10 +164,10 @@ impl ExpressionEvaluator {
if let Ok(n) = token.parse::<i64>() {
return Ok(serde_json::Value::Number(n.into()));
}
if let Ok(f) = token.parse::<f64>() {
if let Some(n) = serde_json::Number::from_f64(f) {
return Ok(serde_json::Value::Number(n));
}
if let Ok(f) = token.parse::<f64>()
&& let Some(n) = serde_json::Number::from_f64(f)
{
return Ok(serde_json::Value::Number(n));
}
// 布尔字面量

View File

@@ -28,7 +28,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: defs,

View File

@@ -1,6 +1,7 @@
use axum::Extension;
use axum::extract::{FromRef, Path, Query, State};
use axum::response::Json;
use validator::Validate;
use erp_core::error::AppError;
use erp_core::rbac::require_permission;
@@ -22,6 +23,7 @@ where
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "workflow:start")?;
req.validate().map_err(|e| AppError::Validation(e.to_string()))?;
let resp = InstanceService::start(
ctx.tenant_id,
@@ -52,7 +54,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: instances,

View File

@@ -1,6 +1,7 @@
use axum::Extension;
use axum::extract::{FromRef, Path, Query, State};
use axum::response::Json;
use validator::Validate;
use erp_core::error::AppError;
use erp_core::rbac::require_permission;
@@ -28,7 +29,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: tasks,
@@ -56,7 +57,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: tasks,
@@ -79,6 +80,7 @@ where
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "workflow:approve")?;
req.validate().map_err(|e| AppError::Validation(e.to_string()))?;
let resp = TaskService::complete(
id,
@@ -105,6 +107,7 @@ where
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "workflow:delegate")?;
req.validate().map_err(|e| AppError::Validation(e.to_string()))?;
let resp =
TaskService::delegate(id, ctx.tenant_id, ctx.user_id, &req, &state.db).await?;

View File

@@ -33,7 +33,7 @@ impl DefinitionService {
.await
.map_err(|e| WorkflowError::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

View File

@@ -38,7 +38,7 @@ impl TaskService {
.await
.map_err(|e| WorkflowError::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
@@ -80,7 +80,7 @@ impl TaskService {
let paginator = task::Entity::find()
.filter(task::Column::TenantId.eq(tenant_id))
.filter(task::Column::AssigneeId.eq(assignee_id))
.filter(task::Column::Status.is_in(["approved", "rejected", "delegated"]))
.filter(task::Column::Status.is_in(["completed", "approved", "rejected", "delegated"]))
.filter(task::Column::DeletedAt.is_null())
.paginate(db, pagination.limit());
@@ -89,7 +89,7 @@ impl TaskService {
.await
.map_err(|e| WorkflowError::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
@@ -142,6 +142,13 @@ impl TaskService {
));
}
// 验证操作者是当前处理人
if task_model.assignee_id != Some(operator_id) {
return Err(WorkflowError::InvalidState(
"只有当前处理人才能完成任务".to_string(),
));
}
let instance_id = task_model.instance_id;
let token_id = task_model.token_id;
@@ -162,6 +169,13 @@ impl TaskService {
WorkflowError::NotFound(format!("流程定义不存在: {}", instance.definition_id))
})?;
if instance.status != "running" {
return Err(WorkflowError::InvalidState(format!(
"流程实例状态不是 running: {}",
instance.status
)));
}
let nodes: Vec<crate::dto::NodeDef> =
serde_json::from_value(definition.nodes.clone()).map_err(|e| {
WorkflowError::InvalidDiagram(format!("节点数据无效: {e}"))
@@ -174,11 +188,11 @@ impl TaskService {
// 准备变量(从 req.form_data 中提取)
let mut variables = HashMap::new();
if let Some(form) = &req.form_data {
if let Some(obj) = form.as_object() {
for (k, v) in obj {
variables.insert(k.clone(), v.clone());
}
if let Some(form) = &req.form_data
&& let Some(obj) = form.as_object()
{
for (k, v) in obj {
variables.insert(k.clone(), v.clone());
}
}
@@ -257,6 +271,13 @@ impl TaskService {
));
}
// 验证操作者是当前处理人
if task_model.assignee_id != Some(operator_id) {
return Err(WorkflowError::InvalidState(
"只有当前处理人才能委派任务".to_string(),
));
}
let mut active: task::ActiveModel = task_model.into();
active.assignee_id = Set(Some(req.delegate_to));
active.updated_at = Set(Utc::now());
@@ -271,6 +292,7 @@ impl TaskService {
}
/// 创建任务记录(由执行引擎调用)。
#[allow(clippy::too_many_arguments)]
pub async fn create_task(
instance_id: Uuid,
tenant_id: Uuid,