Skip to main content

sova_scope/
audit.rs

1//! Scope membership role-change audit log.
2
3use crate::entity::scope_membership_audit;
4use crate::types::ScopeRef;
5use chrono::Utc;
6use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect};
7use sova_core::Result;
8use sova_db::{ActiveModelTrait, DbError, DbHandle, Set};
9
10pub const AUDIT_ADDED: &str = "added";
11pub const AUDIT_ROLE_CHANGED: &str = "role_changed";
12pub const AUDIT_REMOVED: &str = "removed";
13
14#[derive(Clone, Debug)]
15pub struct AuditEntry {
16    pub id: i64,
17    pub scope_kind: String,
18    pub scope_id: i64,
19    pub user_id: i64,
20    pub actor_id: Option<i64>,
21    pub action: String,
22    pub old_role: Option<String>,
23    pub new_role: Option<String>,
24    pub created_at: chrono::DateTime<chrono::Utc>,
25}
26
27pub struct AuditStore;
28
29impl AuditStore {
30    pub async fn record(
31        db: &DbHandle,
32        scope: &ScopeRef,
33        user_id: i64,
34        actor_id: Option<i64>,
35        action: &str,
36        old_role: Option<&str>,
37        new_role: Option<&str>,
38    ) -> Result<AuditEntry> {
39        let row = scope_membership_audit::ActiveModel {
40            scope_kind: Set(scope.kind.clone()),
41            scope_id: Set(scope.id),
42            user_id: Set(user_id),
43            actor_id: Set(actor_id),
44            action: Set(action.into()),
45            old_role: Set(old_role.map(str::to_string)),
46            new_role: Set(new_role.map(str::to_string)),
47            created_at: Set(Utc::now()),
48            ..Default::default()
49        }
50        .insert(db)
51        .await
52        .map_err(DbError::from)?;
53        Ok(model_to_entry(row))
54    }
55
56    pub async fn list_for_scope(
57        db: &DbHandle,
58        scope: ScopeRef,
59        limit: u64,
60    ) -> Result<Vec<AuditEntry>> {
61        Ok(scope_membership_audit::Entity::find()
62            .filter(scope_membership_audit::Column::ScopeKind.eq(scope.kind))
63            .filter(scope_membership_audit::Column::ScopeId.eq(scope.id))
64            .order_by_desc(scope_membership_audit::Column::CreatedAt)
65            .limit(limit)
66            .all(db)
67            .await
68            .map_err(DbError::from)?
69            .into_iter()
70            .map(model_to_entry)
71            .collect())
72    }
73
74    pub async fn list_for_user(
75        db: &DbHandle,
76        user_id: i64,
77        limit: u64,
78    ) -> Result<Vec<AuditEntry>> {
79        Ok(scope_membership_audit::Entity::find()
80            .filter(scope_membership_audit::Column::UserId.eq(user_id))
81            .order_by_desc(scope_membership_audit::Column::CreatedAt)
82            .limit(limit)
83            .all(db)
84            .await
85            .map_err(DbError::from)?
86            .into_iter()
87            .map(model_to_entry)
88            .collect())
89    }
90}
91
92fn model_to_entry(m: scope_membership_audit::Model) -> AuditEntry {
93    AuditEntry {
94        id: m.id,
95        scope_kind: m.scope_kind,
96        scope_id: m.scope_id,
97        user_id: m.user_id,
98        actor_id: m.actor_id,
99        action: m.action,
100        old_role: m.old_role,
101        new_role: m.new_role,
102        created_at: m.created_at,
103    }
104}