Skip to main content

oauth2_passkey/coordination/
login_history.rs

1//! Login history coordination functions
2//!
3//! This module provides coordination functions for recording and retrieving login history.
4
5use 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/// Record a successful login attempt
17///
18/// This function records a login event in the login history database.
19/// It should be called after a successful authentication (passkey or OAuth2).
20#[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            // Log but don't fail the login - recording history is non-critical
37            tracing::warn!(error = %e, "Failed to record login history (non-fatal)");
38            Ok(())
39        }
40    }
41}
42
43/// Record a failed login attempt
44///
45/// Separate from `record_login_success` to enforce type safety: successful logins
46/// always require a known `UserId`, while failures may occur without identifying
47/// the user (e.g., invalid credential ID or CSRF validation failure).
48#[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            // Log but don't fail - recording history is non-critical
74            tracing::warn!(error = %e, "Failed to record login failure (non-fatal)");
75            Ok(())
76        }
77    }
78}
79
80/// Get login history for the current user (user's own view)
81#[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    // Get user from session
88    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/// Get login history for any user (admin view)
105///
106/// Returns full login history entries including unmasked IP addresses.
107/// Requires admin privileges.
108#[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    // Validate admin session
116    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/// Get login history for the current user with date range filtering
130#[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    // Get user from session
139    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/// Query login history for admin with filters (audit page)
157///
158/// Returns full login history entries including unmasked IP addresses.
159/// Supports filtering by user, date range, and success status.
160/// Requires admin privileges.
161#[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    // Validate admin session
172    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}