release(secrets-mcp): 0.5.18 — Web 条目密文值编辑,PATCH /api/secrets/:id 支持 value
This commit is contained in:
@@ -138,6 +138,25 @@ pub(super) struct EntryOptionQuery {
|
||||
|
||||
type EntryApiError = (StatusCode, Json<serde_json::Value>);
|
||||
|
||||
fn require_encryption_key(headers: &HeaderMap, lang: UiLang) -> Result<[u8; 32], EntryApiError> {
|
||||
let enc_key_hex = headers
|
||||
.get("x-encryption-key")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.ok_or_else(|| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "error": tr(lang, "缺少 X-Encryption-Key 请求头", "缺少 X-Encryption-Key 請求標頭", "Missing X-Encryption-Key header") })),
|
||||
)
|
||||
})?;
|
||||
|
||||
secrets_core::crypto::extract_key_from_hex(enc_key_hex).map_err(|_| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "error": tr(lang, "X-Encryption-Key 格式无效", "X-Encryption-Key 格式無效", "Invalid X-Encryption-Key format") })),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn map_entry_mutation_err(e: anyhow::Error, lang: UiLang) -> EntryApiError {
|
||||
if let Some(app_err) = e.downcast_ref::<AppError>() {
|
||||
return map_app_error(app_err, lang);
|
||||
@@ -876,6 +895,7 @@ pub(super) struct SecretPatchBody {
|
||||
name: Option<String>,
|
||||
#[serde(rename = "type")]
|
||||
secret_type: Option<String>,
|
||||
value: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
pub(super) async fn api_secret_patch(
|
||||
@@ -901,6 +921,7 @@ pub(super) async fn api_secret_patch(
|
||||
|
||||
let name = body.name.as_ref().map(|s| s.trim());
|
||||
let secret_type = body.secret_type.as_ref().map(|s| s.trim());
|
||||
let secret_value = body.value.as_ref();
|
||||
|
||||
if let Some(n) = name {
|
||||
if n.is_empty() {
|
||||
@@ -940,30 +961,37 @@ pub(super) async fn api_secret_patch(
|
||||
}
|
||||
}
|
||||
|
||||
if name.is_none() && secret_type.is_none() {
|
||||
if name.is_none() && secret_type.is_none() && secret_value.is_none() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(
|
||||
json!({ "error": tr(lang, "至少需要提供 name 或 type 之一", "至少需要提供 name 或 type 之一", "At least one of name or type is required") }),
|
||||
json!({ "error": tr(lang, "至少需要提供 name、type 或 value 之一", "至少需要提供 name、type 或 value 之一", "At least one of name, type, or value is required") }),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let master_key = if secret_value.is_some() {
|
||||
Some(require_encryption_key(&headers, lang)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut tx = state
|
||||
.pool
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e| map_entry_mutation_err(e.into(), lang))?;
|
||||
|
||||
let secret_row: Option<(String, String)> =
|
||||
sqlx::query_as("SELECT name, type FROM secrets WHERE id = $1 AND user_id = $2 FOR UPDATE")
|
||||
.bind(secret_id)
|
||||
.bind(user_id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| map_entry_mutation_err(e.into(), lang))?;
|
||||
let secret_row: Option<(String, String, Vec<u8>)> = sqlx::query_as(
|
||||
"SELECT name, type, encrypted FROM secrets WHERE id = $1 AND user_id = $2 FOR UPDATE",
|
||||
)
|
||||
.bind(secret_id)
|
||||
.bind(user_id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| map_entry_mutation_err(e.into(), lang))?;
|
||||
|
||||
let Some((old_name, old_type)) = secret_row else {
|
||||
let Some((old_name, old_type, old_encrypted)) = secret_row else {
|
||||
let _ = tx.rollback().await;
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
@@ -988,13 +1016,47 @@ pub(super) async fn api_secret_patch(
|
||||
|
||||
let new_name = name.unwrap_or(&old_name).to_string();
|
||||
let new_type = secret_type.unwrap_or(&old_type).to_string();
|
||||
let new_encrypted = if let Some(value) = secret_value {
|
||||
let encrypted = secrets_core::crypto::encrypt_json(
|
||||
master_key
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "error": tr(lang, "请先设置密码短语后再编辑密文值", "請先設定密碼短語後再編輯密文值", "Unlock your passphrase before editing secret values") })),
|
||||
)
|
||||
})?,
|
||||
value,
|
||||
)
|
||||
.map_err(|e| map_entry_mutation_err(e, lang))?;
|
||||
Some(encrypted)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let value_changed = new_encrypted.is_some();
|
||||
|
||||
if let Err(e) = secrets_core::db::snapshot_secret_history(
|
||||
&mut tx,
|
||||
secrets_core::db::SecretSnapshotParams {
|
||||
secret_id,
|
||||
name: &old_name,
|
||||
encrypted: &old_encrypted,
|
||||
action: if value_changed { "update" } else { "rename" },
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(error = %e, %secret_id, "failed to snapshot secret history before patch");
|
||||
}
|
||||
|
||||
let result = sqlx::query(
|
||||
"UPDATE secrets SET name = $1, type = $2, version = version + 1, updated_at = NOW() \
|
||||
WHERE id = $3",
|
||||
"UPDATE secrets SET name = $1, type = $2, encrypted = $3, version = version + 1, updated_at = NOW() \
|
||||
WHERE id = $4",
|
||||
)
|
||||
.bind(&new_name)
|
||||
.bind(&new_type)
|
||||
.bind(new_encrypted.as_deref().unwrap_or(&old_encrypted))
|
||||
.bind(secret_id)
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
@@ -1018,7 +1080,11 @@ pub(super) async fn api_secret_patch(
|
||||
secrets_core::audit::log_tx(
|
||||
&mut tx,
|
||||
Some(user_id),
|
||||
"rename_secret",
|
||||
if value_changed {
|
||||
"update_secret"
|
||||
} else {
|
||||
"rename_secret"
|
||||
},
|
||||
"",
|
||||
"",
|
||||
&old_name,
|
||||
@@ -1029,6 +1095,7 @@ pub(super) async fn api_secret_patch(
|
||||
"new_name": new_name,
|
||||
"old_type": old_type,
|
||||
"new_type": new_type,
|
||||
"value_updated": value_changed,
|
||||
"linked_entries": linked_entries,
|
||||
}),
|
||||
)
|
||||
@@ -1154,23 +1221,7 @@ pub(super) async fn api_entry_secrets_decrypt(
|
||||
Json(json!({ "error": tr(lang, "未登录", "尚未登入", "Not logged in") })),
|
||||
))?;
|
||||
|
||||
let enc_key_hex = headers
|
||||
.get("x-encryption-key")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.ok_or_else(|| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "error": tr(lang, "缺少 X-Encryption-Key 请求头", "缺少 X-Encryption-Key 請求標頭", "Missing X-Encryption-Key header") })),
|
||||
)
|
||||
})?;
|
||||
|
||||
let master_key =
|
||||
secrets_core::crypto::extract_key_from_hex(enc_key_hex).map_err(|_| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "error": tr(lang, "X-Encryption-Key 格式无效", "X-Encryption-Key 格式無效", "Invalid X-Encryption-Key format") })),
|
||||
)
|
||||
})?;
|
||||
let master_key = require_encryption_key(&headers, lang)?;
|
||||
|
||||
let secrets =
|
||||
get_all_secrets_by_id(&state.pool, entry_id, &master_key, Some(user_id))
|
||||
|
||||
Reference in New Issue
Block a user