update_last_login.rs
143 lignes · 4717 octets
/// Update Last Login Middleware /// Feature 009: Admin & Settings /// /// Middleware to update last_login_at and last_login_ip after successful authentication. /// Extracts IP from request headers (handles X-Forwarded-For, X-Real-IP for reverse proxy). use axum::{ extract::{Request, State}, http::HeaderMap, middleware::Next, response::Response, }; use chrono::Utc; use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, Set}; use tower_sessions::Session; use crate::middleware::auth_required::extract_user_from_jwt; use crate::models::user; /// Extract client IP from request headers /// /// Checks headers in order: /// 1. X-Forwarded-For (first IP if multiple) /// 2. X-Real-IP /// 3. Direct connection IP (from request) - currently not available, returns None /// /// # Arguments /// /// * `headers` - HTTP request headers /// /// # Returns /// /// IP address as String, or None if cannot be determined pub fn extract_client_ip(headers: &HeaderMap) -> Option<String> { // Check X-Forwarded-For header (first IP if multiple) if let Some(forwarded_for) = headers.get("X-Forwarded-For") { if let Ok(forwarded_str) = forwarded_for.to_str() { // X-Forwarded-For can contain multiple IPs: "client, proxy1, proxy2" // Take the first one (original client) let first_ip = forwarded_str.split(',').next().unwrap_or("").trim(); if !first_ip.is_empty() { return Some(first_ip.to_string()); } } } // Check X-Real-IP header if let Some(real_ip) = headers.get("X-Real-IP") { if let Ok(real_ip_str) = real_ip.to_str() { if !real_ip_str.trim().is_empty() { return Some(real_ip_str.trim().to_string()); } } } // For direct connections, we would need access to the connection info // This is not easily available in Axum middleware, so we return None // In production behind a reverse proxy, X-Forwarded-For or X-Real-IP should be set None } /// Middleware that updates last_login_at and last_login_ip after authentication /// /// This middleware should be applied after auth_required middleware to ensure /// the user is authenticated. It updates the user's last login information /// without blocking the request if the update fails. /// /// # Arguments /// /// * `State(db)` - Database connection /// * `session` - Tower session /// * `request` - HTTP request /// * `next` - Next middleware or handler /// /// # Returns /// /// Always passes the request through (non-blocking) pub async fn update_last_login( State(db): State<DatabaseConnection>, _session: Session, request: Request, next: Next, ) -> Response { // Extract user from JWT (reuse auth_required logic) if let Some(claims) = extract_user_from_jwt(&db, &request).await { let user_id = claims.sub; let headers = request.headers(); let client_ip = extract_client_ip(headers); // Update user's last login information if let Ok(Some(user_model)) = user::Entity::find_by_id(user_id).one(&db).await { let mut user_active: user::ActiveModel = user_model.into(); user_active.last_login_at = Set(Some(Utc::now())); if let Some(ip) = client_ip { user_active.last_login_ip = Set(Some(ip)); } // Update without blocking request if it fails if let Err(e) = user_active.update(&db).await { tracing::warn!("Failed to update last_login for user {}: {}", user_id, e); } else { tracing::debug!("Updated last_login for user {}", user_id); } } } // Always proceed to handler (non-blocking) next.run(request).await } /// Internal implementation that takes db and session as parameters pub async fn update_last_login_impl( db: &DatabaseConnection, session: Session, request: Request, next: Next, ) -> Response { update_last_login(State(db.clone()), session, request, next).await } #[cfg(test)] mod tests { use super::*; #[test] fn test_extract_client_ip_from_forwarded_for() { let mut headers = HeaderMap::new(); headers.insert("X-Forwarded-For", "192.168.1.1, 10.0.0.1".parse().unwrap()); let ip = extract_client_ip(&headers); assert_eq!(ip, Some("192.168.1.1".to_string())); } #[test] fn test_extract_client_ip_from_real_ip() { let mut headers = HeaderMap::new(); headers.insert("X-Real-IP", "192.168.1.2".parse().unwrap()); let ip = extract_client_ip(&headers); assert_eq!(ip, Some("192.168.1.2".to_string())); } }
GitRust