feat(auth): 添加异步密码哈希和验证函数
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
refactor(relay): 复用HTTP客户端和请求体序列化结果 feat(kernel): 添加获取单个审批记录的方法 fix(store): 改进SaaS连接错误分类和降级处理 docs: 更新审计文档和系统架构文档 refactor(prompt): 优化SQL查询参数化绑定 refactor(migration): 使用静态SQL和COALESCE更新配置项 feat(commands): 添加审批执行状态追踪和事件通知 chore: 更新启动脚本以支持Admin后台 fix(auth-guard): 优化授权状态管理和错误处理 refactor(db): 使用异步密码哈希函数 refactor(totp): 使用异步密码验证函数 style: 清理无用文件和注释 docs: 更新功能全景和审计文档 refactor(service): 优化HTTP客户端重用和请求处理 fix(connection): 改进SaaS不可用时的降级处理 refactor(handlers): 使用异步密码验证函数 chore: 更新依赖和工具链配置
This commit is contained in:
@@ -17,6 +17,9 @@ fn row_to_template(
|
||||
}
|
||||
}
|
||||
|
||||
/// Row type for agent_template queries (avoids multi-line turbofish parsing issues)
|
||||
type AgentTemplateRow = (String, String, Option<String>, String, String, Option<String>, Option<String>, String, String, Option<f64>, Option<i32>, String, String, i32, String, String);
|
||||
|
||||
/// 创建 Agent 模板
|
||||
pub async fn create_template(
|
||||
db: &PgPool,
|
||||
@@ -58,7 +61,7 @@ pub async fn create_template(
|
||||
|
||||
/// 获取单个模板
|
||||
pub async fn get_template(db: &PgPool, id: &str) -> SaasResult<AgentTemplateInfo> {
|
||||
let row: Option<_> = sqlx::query_as(
|
||||
let row: Option<AgentTemplateRow> = sqlx::query_as(
|
||||
"SELECT id, name, description, category, source, model, system_prompt,
|
||||
tools, capabilities, temperature, max_tokens, visibility, status,
|
||||
current_version, created_at, updated_at
|
||||
@@ -70,7 +73,8 @@ pub async fn get_template(db: &PgPool, id: &str) -> SaasResult<AgentTemplateInfo
|
||||
}
|
||||
|
||||
/// 列出模板(分页 + 过滤)
|
||||
/// 使用动态参数化查询,安全拼接 WHERE 条件。
|
||||
/// Static SQL + conditional filter pattern: ($N IS NULL OR col = $N).
|
||||
/// When the parameter is NULL the whole OR evaluates to TRUE (no filter).
|
||||
pub async fn list_templates(
|
||||
db: &PgPool,
|
||||
query: &AgentTemplateListQuery,
|
||||
@@ -79,80 +83,35 @@ pub async fn list_templates(
|
||||
let page_size = query.page_size.unwrap_or(20).min(100);
|
||||
let offset = ((page - 1) * page_size) as i64;
|
||||
|
||||
// 动态构建参数化 WHERE 子句
|
||||
let mut conditions: Vec<String> = vec!["1=1".to_string()];
|
||||
let mut param_idx = 1u32;
|
||||
let mut cat_bind: Option<String> = None;
|
||||
let mut src_bind: Option<String> = None;
|
||||
let mut vis_bind: Option<String> = None;
|
||||
let mut st_bind: Option<String> = None;
|
||||
|
||||
if let Some(ref cat) = query.category {
|
||||
param_idx += 1;
|
||||
conditions.push(format!("category = ${}", param_idx));
|
||||
cat_bind = Some(cat.clone());
|
||||
}
|
||||
if let Some(ref src) = query.source {
|
||||
param_idx += 1;
|
||||
conditions.push(format!("source = ${}", param_idx));
|
||||
src_bind = Some(src.clone());
|
||||
}
|
||||
if let Some(ref vis) = query.visibility {
|
||||
param_idx += 1;
|
||||
conditions.push(format!("visibility = ${}", param_idx));
|
||||
vis_bind = Some(vis.clone());
|
||||
}
|
||||
if let Some(ref st) = query.status {
|
||||
param_idx += 1;
|
||||
conditions.push(format!("status = ${}", param_idx));
|
||||
st_bind = Some(st.clone());
|
||||
}
|
||||
|
||||
let where_clause = conditions.join(" AND ");
|
||||
|
||||
// COUNT 查询: WHERE 参数绑定 ($1..$N)
|
||||
let count_idx = param_idx;
|
||||
let count_sql = format!(
|
||||
"SELECT COUNT(*) FROM agent_templates WHERE {}",
|
||||
where_clause
|
||||
);
|
||||
let count_limit_idx = count_idx + 1;
|
||||
let count_offset_idx = count_limit_idx + 1;
|
||||
let data_sql = format!(
|
||||
"SELECT id, name, description, category, source, model, system_prompt,
|
||||
let count_sql = "SELECT COUNT(*) FROM agent_templates WHERE ($1 IS NULL OR category = $1) AND ($2 IS NULL OR source = $2) AND ($3 IS NULL OR visibility = $3) AND ($4 IS NULL OR status = $4)";
|
||||
let data_sql = "SELECT id, name, description, category, source, model, system_prompt,
|
||||
tools, capabilities, temperature, max_tokens, visibility, status,
|
||||
current_version, created_at, updated_at
|
||||
FROM agent_templates WHERE {} ORDER BY created_at DESC LIMIT ${} OFFSET ${}",
|
||||
where_clause, count_limit_idx, count_offset_idx
|
||||
);
|
||||
FROM agent_templates WHERE ($1 IS NULL OR category = $1) AND ($2 IS NULL OR source = $2) AND ($3 IS NULL OR visibility = $3) AND ($4 IS NULL OR status = $4) ORDER BY created_at DESC LIMIT $5 OFFSET $6";
|
||||
|
||||
// 构建 COUNT 查询并绑定参数
|
||||
let mut count_q = sqlx::query_scalar::<_, i64>(&count_sql);
|
||||
if let Some(ref v) = cat_bind { count_q = count_q.bind(v); }
|
||||
if let Some(ref v) = src_bind { count_q = count_q.bind(v); }
|
||||
if let Some(ref v) = vis_bind { count_q = count_q.bind(v); }
|
||||
if let Some(ref v) = st_bind { count_q = count_q.bind(v); }
|
||||
let total: i64 = count_q.fetch_one(db).await?;
|
||||
let total: i64 = sqlx::query_scalar(count_sql)
|
||||
.bind(&query.category)
|
||||
.bind(&query.source)
|
||||
.bind(&query.visibility)
|
||||
.bind(&query.status)
|
||||
.fetch_one(db).await?;
|
||||
|
||||
// 构建数据查询并绑定参数
|
||||
let mut data_q = sqlx::query_as::<_, (
|
||||
String, String, Option<String>, String, String, Option<String>, Option<String>,
|
||||
String, String, Option<f64>, Option<i32>, String, String, i32, String, String
|
||||
)>(&data_sql);
|
||||
if let Some(ref v) = cat_bind { data_q = data_q.bind(v); }
|
||||
if let Some(ref v) = src_bind { data_q = data_q.bind(v); }
|
||||
if let Some(ref v) = vis_bind { data_q = data_q.bind(v); }
|
||||
if let Some(ref v) = st_bind { data_q = data_q.bind(v); }
|
||||
data_q = data_q.bind(page_size as i64).bind(offset);
|
||||
|
||||
let rows = data_q.fetch_all(db).await?;
|
||||
let rows: Vec<AgentTemplateRow> = sqlx::query_as(data_sql)
|
||||
.bind(&query.category)
|
||||
.bind(&query.source)
|
||||
.bind(&query.visibility)
|
||||
.bind(&query.status)
|
||||
.bind(page_size as i64)
|
||||
.bind(offset)
|
||||
.fetch_all(db).await?;
|
||||
let items = rows.into_iter().map(row_to_template).collect();
|
||||
|
||||
Ok(crate::common::PaginatedResponse { items, total, page, page_size })
|
||||
}
|
||||
|
||||
/// 更新模板
|
||||
/// 使用动态参数化查询,安全拼接 SET 子句。
|
||||
/// COALESCE pattern: all updatable fields in a single static SQL.
|
||||
/// NULL parameters leave the column unchanged.
|
||||
pub async fn update_template(
|
||||
db: &PgPool,
|
||||
id: &str,
|
||||
@@ -166,102 +125,41 @@ pub async fn update_template(
|
||||
visibility: Option<&str>,
|
||||
status: Option<&str>,
|
||||
) -> SaasResult<AgentTemplateInfo> {
|
||||
// 确认存在
|
||||
// Confirm existence
|
||||
get_template(db, id).await?;
|
||||
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let mut set_clauses: Vec<String> = vec![];
|
||||
let mut param_idx = 1u32;
|
||||
|
||||
// 收集需要绑定的值(按顺序)
|
||||
let mut desc_val: Option<String> = None;
|
||||
let mut model_val: Option<String> = None;
|
||||
let mut sp_val: Option<String> = None;
|
||||
let mut tools_val: Option<String> = None;
|
||||
let mut caps_val: Option<String> = None;
|
||||
let mut temp_val: Option<f64> = None;
|
||||
let mut mt_val: Option<i32> = None;
|
||||
let mut vis_val: Option<String> = None;
|
||||
let mut st_val: Option<String> = None;
|
||||
// Serialize JSON fields upfront so we can bind Option<&str> consistently
|
||||
let tools_json = tools.map(|t| serde_json::to_string(t).unwrap_or_else(|_| "[]".to_string()));
|
||||
let caps_json = capabilities.map(|c| serde_json::to_string(c).unwrap_or_else(|_| "[]".to_string()));
|
||||
|
||||
if let Some(desc) = description {
|
||||
param_idx += 1;
|
||||
set_clauses.push(format!("description = ${}", param_idx));
|
||||
desc_val = Some(desc.to_string());
|
||||
}
|
||||
if let Some(m) = model {
|
||||
param_idx += 1;
|
||||
set_clauses.push(format!("model = ${}", param_idx));
|
||||
model_val = Some(m.to_string());
|
||||
}
|
||||
if let Some(sp) = system_prompt {
|
||||
param_idx += 1;
|
||||
set_clauses.push(format!("system_prompt = ${}", param_idx));
|
||||
sp_val = Some(sp.to_string());
|
||||
}
|
||||
if let Some(t) = tools {
|
||||
let json = serde_json::to_string(t).unwrap_or_else(|_| "[]".to_string());
|
||||
param_idx += 1;
|
||||
set_clauses.push(format!("tools = ${}", param_idx));
|
||||
tools_val = Some(json);
|
||||
}
|
||||
if let Some(c) = capabilities {
|
||||
let json = serde_json::to_string(c).unwrap_or_else(|_| "[]".to_string());
|
||||
param_idx += 1;
|
||||
set_clauses.push(format!("capabilities = ${}", param_idx));
|
||||
caps_val = Some(json);
|
||||
}
|
||||
if let Some(t) = temperature {
|
||||
param_idx += 1;
|
||||
set_clauses.push(format!("temperature = ${}", param_idx));
|
||||
temp_val = Some(t);
|
||||
}
|
||||
if let Some(m) = max_tokens {
|
||||
param_idx += 1;
|
||||
set_clauses.push(format!("max_tokens = ${}", param_idx));
|
||||
mt_val = Some(m);
|
||||
}
|
||||
if let Some(v) = visibility {
|
||||
param_idx += 1;
|
||||
set_clauses.push(format!("visibility = ${}", param_idx));
|
||||
vis_val = Some(v.to_string());
|
||||
}
|
||||
if let Some(s) = status {
|
||||
param_idx += 1;
|
||||
set_clauses.push(format!("status = ${}", param_idx));
|
||||
st_val = Some(s.to_string());
|
||||
}
|
||||
|
||||
if set_clauses.is_empty() {
|
||||
return get_template(db, id).await;
|
||||
}
|
||||
|
||||
// updated_at
|
||||
param_idx += 1;
|
||||
set_clauses.push(format!("updated_at = ${}", param_idx));
|
||||
|
||||
// WHERE id = $N
|
||||
let id_idx = param_idx + 1;
|
||||
|
||||
let sql = format!(
|
||||
"UPDATE agent_templates SET {} WHERE id = ${}",
|
||||
set_clauses.join(", "), id_idx
|
||||
);
|
||||
|
||||
let mut q = sqlx::query(&sql);
|
||||
if let Some(ref v) = desc_val { q = q.bind(v); }
|
||||
if let Some(ref v) = model_val { q = q.bind(v); }
|
||||
if let Some(ref v) = sp_val { q = q.bind(v); }
|
||||
if let Some(ref v) = tools_val { q = q.bind(v); }
|
||||
if let Some(ref v) = caps_val { q = q.bind(v); }
|
||||
if let Some(v) = temp_val { q = q.bind(v); }
|
||||
if let Some(v) = mt_val { q = q.bind(v); }
|
||||
if let Some(ref v) = vis_val { q = q.bind(v); }
|
||||
if let Some(ref v) = st_val { q = q.bind(v); }
|
||||
q = q.bind(&now);
|
||||
q = q.bind(id);
|
||||
|
||||
q.execute(db).await?;
|
||||
sqlx::query(
|
||||
"UPDATE agent_templates SET
|
||||
description = COALESCE($1, description),
|
||||
model = COALESCE($2, model),
|
||||
system_prompt = COALESCE($3, system_prompt),
|
||||
tools = COALESCE($4, tools),
|
||||
capabilities = COALESCE($5, capabilities),
|
||||
temperature = COALESCE($6, temperature),
|
||||
max_tokens = COALESCE($7, max_tokens),
|
||||
visibility = COALESCE($8, visibility),
|
||||
status = COALESCE($9, status),
|
||||
updated_at = $10
|
||||
WHERE id = $11"
|
||||
)
|
||||
.bind(description)
|
||||
.bind(model)
|
||||
.bind(system_prompt)
|
||||
.bind(tools_json.as_deref())
|
||||
.bind(caps_json.as_deref())
|
||||
.bind(temperature)
|
||||
.bind(max_tokens)
|
||||
.bind(visibility)
|
||||
.bind(status)
|
||||
.bind(&now)
|
||||
.bind(id)
|
||||
.execute(db).await?;
|
||||
|
||||
get_template(db, id).await
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user