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:
@@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
// 布尔字面量
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user