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:
@@ -14,55 +14,109 @@ pub async fn list_accounts(
|
||||
let page_size = query.page_size.unwrap_or(20).min(100);
|
||||
let offset = (page - 1) * page_size;
|
||||
|
||||
let mut where_clauses = Vec::new();
|
||||
let mut params: Vec<String> = Vec::new();
|
||||
let mut param_idx = 1usize;
|
||||
|
||||
if let Some(role) = &query.role {
|
||||
where_clauses.push(format!("role = ${}", param_idx));
|
||||
param_idx += 1;
|
||||
params.push(role.clone());
|
||||
}
|
||||
if let Some(status) = &query.status {
|
||||
where_clauses.push(format!("status = ${}", param_idx));
|
||||
param_idx += 1;
|
||||
params.push(status.clone());
|
||||
}
|
||||
if let Some(search) = &query.search {
|
||||
where_clauses.push(format!("(username LIKE ${} OR email LIKE ${} OR display_name LIKE ${})", param_idx, param_idx + 1, param_idx + 2));
|
||||
param_idx += 3;
|
||||
let pattern = format!("%{}%", search);
|
||||
params.push(pattern.clone());
|
||||
params.push(pattern.clone());
|
||||
params.push(pattern);
|
||||
}
|
||||
|
||||
let where_sql = if where_clauses.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("WHERE {}", where_clauses.join(" AND "))
|
||||
// Static SQL per combination -- no format!() string interpolation
|
||||
let (total, rows) = match (&query.role, &query.status, &query.search) {
|
||||
// role + status + search
|
||||
(Some(role), Some(status), Some(search)) => {
|
||||
let pattern = format!("%{}%", search);
|
||||
let total: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM accounts WHERE role = $1 AND status = $2 AND (username LIKE $3 OR email LIKE $3 OR display_name LIKE $3)"
|
||||
).bind(role).bind(status).bind(&pattern).fetch_one(db).await?;
|
||||
let rows = sqlx::query_as::<_, AccountRow>(
|
||||
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at
|
||||
FROM accounts WHERE role = $1 AND status = $2 AND (username LIKE $3 OR email LIKE $3 OR display_name LIKE $3)
|
||||
ORDER BY created_at DESC LIMIT $4 OFFSET $5"
|
||||
).bind(role).bind(status).bind(&pattern).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
||||
(total, rows)
|
||||
}
|
||||
// role + status
|
||||
(Some(role), Some(status), None) => {
|
||||
let total: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM accounts WHERE role = $1 AND status = $2"
|
||||
).bind(role).bind(status).fetch_one(db).await?;
|
||||
let rows = sqlx::query_as::<_, AccountRow>(
|
||||
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at
|
||||
FROM accounts WHERE role = $1 AND status = $2
|
||||
ORDER BY created_at DESC LIMIT $3 OFFSET $4"
|
||||
).bind(role).bind(status).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
||||
(total, rows)
|
||||
}
|
||||
// role + search
|
||||
(Some(role), None, Some(search)) => {
|
||||
let pattern = format!("%{}%", search);
|
||||
let total: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM accounts WHERE role = $1 AND (username LIKE $2 OR email LIKE $2 OR display_name LIKE $2)"
|
||||
).bind(role).bind(&pattern).fetch_one(db).await?;
|
||||
let rows = sqlx::query_as::<_, AccountRow>(
|
||||
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at
|
||||
FROM accounts WHERE role = $1 AND (username LIKE $2 OR email LIKE $2 OR display_name LIKE $2)
|
||||
ORDER BY created_at DESC LIMIT $3 OFFSET $4"
|
||||
).bind(role).bind(&pattern).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
||||
(total, rows)
|
||||
}
|
||||
// status + search
|
||||
(None, Some(status), Some(search)) => {
|
||||
let pattern = format!("%{}%", search);
|
||||
let total: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM accounts WHERE status = $1 AND (username LIKE $2 OR email LIKE $2 OR display_name LIKE $2)"
|
||||
).bind(status).bind(&pattern).fetch_one(db).await?;
|
||||
let rows = sqlx::query_as::<_, AccountRow>(
|
||||
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at
|
||||
FROM accounts WHERE status = $1 AND (username LIKE $2 OR email LIKE $2 OR display_name LIKE $2)
|
||||
ORDER BY created_at DESC LIMIT $3 OFFSET $4"
|
||||
).bind(status).bind(&pattern).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
||||
(total, rows)
|
||||
}
|
||||
// role only
|
||||
(Some(role), None, None) => {
|
||||
let total: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM accounts WHERE role = $1"
|
||||
).bind(role).fetch_one(db).await?;
|
||||
let rows = sqlx::query_as::<_, AccountRow>(
|
||||
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at
|
||||
FROM accounts WHERE role = $1
|
||||
ORDER BY created_at DESC LIMIT $2 OFFSET $3"
|
||||
).bind(role).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
||||
(total, rows)
|
||||
}
|
||||
// status only
|
||||
(None, Some(status), None) => {
|
||||
let total: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM accounts WHERE status = $1"
|
||||
).bind(status).fetch_one(db).await?;
|
||||
let rows = sqlx::query_as::<_, AccountRow>(
|
||||
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at
|
||||
FROM accounts WHERE status = $1
|
||||
ORDER BY created_at DESC LIMIT $2 OFFSET $3"
|
||||
).bind(status).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
||||
(total, rows)
|
||||
}
|
||||
// search only
|
||||
(None, None, Some(search)) => {
|
||||
let pattern = format!("%{}%", search);
|
||||
let total: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM accounts WHERE (username LIKE $1 OR email LIKE $1 OR display_name LIKE $1)"
|
||||
).bind(&pattern).fetch_one(db).await?;
|
||||
let rows = sqlx::query_as::<_, AccountRow>(
|
||||
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at
|
||||
FROM accounts WHERE (username LIKE $1 OR email LIKE $1 OR display_name LIKE $1)
|
||||
ORDER BY created_at DESC LIMIT $2 OFFSET $3"
|
||||
).bind(&pattern).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
||||
(total, rows)
|
||||
}
|
||||
// no filter
|
||||
(None, None, None) => {
|
||||
let total: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM accounts"
|
||||
).fetch_one(db).await?;
|
||||
let rows = sqlx::query_as::<_, AccountRow>(
|
||||
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at
|
||||
FROM accounts ORDER BY created_at DESC LIMIT $1 OFFSET $2"
|
||||
).bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
||||
(total, rows)
|
||||
}
|
||||
};
|
||||
|
||||
let count_sql = format!("SELECT COUNT(*) as count FROM accounts {}", where_sql);
|
||||
let mut count_query = sqlx::query_scalar::<_, i64>(&count_sql);
|
||||
for p in ¶ms {
|
||||
count_query = count_query.bind(p);
|
||||
}
|
||||
let total: i64 = count_query.fetch_one(db).await?;
|
||||
|
||||
let limit_idx = param_idx;
|
||||
let offset_idx = param_idx + 1;
|
||||
let data_sql = format!(
|
||||
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at
|
||||
FROM accounts {} ORDER BY created_at DESC LIMIT ${} OFFSET ${}",
|
||||
where_sql, limit_idx, offset_idx
|
||||
);
|
||||
let mut data_query = sqlx::query_as::<_, AccountRow>(&data_sql);
|
||||
for p in ¶ms {
|
||||
data_query = data_query.bind(p);
|
||||
}
|
||||
let rows = data_query.bind(page_size as i64).bind(offset as i64).fetch_all(db).await?;
|
||||
|
||||
let items: Vec<serde_json::Value> = rows
|
||||
.into_iter()
|
||||
.map(|r| {
|
||||
@@ -102,30 +156,26 @@ pub async fn update_account(
|
||||
req: &UpdateAccountRequest,
|
||||
) -> SaasResult<serde_json::Value> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let mut updates = Vec::new();
|
||||
let mut params: Vec<String> = Vec::new();
|
||||
let mut param_idx = 1usize;
|
||||
|
||||
if let Some(ref v) = req.display_name { updates.push(format!("display_name = ${}", param_idx)); param_idx += 1; params.push(v.clone()); }
|
||||
if let Some(ref v) = req.email { updates.push(format!("email = ${}", param_idx)); param_idx += 1; params.push(v.clone()); }
|
||||
if let Some(ref v) = req.role { updates.push(format!("role = ${}", param_idx)); param_idx += 1; params.push(v.clone()); }
|
||||
if let Some(ref v) = req.avatar_url { updates.push(format!("avatar_url = ${}", param_idx)); param_idx += 1; params.push(v.clone()); }
|
||||
// COALESCE pattern: all updatable fields in a single static SQL.
|
||||
// NULL parameters leave the column unchanged.
|
||||
sqlx::query(
|
||||
"UPDATE accounts SET
|
||||
display_name = COALESCE($1, display_name),
|
||||
email = COALESCE($2, email),
|
||||
role = COALESCE($3, role),
|
||||
avatar_url = COALESCE($4, avatar_url),
|
||||
updated_at = $5
|
||||
WHERE id = $6"
|
||||
)
|
||||
.bind(req.display_name.as_deref())
|
||||
.bind(req.email.as_deref())
|
||||
.bind(req.role.as_deref())
|
||||
.bind(req.avatar_url.as_deref())
|
||||
.bind(&now)
|
||||
.bind(account_id)
|
||||
.execute(db).await?;
|
||||
|
||||
if updates.is_empty() {
|
||||
return get_account(db, account_id).await;
|
||||
}
|
||||
|
||||
updates.push(format!("updated_at = ${}", param_idx));
|
||||
param_idx += 1;
|
||||
params.push(now.clone());
|
||||
params.push(account_id.to_string());
|
||||
|
||||
let sql = format!("UPDATE accounts SET {} WHERE id = ${}", updates.join(", "), param_idx);
|
||||
let mut query = sqlx::query(&sql);
|
||||
for p in ¶ms {
|
||||
query = query.bind(p);
|
||||
}
|
||||
query.execute(db).await?;
|
||||
get_account(db, account_id).await
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user