Skip to main content

allowthem_core/
audit.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4use crate::db::Db;
5use crate::error::AuthError;
6use crate::types::{AuditEntryId, UserId};
7
8/// Every type of authentication event that can be recorded.
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
10#[sqlx(rename_all = "snake_case")]
11pub enum AuditEvent {
12    Login,
13    LoginFailed,
14    Logout,
15    Register,
16    PasswordChange,
17    PasswordReset,
18    RoleAssigned,
19    RoleUnassigned,
20    PermissionAssigned,
21    PermissionUnassigned,
22    SessionCreated,
23    SessionExpired,
24    UserUpdated,
25    UserDeleted,
26}
27
28/// A single record in the audit log.
29#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
30pub struct AuditEntry {
31    pub id: AuditEntryId,
32    pub event_type: AuditEvent,
33    pub user_id: Option<UserId>,
34    pub target_id: Option<String>,
35    pub ip_address: Option<String>,
36    pub user_agent: Option<String>,
37    pub detail: Option<String>,
38    pub created_at: DateTime<Utc>,
39}
40
41impl Db {
42    /// Record an audit event.
43    ///
44    /// `user_id` may be `None` for events where no authenticated user is
45    /// involved (e.g. a failed login attempt against an unknown email).
46    pub async fn log_audit(
47        &self,
48        event_type: AuditEvent,
49        user_id: Option<&UserId>,
50        target_id: Option<&str>,
51        ip_address: Option<&str>,
52        user_agent: Option<&str>,
53        detail: Option<&str>,
54    ) -> Result<(), AuthError> {
55        let id = AuditEntryId::new();
56        sqlx::query(
57            "INSERT INTO allowthem_audit_log
58             (id, event_type, user_id, target_id, ip_address, user_agent, detail)
59             VALUES (?, ?, ?, ?, ?, ?, ?)",
60        )
61        .bind(id)
62        .bind(event_type)
63        .bind(user_id.copied())
64        .bind(target_id)
65        .bind(ip_address)
66        .bind(user_agent)
67        .bind(detail)
68        .execute(self.pool())
69        .await
70        .map_err(AuthError::Database)?;
71        Ok(())
72    }
73
74    /// Retrieve audit log entries, optionally filtered by user.
75    ///
76    /// Results are ordered by `created_at` descending (newest first).
77    pub async fn get_audit_log(
78        &self,
79        user_id: Option<&UserId>,
80        limit: u32,
81        offset: u32,
82    ) -> Result<Vec<AuditEntry>, AuthError> {
83        match user_id {
84            Some(uid) => {
85                sqlx::query_as::<_, AuditEntry>(
86                    "SELECT id, event_type, user_id, target_id, ip_address, user_agent, detail, created_at
87                     FROM allowthem_audit_log
88                     WHERE user_id = ?
89                     ORDER BY created_at DESC
90                     LIMIT ? OFFSET ?",
91                )
92                .bind(*uid)
93                .bind(limit)
94                .bind(offset)
95                .fetch_all(self.pool())
96                .await
97                .map_err(AuthError::Database)
98            }
99            None => {
100                sqlx::query_as::<_, AuditEntry>(
101                    "SELECT id, event_type, user_id, target_id, ip_address, user_agent, detail, created_at
102                     FROM allowthem_audit_log
103                     ORDER BY created_at DESC
104                     LIMIT ? OFFSET ?",
105                )
106                .bind(limit)
107                .bind(offset)
108                .fetch_all(self.pool())
109                .await
110                .map_err(AuthError::Database)
111            }
112        }
113    }
114
115    /// Retrieve audit log entries filtered by event type.
116    ///
117    /// Results are ordered by `created_at` descending (newest first).
118    pub async fn get_audit_log_by_event(
119        &self,
120        event_type: AuditEvent,
121        limit: u32,
122        offset: u32,
123    ) -> Result<Vec<AuditEntry>, AuthError> {
124        sqlx::query_as::<_, AuditEntry>(
125            "SELECT id, event_type, user_id, target_id, ip_address, user_agent, detail, created_at
126             FROM allowthem_audit_log
127             WHERE event_type = ?
128             ORDER BY created_at DESC
129             LIMIT ? OFFSET ?",
130        )
131        .bind(event_type)
132        .bind(limit)
133        .bind(offset)
134        .fetch_all(self.pool())
135        .await
136        .map_err(AuthError::Database)
137    }
138}