Skip to main content

tandem_server/
audit.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use tandem_types::TenantContext;
4use tokio::fs::{self, OpenOptions};
5use tokio::io::AsyncWriteExt;
6use uuid::Uuid;
7
8use crate::{now_ms, AppState};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum AuditDurability {
13    BestEffort,
14    DurableRequired,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ProtectedAuditEnvelope {
19    pub event_id: String,
20    pub durability: AuditDurability,
21    pub event_type: String,
22    #[serde(default)]
23    pub tenant_context: TenantContext,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub actor: Option<String>,
26    pub payload: Value,
27    pub created_at_ms: u64,
28}
29
30pub async fn append_protected_audit_event(
31    state: &AppState,
32    event_type: impl Into<String>,
33    tenant_context: &TenantContext,
34    actor: Option<String>,
35    payload: Value,
36) -> anyhow::Result<()> {
37    let path = state.protected_audit_path.clone();
38    if let Some(parent) = path.parent() {
39        fs::create_dir_all(parent).await?;
40    }
41    let row = ProtectedAuditEnvelope {
42        event_id: Uuid::new_v4().to_string(),
43        durability: AuditDurability::DurableRequired,
44        event_type: event_type.into(),
45        tenant_context: tenant_context.clone(),
46        actor,
47        payload,
48        created_at_ms: now_ms(),
49    };
50    let mut file = OpenOptions::new()
51        .create(true)
52        .append(true)
53        .open(&path)
54        .await?;
55    file.write_all(serde_json::to_string(&row)?.as_bytes())
56        .await?;
57    file.write_all(b"\n").await?;
58    file.flush().await?;
59    Ok(())
60}