oauth2_passkey/audit/
types.rs1use chrono::{DateTime, Utc};
4use http::HeaderMap;
5use serde::{Deserialize, Serialize};
6use sqlx::FromRow;
7use std::fmt;
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11#[serde(rename_all = "snake_case")]
12pub(crate) enum AuthMethod {
13 Passkey,
14 OAuth2,
15 FedCM,
16}
17
18impl fmt::Display for AuthMethod {
19 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20 match self {
21 AuthMethod::Passkey => write!(f, "passkey"),
22 AuthMethod::OAuth2 => write!(f, "oauth2"),
23 AuthMethod::FedCM => write!(f, "fedcm"),
24 }
25 }
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
30pub struct LoginHistoryEntry {
31 pub id: Option<i64>,
33 pub user_id: String,
35 pub timestamp: DateTime<Utc>,
37 pub auth_method: String,
39 pub ip_address: Option<String>,
41 pub user_agent: Option<String>,
43 pub success: bool,
45 pub credential_id: Option<String>,
47 pub provider: Option<String>,
49 pub provider_user_id: Option<String>,
51 pub failure_reason: Option<String>,
53 pub aaguid: Option<String>,
55 pub email: Option<String>,
57}
58
59#[derive(Debug, Clone, Default)]
64pub(crate) struct AuthMethodDetails {
65 pub credential_id: Option<String>,
67 pub provider: Option<String>,
69 pub provider_user_id: Option<String>,
71 pub aaguid: Option<String>,
73 pub email: Option<String>,
75}
76
77impl LoginHistoryEntry {
78 pub(crate) fn success(
80 user_id: String,
81 auth_method: AuthMethod,
82 context: LoginContext,
83 details: AuthMethodDetails,
84 ) -> Self {
85 Self {
86 id: None,
87 user_id,
88 timestamp: Utc::now(),
89 auth_method: auth_method.to_string(),
90 ip_address: context.ip_address,
91 user_agent: context.user_agent.map(|ua| truncate_user_agent(&ua)),
92 success: true,
93 credential_id: details.credential_id,
94 provider: details.provider,
95 provider_user_id: details.provider_user_id,
96 failure_reason: None,
97 aaguid: details.aaguid,
98 email: details.email,
99 }
100 }
101
102 pub(crate) fn failure(
104 user_id: String,
105 auth_method: AuthMethod,
106 context: LoginContext,
107 credential_id: Option<String>,
108 reason: String,
109 ) -> Self {
110 Self {
111 id: None,
112 user_id,
113 timestamp: Utc::now(),
114 auth_method: auth_method.to_string(),
115 ip_address: context.ip_address,
116 user_agent: context.user_agent.map(|ua| truncate_user_agent(&ua)),
117 success: false,
118 credential_id,
119 provider: None,
120 provider_user_id: None,
121 failure_reason: Some(reason),
122 aaguid: None,
123 email: None,
124 }
125 }
126}
127
128#[derive(Debug, Clone, Default)]
130pub(crate) struct LoginContext {
131 ip_address: Option<String>,
133 user_agent: Option<String>,
135}
136
137impl LoginContext {
138 pub(crate) fn from_headers(headers: &HeaderMap) -> Self {
143 let ip_address = headers
144 .get("x-forwarded-for")
145 .and_then(|v| v.to_str().ok())
146 .map(|s| s.split(',').next().unwrap_or(s).trim().to_string())
147 .or_else(|| {
148 headers
149 .get("x-real-ip")
150 .and_then(|v| v.to_str().ok())
151 .map(|s| s.to_string())
152 });
153
154 let user_agent = headers
155 .get("user-agent")
156 .and_then(|v| v.to_str().ok())
157 .map(|s| s.to_string());
158
159 Self {
160 ip_address,
161 user_agent,
162 }
163 }
164}
165
166fn truncate_user_agent(ua: &str) -> String {
168 const MAX_LENGTH: usize = 512;
169 if ua.len() > MAX_LENGTH {
170 ua[..MAX_LENGTH].to_string()
171 } else {
172 ua.to_string()
173 }
174}
175
176#[cfg(test)]
177mod tests;