feat(core): entry 历史附带关联 secret 密文快照,rollback 可恢复 N:N 与密文

- db: metadata_with_secret_snapshot / strip / parse 辅助
- add/update/delete/rollback 在写 entries_history 前合并快照
- rollback: 按历史快照同步 entry_secrets、更新或插入 secrets
- 满足 clippy collapsible_if
This commit is contained in:
voson
2026-04-05 16:00:26 +08:00
parent 63cb3a8216
commit 7c53bfb782
6 changed files with 313 additions and 39 deletions

View File

@@ -1,7 +1,7 @@
use std::str::FromStr;
use anyhow::{Context, Result};
use serde_json::Value;
use serde_json::{Map, Value};
use sqlx::PgPool;
use sqlx::postgres::{PgConnectOptions, PgPoolOptions, PgSslMode};
@@ -562,4 +562,75 @@ pub async fn snapshot_secret_history(
Ok(())
}
pub const ENTRY_HISTORY_SECRETS_KEY: &str = "__secrets_snapshot_v1";
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct EntrySecretSnapshot {
pub name: String,
#[serde(rename = "type")]
pub secret_type: String,
pub encrypted_hex: String,
}
pub async fn metadata_with_secret_snapshot(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
entry_id: uuid::Uuid,
metadata: &Value,
) -> Result<Value> {
#[derive(sqlx::FromRow)]
struct Row {
name: String,
#[sqlx(rename = "type")]
secret_type: String,
encrypted: Vec<u8>,
}
let rows: Vec<Row> = sqlx::query_as(
"SELECT s.name, s.type, s.encrypted \
FROM entry_secrets es \
JOIN secrets s ON s.id = es.secret_id \
WHERE es.entry_id = $1 \
ORDER BY s.name ASC",
)
.bind(entry_id)
.fetch_all(&mut **tx)
.await?;
let snapshots: Vec<EntrySecretSnapshot> = rows
.into_iter()
.map(|r| EntrySecretSnapshot {
name: r.name,
secret_type: r.secret_type,
encrypted_hex: ::hex::encode(r.encrypted),
})
.collect();
let mut merged = match metadata.clone() {
Value::Object(obj) => obj,
_ => Map::new(),
};
merged.insert(
ENTRY_HISTORY_SECRETS_KEY.to_string(),
serde_json::to_value(snapshots)?,
);
Ok(Value::Object(merged))
}
pub fn strip_secret_snapshot_from_metadata(metadata: &Value) -> Value {
let mut m = match metadata.clone() {
Value::Object(obj) => obj,
_ => return metadata.clone(),
};
m.remove(ENTRY_HISTORY_SECRETS_KEY);
Value::Object(m)
}
pub fn entry_secret_snapshot_from_metadata(metadata: &Value) -> Option<Vec<EntrySecretSnapshot>> {
let Value::Object(map) = metadata else {
return None;
};
let raw = map.get(ENTRY_HISTORY_SECRETS_KEY)?;
serde_json::from_value(raw.clone()).ok()
}
// ── DB helpers ────────────────────────────────────────────────────────────────