Some checks failed
Secrets v3 CI / 检查 (push) Has been cancelled
- Add apps/api, desktop Tauri shell, domain/application/crypto/device-auth/infrastructure-db - Replace desktop-daemon vault integration; drop secrets-core and secrets-mcp* - Ignore apps/desktop/dist and generated Tauri icons; document icon/dist steps in AGENTS.md - Apply rustfmt; fix clippy (collapsible_if, HTTP method as str)
148 lines
3.4 KiB
Rust
148 lines
3.4 KiB
Rust
use anyhow::{Context, Result};
|
|
use chrono::{DateTime, Utc};
|
|
use sqlx::PgPool;
|
|
use uuid::Uuid;
|
|
|
|
use secrets_domain::{VaultObjectEnvelope, VaultObjectKind, VaultTombstone};
|
|
|
|
#[derive(Debug, sqlx::FromRow)]
|
|
struct VaultObjectRow {
|
|
object_id: Uuid,
|
|
_object_kind: String,
|
|
revision: i64,
|
|
cipher_version: i32,
|
|
ciphertext: Vec<u8>,
|
|
content_hash: String,
|
|
deleted_at: Option<DateTime<Utc>>,
|
|
updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
impl From<VaultObjectRow> for VaultObjectEnvelope {
|
|
fn from(row: VaultObjectRow) -> Self {
|
|
Self {
|
|
object_id: row.object_id,
|
|
object_kind: VaultObjectKind::Cipher,
|
|
revision: row.revision,
|
|
cipher_version: row.cipher_version,
|
|
ciphertext: row.ciphertext,
|
|
content_hash: row.content_hash,
|
|
deleted_at: row.deleted_at,
|
|
updated_at: row.updated_at,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn list_objects_since(
|
|
pool: &PgPool,
|
|
user_id: Uuid,
|
|
cursor: i64,
|
|
limit: i64,
|
|
) -> Result<Vec<VaultObjectEnvelope>> {
|
|
let rows = sqlx::query_as::<_, VaultObjectRow>(
|
|
r#"
|
|
SELECT
|
|
object_id,
|
|
object_kind AS _object_kind,
|
|
revision,
|
|
cipher_version,
|
|
ciphertext,
|
|
content_hash,
|
|
deleted_at,
|
|
updated_at
|
|
FROM vault_objects
|
|
WHERE user_id = $1
|
|
AND revision > $2
|
|
ORDER BY revision ASC
|
|
LIMIT $3
|
|
"#,
|
|
)
|
|
.bind(user_id)
|
|
.bind(cursor)
|
|
.bind(limit.max(1))
|
|
.fetch_all(pool)
|
|
.await
|
|
.context("failed to list vault objects")?;
|
|
|
|
Ok(rows.into_iter().map(Into::into).collect())
|
|
}
|
|
|
|
pub async fn get_object(
|
|
pool: &PgPool,
|
|
user_id: Uuid,
|
|
object_id: Uuid,
|
|
) -> Result<Option<VaultObjectEnvelope>> {
|
|
let row = sqlx::query_as::<_, VaultObjectRow>(
|
|
r#"
|
|
SELECT
|
|
object_id,
|
|
object_kind AS _object_kind,
|
|
revision,
|
|
cipher_version,
|
|
ciphertext,
|
|
content_hash,
|
|
deleted_at,
|
|
updated_at
|
|
FROM vault_objects
|
|
WHERE user_id = $1
|
|
AND object_id = $2
|
|
"#,
|
|
)
|
|
.bind(user_id)
|
|
.bind(object_id)
|
|
.fetch_optional(pool)
|
|
.await
|
|
.context("failed to load vault object")?;
|
|
|
|
Ok(row.map(Into::into))
|
|
}
|
|
|
|
pub async fn list_tombstones_since(
|
|
pool: &PgPool,
|
|
user_id: Uuid,
|
|
cursor: i64,
|
|
limit: i64,
|
|
) -> Result<Vec<VaultTombstone>> {
|
|
let rows = sqlx::query_as::<_, (Uuid, i64, DateTime<Utc>)>(
|
|
r#"
|
|
SELECT object_id, revision, deleted_at
|
|
FROM vault_objects
|
|
WHERE user_id = $1
|
|
AND revision > $2
|
|
AND deleted_at IS NOT NULL
|
|
ORDER BY revision ASC
|
|
LIMIT $3
|
|
"#,
|
|
)
|
|
.bind(user_id)
|
|
.bind(cursor)
|
|
.bind(limit.max(1))
|
|
.fetch_all(pool)
|
|
.await
|
|
.context("failed to list tombstones")?;
|
|
|
|
Ok(rows
|
|
.into_iter()
|
|
.map(|(object_id, revision, deleted_at)| VaultTombstone {
|
|
object_id,
|
|
revision,
|
|
deleted_at,
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
pub async fn max_server_revision(pool: &PgPool, user_id: Uuid) -> Result<i64> {
|
|
let revision = sqlx::query_scalar::<_, Option<i64>>(
|
|
r#"
|
|
SELECT MAX(revision)
|
|
FROM vault_objects
|
|
WHERE user_id = $1
|
|
"#,
|
|
)
|
|
.bind(user_id)
|
|
.fetch_one(pool)
|
|
.await
|
|
.context("failed to load max server revision")?;
|
|
|
|
Ok(revision.unwrap_or(0))
|
|
}
|