oauth2_passkey/coordination/
login_history.rs1use chrono::{DateTime, Utc};
6
7use crate::audit::{
8 AuthMethod, AuthMethodDetails, LoginContext, LoginHistoryEntry, LoginHistoryError,
9 LoginHistoryStore,
10};
11use crate::session::{SessionId, UserId, get_user_from_session};
12
13use super::admin::validate_admin_session;
14use super::errors::CoordinationError;
15
16#[tracing::instrument(skip(context, details), fields(user_id = %user_id.as_str(), auth_method = %auth_method))]
21pub(super) async fn record_login_success(
22 user_id: UserId,
23 auth_method: AuthMethod,
24 context: LoginContext,
25 details: AuthMethodDetails,
26) -> Result<(), CoordinationError> {
27 let entry =
28 LoginHistoryEntry::success(user_id.as_str().to_string(), auth_method, context, details);
29
30 match LoginHistoryStore::insert(entry).await {
31 Ok(_) => {
32 tracing::debug!("Login history recorded successfully");
33 Ok(())
34 }
35 Err(e) => {
36 tracing::warn!(error = %e, "Failed to record login history (non-fatal)");
38 Ok(())
39 }
40 }
41}
42
43#[tracing::instrument(skip(context), fields(user_id = user_id.as_ref().map(|u| u.as_str()), auth_method = %auth_method))]
49pub(super) async fn record_login_failure(
50 user_id: Option<UserId>,
51 auth_method: AuthMethod,
52 context: LoginContext,
53 credential_id: Option<String>,
54 failure_reason: String,
55) -> Result<(), CoordinationError> {
56 let entry = LoginHistoryEntry::failure(
57 user_id
58 .as_ref()
59 .map(|u| u.as_str().to_string())
60 .unwrap_or_default(),
61 auth_method,
62 context,
63 credential_id,
64 failure_reason,
65 );
66
67 match LoginHistoryStore::insert(entry).await {
68 Ok(_) => {
69 tracing::debug!("Login failure recorded successfully");
70 Ok(())
71 }
72 Err(e) => {
73 tracing::warn!(error = %e, "Failed to record login failure (non-fatal)");
75 Ok(())
76 }
77 }
78}
79
80#[tracing::instrument(skip(session_cookie), fields(user_id))]
82pub async fn get_own_login_history(
83 session_cookie: &crate::session::SessionCookie,
84 limit: Option<i64>,
85 offset: Option<i64>,
86) -> Result<Vec<LoginHistoryEntry>, CoordinationError> {
87 let session_user = get_user_from_session(session_cookie)
89 .await
90 .map_err(|_| CoordinationError::Unauthorized)?;
91
92 tracing::Span::current().record("user_id", &session_user.id);
93
94 let limit = limit.unwrap_or(50);
95 let offset = offset.unwrap_or(0);
96
97 let entries = LoginHistoryStore::get_by_user(&session_user.id, limit, offset)
98 .await
99 .map_err(|e| CoordinationError::Database(e.to_string()))?;
100
101 Ok(entries)
102}
103
104#[tracing::instrument(fields(admin_user_id, target_user_id = %target_user_id.as_str()))]
109pub async fn get_user_login_history_admin(
110 session_id: SessionId,
111 target_user_id: UserId,
112 limit: Option<i64>,
113 offset: Option<i64>,
114) -> Result<Vec<LoginHistoryEntry>, CoordinationError> {
115 let admin_user = validate_admin_session(session_id).await?;
117 tracing::Span::current().record("admin_user_id", &admin_user.id);
118
119 let limit = limit.unwrap_or(50);
120 let offset = offset.unwrap_or(0);
121
122 let entries = LoginHistoryStore::get_by_user(target_user_id.as_str(), limit, offset)
123 .await
124 .map_err(|e| CoordinationError::Database(e.to_string()))?;
125
126 Ok(entries)
127}
128
129#[tracing::instrument(skip(session_cookie), fields(user_id))]
131pub async fn get_own_login_history_with_date_range(
132 session_cookie: &crate::session::SessionCookie,
133 from: Option<DateTime<Utc>>,
134 to: Option<DateTime<Utc>>,
135 limit: Option<i64>,
136 offset: Option<i64>,
137) -> Result<Vec<LoginHistoryEntry>, CoordinationError> {
138 let session_user = get_user_from_session(session_cookie)
140 .await
141 .map_err(|_| CoordinationError::Unauthorized)?;
142
143 tracing::Span::current().record("user_id", &session_user.id);
144
145 let limit = limit.unwrap_or(50);
146 let offset = offset.unwrap_or(0);
147
148 let entries =
149 LoginHistoryStore::get_by_user_with_date_range(&session_user.id, from, to, limit, offset)
150 .await
151 .map_err(|e| CoordinationError::Database(e.to_string()))?;
152
153 Ok(entries)
154}
155
156#[tracing::instrument(fields(admin_user_id))]
162pub async fn query_login_history_admin(
163 session_id: SessionId,
164 user_id: Option<&str>,
165 from: Option<DateTime<Utc>>,
166 to: Option<DateTime<Utc>>,
167 success: Option<bool>,
168 limit: Option<i64>,
169 offset: Option<i64>,
170) -> Result<Vec<LoginHistoryEntry>, CoordinationError> {
171 let admin_user = validate_admin_session(session_id).await?;
173 tracing::Span::current().record("admin_user_id", &admin_user.id);
174
175 let limit = limit.unwrap_or(50);
176 let offset = offset.unwrap_or(0);
177
178 let entries = LoginHistoryStore::query_admin(user_id, from, to, success, limit, offset)
179 .await
180 .map_err(|e| CoordinationError::Database(e.to_string()))?;
181
182 Ok(entries)
183}
184
185impl From<LoginHistoryError> for CoordinationError {
186 fn from(err: LoginHistoryError) -> Self {
187 CoordinationError::Database(err.to_string())
188 }
189}