use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use sqlx::FromRow;
use std::fmt;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct AuditLog {
pub log_id: Uuid,
pub user_id: Option<Uuid>,
pub action: AuditAction,
pub resource_type: String,
pub resource_id: Option<Uuid>,
pub details: Option<JsonValue>,
pub result: AuditResult,
pub error_message: Option<String>,
pub ip_address: Option<String>,
pub user_agent: Option<String>,
pub endpoint: Option<String>,
pub request_id: Option<String>,
pub severity: AuditSeverity,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, sqlx::Type, PartialEq, Eq)]
#[sqlx(type_name = "varchar", rename_all = "snake_case")]
#[derive(Default)]
pub enum AuditAction {
UserRegister,
UserLogin,
UserLogout,
UserUpdate,
UserDelete,
PasswordChange,
PasswordReset,
EmailVerify,
TwoFactorEnable,
TwoFactorDisable,
KycSubmit,
KycApprove,
KycReject,
KycUpdate,
TokenCreate,
TokenUpdate,
TokenPause,
TokenClose,
TokenMetadataUpdate,
OrderCreate,
OrderCancel,
OrderFill,
TradeExecute,
PaymentCreate,
PaymentDetected,
PaymentConfirmed,
PaymentExpired,
PaymentRefund,
BalanceDeposit,
BalanceWithdraw,
BalanceTransfer,
BalanceAdjustment,
CommitmentCreate,
CommitmentSubmitEvidence,
CommitmentVerify,
ReputationAdjust,
AdminAccessGrant,
AdminAccessRevoke,
#[default]
AdminActionPerformed,
SystemConfigUpdate,
EmergencyStop,
EmergencyResume,
LoginFailed,
AccessDenied,
SuspiciousActivity,
RateLimitExceeded,
SecurityAlert,
AmlScreening,
ComplianceReport,
DataExport,
DataDelete,
}
impl fmt::Display for AuditAction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, sqlx::Type, PartialEq, Eq)]
#[sqlx(type_name = "varchar", rename_all = "lowercase")]
#[derive(Default)]
pub enum AuditResult {
#[default]
Success,
Failure,
Partial,
}
impl fmt::Display for AuditResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AuditResult::Success => write!(f, "success"),
AuditResult::Failure => write!(f, "failure"),
AuditResult::Partial => write!(f, "partial"),
}
}
}
#[derive(
Debug, Clone, Copy, Serialize, Deserialize, sqlx::Type, PartialEq, Eq, PartialOrd, Ord,
)]
#[sqlx(type_name = "varchar", rename_all = "lowercase")]
#[derive(Default)]
pub enum AuditSeverity {
#[default]
Info,
Warning,
Error,
Critical,
}
impl fmt::Display for AuditSeverity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AuditSeverity::Info => write!(f, "info"),
AuditSeverity::Warning => write!(f, "warning"),
AuditSeverity::Error => write!(f, "error"),
AuditSeverity::Critical => write!(f, "critical"),
}
}
}
impl fmt::Display for AuditLog {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"AuditLog({}, action={}, result={}, severity={})",
self.log_id, self.action, self.result, self.severity
)
}
}
impl AuditLog {
pub fn builder() -> AuditLogBuilder {
AuditLogBuilder::new()
}
pub fn is_security_event(&self) -> bool {
matches!(
self.action,
AuditAction::LoginFailed
| AuditAction::AccessDenied
| AuditAction::SuspiciousActivity
| AuditAction::SecurityAlert
| AuditAction::TwoFactorEnable
| AuditAction::TwoFactorDisable
| AuditAction::EmergencyStop
| AuditAction::EmergencyResume
)
}
pub fn is_compliance_event(&self) -> bool {
matches!(
self.action,
AuditAction::KycSubmit
| AuditAction::KycApprove
| AuditAction::KycReject
| AuditAction::AmlScreening
| AuditAction::ComplianceReport
| AuditAction::DataExport
| AuditAction::DataDelete
)
}
pub fn requires_immediate_attention(&self) -> bool {
self.severity >= AuditSeverity::Error
|| (self.is_security_event() && self.result == AuditResult::Failure)
}
}
pub struct AuditLogBuilder {
user_id: Option<Uuid>,
action: Option<AuditAction>,
resource_type: Option<String>,
resource_id: Option<Uuid>,
details: Option<JsonValue>,
result: AuditResult,
error_message: Option<String>,
ip_address: Option<String>,
user_agent: Option<String>,
endpoint: Option<String>,
request_id: Option<String>,
severity: AuditSeverity,
}
impl AuditLogBuilder {
pub fn new() -> Self {
Self {
user_id: None,
action: None,
resource_type: None,
resource_id: None,
details: None,
result: AuditResult::Success,
error_message: None,
ip_address: None,
user_agent: None,
endpoint: None,
request_id: None,
severity: AuditSeverity::Info,
}
}
pub fn user_id(mut self, user_id: Uuid) -> Self {
self.user_id = Some(user_id);
self
}
pub fn action(mut self, action: AuditAction) -> Self {
self.action = Some(action);
self
}
pub fn resource(mut self, resource_type: String, resource_id: Option<Uuid>) -> Self {
self.resource_type = Some(resource_type);
self.resource_id = resource_id;
self
}
pub fn details(mut self, details: JsonValue) -> Self {
self.details = Some(details);
self
}
pub fn result(mut self, result: AuditResult) -> Self {
self.result = result;
self
}
pub fn error(mut self, error_message: String) -> Self {
self.error_message = Some(error_message);
self.result = AuditResult::Failure;
self
}
pub fn ip_address(mut self, ip: String) -> Self {
self.ip_address = Some(ip);
self
}
pub fn user_agent(mut self, ua: String) -> Self {
self.user_agent = Some(ua);
self
}
pub fn endpoint(mut self, endpoint: String) -> Self {
self.endpoint = Some(endpoint);
self
}
pub fn request_id(mut self, request_id: String) -> Self {
self.request_id = Some(request_id);
self
}
pub fn severity(mut self, severity: AuditSeverity) -> Self {
self.severity = severity;
self
}
pub fn build(self) -> Result<AuditLog, String> {
let action = self
.action
.ok_or_else(|| "Action is required".to_string())?;
Ok(AuditLog {
log_id: Uuid::new_v4(),
user_id: self.user_id,
action,
resource_type: self.resource_type.unwrap_or_else(|| "unknown".to_string()),
resource_id: self.resource_id,
details: self.details,
result: self.result,
error_message: self.error_message,
ip_address: self.ip_address,
user_agent: self.user_agent,
endpoint: self.endpoint,
request_id: self.request_id,
severity: self.severity,
created_at: Utc::now(),
})
}
}
impl Default for AuditLogBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Deserialize)]
pub struct AuditLogQuery {
pub user_id: Option<Uuid>,
pub action: Option<AuditAction>,
pub resource_type: Option<String>,
pub resource_id: Option<Uuid>,
pub result: Option<AuditResult>,
pub severity: Option<AuditSeverity>,
pub start_date: Option<DateTime<Utc>>,
pub end_date: Option<DateTime<Utc>>,
pub page: Option<u32>,
pub limit: Option<u32>,
}
#[derive(Debug, Serialize)]
pub struct AuditStats {
pub total_events: i64,
pub events_by_severity: Vec<SeverityCount>,
pub events_by_action: Vec<ActionCount>,
pub success_rate_percent: f64,
pub security_events_count: i64,
pub compliance_events_count: i64,
pub failed_login_attempts: i64,
pub critical_events_last_24h: i64,
}
#[derive(Debug, Serialize)]
pub struct SeverityCount {
pub severity: AuditSeverity,
pub count: i64,
}
#[derive(Debug, Serialize)]
pub struct ActionCount {
pub action: AuditAction,
pub count: i64,
}
pub struct AuditRetentionPolicy;
impl AuditRetentionPolicy {
pub const NORMAL_RETENTION_DAYS: i64 = 365;
pub const SECURITY_RETENTION_DAYS: i64 = 2555;
pub const COMPLIANCE_RETENTION_DAYS: i64 = 2555;
pub fn get_retention_days(log: &AuditLog) -> i64 {
if log.is_security_event() {
Self::SECURITY_RETENTION_DAYS
} else if log.is_compliance_event() {
Self::COMPLIANCE_RETENTION_DAYS
} else {
Self::NORMAL_RETENTION_DAYS
}
}
pub fn should_delete(log: &AuditLog) -> bool {
let retention_days = Self::get_retention_days(log);
let retention_cutoff = Utc::now() - chrono::Duration::days(retention_days);
log.created_at < retention_cutoff
}
}
#[macro_export]
macro_rules! audit_log {
($action:expr, $user_id:expr, $resource_type:expr, $resource_id:expr) => {
$crate::models::audit::AuditLog::builder()
.action($action)
.user_id($user_id)
.resource($resource_type.to_string(), Some($resource_id))
.build()
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audit_log_builder() {
let log = AuditLog::builder()
.user_id(Uuid::new_v4())
.action(AuditAction::UserLogin)
.resource("user".to_string(), None)
.severity(AuditSeverity::Info)
.build()
.unwrap();
assert_eq!(log.action, AuditAction::UserLogin);
assert_eq!(log.result, AuditResult::Success);
assert_eq!(log.severity, AuditSeverity::Info);
}
#[test]
fn test_security_event_detection() {
let log = AuditLog::builder()
.action(AuditAction::LoginFailed)
.severity(AuditSeverity::Warning)
.build()
.unwrap();
assert!(log.is_security_event());
assert!(!log.is_compliance_event());
}
#[test]
fn test_compliance_event_detection() {
let log = AuditLog::builder()
.action(AuditAction::KycApprove)
.severity(AuditSeverity::Info)
.build()
.unwrap();
assert!(!log.is_security_event());
assert!(log.is_compliance_event());
}
#[test]
fn test_requires_immediate_attention() {
let critical_log = AuditLog::builder()
.action(AuditAction::SecurityAlert)
.severity(AuditSeverity::Critical)
.build()
.unwrap();
assert!(critical_log.requires_immediate_attention());
let normal_log = AuditLog::builder()
.action(AuditAction::UserLogin)
.severity(AuditSeverity::Info)
.build()
.unwrap();
assert!(!normal_log.requires_immediate_attention());
}
#[test]
fn test_retention_policy() {
let security_log = AuditLog::builder()
.action(AuditAction::LoginFailed)
.build()
.unwrap();
assert_eq!(
AuditRetentionPolicy::get_retention_days(&security_log),
AuditRetentionPolicy::SECURITY_RETENTION_DAYS
);
let compliance_log = AuditLog::builder()
.action(AuditAction::KycApprove)
.build()
.unwrap();
assert_eq!(
AuditRetentionPolicy::get_retention_days(&compliance_log),
AuditRetentionPolicy::COMPLIANCE_RETENTION_DAYS
);
let normal_log = AuditLog::builder()
.action(AuditAction::TokenCreate)
.build()
.unwrap();
assert_eq!(
AuditRetentionPolicy::get_retention_days(&normal_log),
AuditRetentionPolicy::NORMAL_RETENTION_DAYS
);
}
#[test]
fn test_audit_log_error() {
let log = AuditLog::builder()
.action(AuditAction::OrderCreate)
.error("Insufficient balance".to_string())
.severity(AuditSeverity::Error)
.build()
.unwrap();
assert_eq!(log.result, AuditResult::Failure);
assert_eq!(log.error_message, Some("Insufficient balance".to_string()));
}
}