release(secrets-mcp): 0.5.10 — Web 模块化、性能与错误处理
- 拆分 web.rs 为 web/ 子模块;统一 client_ip 提取 - core: user_scope SQL 复用、env_map N+1 消除、FETCH_ALL 上限调整 - entries 列表页并行查询;PgPool 去 Arc;结构化 NotFound 等错误 - CI: SSH 私钥安全写入;crypto/hex 与依赖清理;MCP 输入长度校验 - AGENTS: API Key 明文存储设计说明
This commit is contained in:
@@ -5,6 +5,7 @@ use uuid::Uuid;
|
||||
|
||||
use crate::db;
|
||||
use crate::models::{EntryRow, EntryWriteRow, SecretFieldRow};
|
||||
use crate::service::util::user_scope_condition;
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct DeletedEntry {
|
||||
@@ -126,48 +127,32 @@ async fn delete_one(
|
||||
// - 2+ matches → disambiguation error (same as non-dry-run)
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct DryRunRow {
|
||||
#[allow(dead_code)]
|
||||
id: Uuid,
|
||||
folder: String,
|
||||
#[sqlx(rename = "type")]
|
||||
entry_type: String,
|
||||
}
|
||||
|
||||
let rows: Vec<DryRunRow> = if let Some(uid) = user_id {
|
||||
if let Some(f) = folder {
|
||||
sqlx::query_as(
|
||||
"SELECT id, folder, type FROM entries WHERE user_id = $1 AND folder = $2 AND name = $3",
|
||||
)
|
||||
.bind(uid)
|
||||
.bind(f)
|
||||
.bind(name)
|
||||
.fetch_all(pool)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_as(
|
||||
"SELECT id, folder, type FROM entries WHERE user_id = $1 AND name = $2",
|
||||
)
|
||||
.bind(uid)
|
||||
.bind(name)
|
||||
.fetch_all(pool)
|
||||
.await?
|
||||
}
|
||||
} else if let Some(f) = folder {
|
||||
sqlx::query_as(
|
||||
"SELECT id, folder, type FROM entries WHERE user_id IS NULL AND folder = $1 AND name = $2",
|
||||
)
|
||||
.bind(f)
|
||||
.bind(name)
|
||||
.fetch_all(pool)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_as(
|
||||
"SELECT id, folder, type FROM entries WHERE user_id IS NULL AND name = $1",
|
||||
)
|
||||
.bind(name)
|
||||
.fetch_all(pool)
|
||||
.await?
|
||||
};
|
||||
let mut idx = 1i32;
|
||||
let user_cond = user_scope_condition(user_id, &mut idx);
|
||||
let mut conditions = vec![user_cond];
|
||||
if folder.is_some() {
|
||||
conditions.push(format!("folder = ${}", idx));
|
||||
idx += 1;
|
||||
}
|
||||
conditions.push(format!("name = ${}", idx));
|
||||
let sql = format!(
|
||||
"SELECT folder, type FROM entries WHERE {}",
|
||||
conditions.join(" AND ")
|
||||
);
|
||||
let mut q = sqlx::query_as::<_, DryRunRow>(&sql);
|
||||
if let Some(uid) = user_id {
|
||||
q = q.bind(uid);
|
||||
}
|
||||
if let Some(f) = folder {
|
||||
q = q.bind(f);
|
||||
}
|
||||
q = q.bind(name);
|
||||
let rows = q.fetch_all(pool).await?;
|
||||
|
||||
return match rows.len() {
|
||||
0 => Ok(DeleteResult {
|
||||
@@ -175,7 +160,10 @@ async fn delete_one(
|
||||
dry_run: true,
|
||||
}),
|
||||
1 => {
|
||||
let row = rows.into_iter().next().unwrap();
|
||||
let row = rows
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("internal: matched row vanished"))?;
|
||||
Ok(DeleteResult {
|
||||
deleted: vec![DeletedEntry {
|
||||
name: name.to_string(),
|
||||
@@ -201,45 +189,27 @@ async fn delete_one(
|
||||
let mut tx = pool.begin().await?;
|
||||
|
||||
// Fetch matching rows with FOR UPDATE; use folder when provided to resolve ambiguity.
|
||||
let rows: Vec<EntryRow> = if let Some(uid) = user_id {
|
||||
if let Some(f) = folder {
|
||||
sqlx::query_as(
|
||||
"SELECT id, version, folder, type, tags, metadata, notes FROM entries \
|
||||
WHERE user_id = $1 AND folder = $2 AND name = $3 FOR UPDATE",
|
||||
)
|
||||
.bind(uid)
|
||||
.bind(f)
|
||||
.bind(name)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_as(
|
||||
"SELECT id, version, folder, type, tags, metadata, notes FROM entries \
|
||||
WHERE user_id = $1 AND name = $2 FOR UPDATE",
|
||||
)
|
||||
.bind(uid)
|
||||
.bind(name)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?
|
||||
}
|
||||
} else if let Some(f) = folder {
|
||||
sqlx::query_as(
|
||||
"SELECT id, version, folder, type, tags, metadata, notes FROM entries \
|
||||
WHERE user_id IS NULL AND folder = $1 AND name = $2 FOR UPDATE",
|
||||
)
|
||||
.bind(f)
|
||||
.bind(name)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_as(
|
||||
"SELECT id, version, folder, type, tags, metadata, notes FROM entries \
|
||||
WHERE user_id IS NULL AND name = $1 FOR UPDATE",
|
||||
)
|
||||
.bind(name)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?
|
||||
};
|
||||
let mut idx = 1i32;
|
||||
let user_cond = user_scope_condition(user_id, &mut idx);
|
||||
let mut conditions = vec![user_cond];
|
||||
if folder.is_some() {
|
||||
conditions.push(format!("folder = ${}", idx));
|
||||
idx += 1;
|
||||
}
|
||||
conditions.push(format!("name = ${}", idx));
|
||||
let sql = format!(
|
||||
"SELECT id, version, folder, type, tags, metadata, notes FROM entries WHERE {} FOR UPDATE",
|
||||
conditions.join(" AND ")
|
||||
);
|
||||
let mut q = sqlx::query_as::<_, EntryRow>(&sql);
|
||||
if let Some(uid) = user_id {
|
||||
q = q.bind(uid);
|
||||
}
|
||||
if let Some(f) = folder {
|
||||
q = q.bind(f);
|
||||
}
|
||||
q = q.bind(name);
|
||||
let rows = q.fetch_all(&mut *tx).await?;
|
||||
|
||||
let row = match rows.len() {
|
||||
0 => {
|
||||
@@ -249,7 +219,10 @@ async fn delete_one(
|
||||
dry_run: false,
|
||||
});
|
||||
}
|
||||
1 => rows.into_iter().next().unwrap(),
|
||||
1 => rows
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("internal: matched row vanished"))?,
|
||||
_ => {
|
||||
tx.rollback().await?;
|
||||
let folders: Vec<&str> = rows.iter().map(|r| r.folder.as_str()).collect();
|
||||
|
||||
Reference in New Issue
Block a user