Skip to main content

modo/audit/
record.rs

1use serde::Serialize;
2
3use crate::db::{ColumnMap, FromRow};
4use crate::error::Result;
5
6/// Stored audit event returned by [`AuditRepo`](super::AuditRepo) queries.
7///
8/// All fields are flat — [`ClientInfo`](crate::client::ClientInfo) is
9/// expanded into `ip`, `user_agent`, `device_name`, `device_type`,
10/// `fingerprint` columns.
11#[derive(Debug, Clone, Serialize)]
12pub struct AuditRecord {
13    /// Unique identifier (ULID).
14    pub id: String,
15    /// Who performed the action (user ID, API key, `"system"`, etc.).
16    pub actor: String,
17    /// Dot-delimited action identifier (e.g. `"doc.deleted"`).
18    pub action: String,
19    /// Kind of resource affected (e.g. `"document"`, `"user"`).
20    pub resource_type: String,
21    /// Identifier of the affected resource.
22    pub resource_id: String,
23    /// Arbitrary JSON payload attached at record time; defaults to `{}`.
24    pub metadata: serde_json::Value,
25    /// Client IP address, if provided.
26    pub ip: Option<String>,
27    /// Client user-agent string, if provided.
28    pub user_agent: Option<String>,
29    /// Parsed human-readable device name (e.g. `"Chrome on macOS"`), if provided.
30    pub device_name: Option<String>,
31    /// Parsed device type (`"desktop"`/`"mobile"`/`"tablet"`), if provided.
32    pub device_type: Option<String>,
33    /// Client fingerprint, if provided.
34    pub fingerprint: Option<String>,
35    /// Tenant identifier for multi-tenant apps.
36    pub tenant_id: Option<String>,
37    /// ISO 8601 timestamp of when the event was recorded.
38    pub created_at: String,
39}
40
41impl FromRow for AuditRecord {
42    fn from_row(row: &libsql::Row) -> Result<Self> {
43        let cols = ColumnMap::from_row(row);
44        let metadata_str: String = cols.get(row, "metadata")?;
45        let metadata: serde_json::Value = serde_json::from_str(&metadata_str).map_err(|e| {
46            crate::error::Error::internal(format!("invalid audit metadata JSON: {e}"))
47        })?;
48
49        Ok(Self {
50            id: cols.get(row, "id")?,
51            actor: cols.get(row, "actor")?,
52            action: cols.get(row, "action")?,
53            resource_type: cols.get(row, "resource_type")?,
54            resource_id: cols.get(row, "resource_id")?,
55            metadata,
56            ip: cols.get(row, "ip")?,
57            user_agent: cols.get(row, "user_agent")?,
58            device_name: cols.get(row, "device_name")?,
59            device_type: cols.get(row, "device_type")?,
60            fingerprint: cols.get(row, "fingerprint")?,
61            tenant_id: cols.get(row, "tenant_id")?,
62            created_at: cols.get(row, "created_at")?,
63        })
64    }
65}