use chrono::{DateTime, Utc};
use crate::audit::{
AuthMethod, AuthMethodDetails, LoginContext, LoginHistoryEntry, LoginHistoryError,
LoginHistoryStore,
};
use crate::session::{SessionId, UserId, get_user_from_session};
use super::admin::validate_admin_session;
use super::errors::CoordinationError;
#[tracing::instrument(skip(context, details), fields(user_id = %user_id.as_str(), auth_method = %auth_method))]
pub(super) async fn record_login_success(
user_id: UserId,
auth_method: AuthMethod,
context: LoginContext,
details: AuthMethodDetails,
) -> Result<(), CoordinationError> {
let entry =
LoginHistoryEntry::success(user_id.as_str().to_string(), auth_method, context, details);
match LoginHistoryStore::insert(entry).await {
Ok(_) => {
tracing::debug!("Login history recorded successfully");
Ok(())
}
Err(e) => {
tracing::warn!(error = %e, "Failed to record login history (non-fatal)");
Ok(())
}
}
}
#[tracing::instrument(skip(context), fields(user_id = user_id.as_ref().map(|u| u.as_str()), auth_method = %auth_method))]
pub(super) async fn record_login_failure(
user_id: Option<UserId>,
auth_method: AuthMethod,
context: LoginContext,
credential_id: Option<String>,
failure_reason: String,
) -> Result<(), CoordinationError> {
let entry = LoginHistoryEntry::failure(
user_id
.as_ref()
.map(|u| u.as_str().to_string())
.unwrap_or_default(),
auth_method,
context,
credential_id,
failure_reason,
);
match LoginHistoryStore::insert(entry).await {
Ok(_) => {
tracing::debug!("Login failure recorded successfully");
Ok(())
}
Err(e) => {
tracing::warn!(error = %e, "Failed to record login failure (non-fatal)");
Ok(())
}
}
}
#[tracing::instrument(skip(session_cookie), fields(user_id))]
pub async fn get_own_login_history(
session_cookie: &crate::session::SessionCookie,
limit: Option<i64>,
offset: Option<i64>,
) -> Result<Vec<LoginHistoryEntry>, CoordinationError> {
let session_user = get_user_from_session(session_cookie)
.await
.map_err(|_| CoordinationError::Unauthorized)?;
tracing::Span::current().record("user_id", &session_user.id);
let limit = limit.unwrap_or(50);
let offset = offset.unwrap_or(0);
let entries = LoginHistoryStore::get_by_user(&session_user.id, limit, offset)
.await
.map_err(|e| CoordinationError::Database(e.to_string()))?;
Ok(entries)
}
#[tracing::instrument(fields(admin_user_id, target_user_id = %target_user_id.as_str()))]
pub async fn get_user_login_history_admin(
session_id: SessionId,
target_user_id: UserId,
limit: Option<i64>,
offset: Option<i64>,
) -> Result<Vec<LoginHistoryEntry>, CoordinationError> {
let admin_user = validate_admin_session(session_id).await?;
tracing::Span::current().record("admin_user_id", &admin_user.id);
let limit = limit.unwrap_or(50);
let offset = offset.unwrap_or(0);
let entries = LoginHistoryStore::get_by_user(target_user_id.as_str(), limit, offset)
.await
.map_err(|e| CoordinationError::Database(e.to_string()))?;
Ok(entries)
}
#[tracing::instrument(skip(session_cookie), fields(user_id))]
pub async fn get_own_login_history_with_date_range(
session_cookie: &crate::session::SessionCookie,
from: Option<DateTime<Utc>>,
to: Option<DateTime<Utc>>,
limit: Option<i64>,
offset: Option<i64>,
) -> Result<Vec<LoginHistoryEntry>, CoordinationError> {
let session_user = get_user_from_session(session_cookie)
.await
.map_err(|_| CoordinationError::Unauthorized)?;
tracing::Span::current().record("user_id", &session_user.id);
let limit = limit.unwrap_or(50);
let offset = offset.unwrap_or(0);
let entries =
LoginHistoryStore::get_by_user_with_date_range(&session_user.id, from, to, limit, offset)
.await
.map_err(|e| CoordinationError::Database(e.to_string()))?;
Ok(entries)
}
#[tracing::instrument(fields(admin_user_id))]
pub async fn query_login_history_admin(
session_id: SessionId,
user_id: Option<&str>,
from: Option<DateTime<Utc>>,
to: Option<DateTime<Utc>>,
success: Option<bool>,
limit: Option<i64>,
offset: Option<i64>,
) -> Result<Vec<LoginHistoryEntry>, CoordinationError> {
let admin_user = validate_admin_session(session_id).await?;
tracing::Span::current().record("admin_user_id", &admin_user.id);
let limit = limit.unwrap_or(50);
let offset = offset.unwrap_or(0);
let entries = LoginHistoryStore::query_admin(user_id, from, to, success, limit, offset)
.await
.map_err(|e| CoordinationError::Database(e.to_string()))?;
Ok(entries)
}
impl From<LoginHistoryError> for CoordinationError {
fn from(err: LoginHistoryError) -> Self {
CoordinationError::Database(err.to_string())
}
}