use backbone_auth::*;
use uuid::Uuid;
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH, Duration};
use tokio::time::sleep;
use serde::{Serialize, Deserialize};
use async_trait::async_trait;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
pub id: String,
pub user_id: Uuid,
pub token_hash: String,
pub device_info: DeviceInfo,
pub ip_address: String,
pub user_agent: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub last_accessed_at: chrono::DateTime<chrono::Utc>,
pub expires_at: chrono::DateTime<chrono::Utc>,
pub is_active: bool,
pub session_type: SessionType,
pub location: Option<GeoLocation>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SessionType {
Web,
Mobile,
Desktop,
Api,
CLI,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeoLocation {
pub country: String,
pub city: String,
pub latitude: f64,
pub longitude: f64,
}
#[async_trait]
pub trait SessionRepository {
async fn create_session(&self, session: &Session) -> Result<()>;
async fn find_by_token(&self, token_hash: &str) -> Result<Option<Session>>;
async fn find_by_user_id(&self, user_id: &Uuid) -> Result<Vec<Session>>;
async fn update_last_accessed(&self, session_id: &str) -> Result<()>;
async fn revoke_session(&self, session_id: &str) -> Result<()>;
async fn revoke_all_user_sessions(&self, user_id: &Uuid) -> Result<()>;
async fn revoke_other_sessions(&self, user_id: &Uuid, keep_session_id: &str) -> Result<()>;
async fn cleanup_expired_sessions(&self) -> Result<usize>;
async fn get_active_sessions_count(&self, user_id: &Uuid) -> Result<usize>;
}
struct InMemorySessionRepository {
sessions: std::sync::Arc<tokio::sync::RwLock<HashMap<String, Session>>>,
}
impl InMemorySessionRepository {
fn new() -> Self {
Self {
sessions: std::sync::Arc::new(tokio::sync::RwLock::new(HashMap::new())),
}
}
}
#[async_trait]
impl SessionRepository for InMemorySessionRepository {
async fn create_session(&self, session: &Session) -> Result<()> {
let mut sessions = self.sessions.write().await;
sessions.insert(session.id.clone(), session.clone());
println!("💾 Created session: {} for user: {}", session.id, session.user_id);
Ok(())
}
async fn find_by_token(&self, token_hash: &str) -> Result<Option<Session>> {
let sessions = self.sessions.read().await;
Ok(sessions
.values()
.find(|s| s.token_hash == token_hash && s.is_active && s.expires_at > chrono::Utc::now())
.cloned())
}
async fn find_by_user_id(&self, user_id: &Uuid) -> Result<Vec<Session>> {
let sessions = self.sessions.read().await;
Ok(sessions
.values()
.filter(|s| s.user_id == *user_id && s.is_active && s.expires_at > chrono::Utc::now())
.cloned()
.collect())
}
async fn update_last_accessed(&self, session_id: &str) -> Result<()> {
let mut sessions = self.sessions.write().await;
if let Some(session) = sessions.get_mut(session_id) {
session.last_accessed_at = chrono::Utc::now();
println!("🔄 Updated last accessed time for session: {}", session_id);
}
Ok(())
}
async fn revoke_session(&self, session_id: &str) -> Result<()> {
let mut sessions = self.sessions.write().await;
if let Some(session) = sessions.get_mut(session_id) {
session.is_active = false;
println!("🚫 Revoked session: {}", session_id);
}
Ok(())
}
async fn revoke_all_user_sessions(&self, user_id: &Uuid) -> Result<()> {
let mut sessions = self.sessions.write().await;
let mut revoked_count = 0;
for session in sessions.values_mut() {
if session.user_id == *user_id && session.is_active {
session.is_active = false;
revoked_count += 1;
}
}
println!("🚫 Revoked {} sessions for user: {}", revoked_count, user_id);
Ok(())
}
async fn revoke_other_sessions(&self, user_id: &Uuid, keep_session_id: &str) -> Result<()> {
let mut sessions = self.sessions.write().await;
let mut revoked_count = 0;
for (session_id, session) in sessions.iter_mut() {
if session.user_id == *user_id && session.is_active && session_id != keep_session_id {
session.is_active = false;
revoked_count += 1;
}
}
println!("🚫 Revoked {} other sessions for user: {}", revoked_count, user_id);
Ok(())
}
async fn cleanup_expired_sessions(&self) -> Result<usize> {
let mut sessions = self.sessions.write().await;
let before_count = sessions.len();
sessions.retain(|_, session| session.expires_at > chrono::Utc::now() || session.is_active);
let after_count = sessions.len();
let cleaned_count = before_count - after_count;
if cleaned_count > 0 {
println!("🧹 Cleaned up {} expired sessions", cleaned_count);
}
Ok(cleaned_count)
}
async fn get_active_sessions_count(&self, user_id: &Uuid) -> Result<usize> {
let sessions = self.sessions.read().await;
Ok(sessions
.values()
.filter(|s| s.user_id == *user_id && s.is_active && s.expires_at > chrono::Utc::now())
.count())
}
}
pub struct SessionManager {
session_repository: Box<dyn SessionRepository + Send + Sync>,
max_sessions_per_user: usize,
session_timeout_hours: i64,
}
impl SessionManager {
pub fn new(session_repository: Box<dyn SessionRepository + Send + Sync>) -> Self {
Self {
session_repository,
max_sessions_per_user: 5, session_timeout_hours: 24, }
}
pub fn with_limits(
mut self,
max_sessions: usize,
timeout_hours: i64
) -> Self {
self.max_sessions_per_user = max_sessions;
self.session_timeout_hours = timeout_hours;
self
}
pub async fn create_session(
&self,
user_id: &Uuid,
token: &str,
device_info: DeviceInfo,
ip_address: &str,
session_type: SessionType,
) -> Result<String> {
let active_count = self.session_repository.get_active_sessions_count(user_id).await?;
if active_count >= self.max_sessions_per_user {
self.revoke_oldest_session(user_id).await?;
}
let session_id = Uuid::new_v4().to_string();
let token_hash = self.hash_token(token)?;
let location = self.detect_location(ip_address);
let session = Session {
id: session_id.clone(),
user_id: *user_id,
token_hash,
device_info,
ip_address: ip_address.to_string(),
user_agent: device_info.user_agent.clone(),
created_at: chrono::Utc::now(),
last_accessed_at: chrono::Utc::now(),
expires_at: chrono::Utc::now() + chrono::Duration::hours(self.session_timeout_hours),
is_active: true,
session_type,
location,
};
self.session_repository.create_session(&session).await?;
println!("🔐 Created new session: {} for user: {}", session_id, user_id);
Ok(session_id)
}
pub async fn validate_session(&self, token: &str) -> Result<Option<Session>> {
let token_hash = self.hash_token(token)?;
if let Some(mut session) = self.session_repository.find_by_token(&token_hash).await? {
if session.expires_at > chrono::Utc::now() && session.is_active {
self.session_repository.update_last_accessed(&session.id).await?;
session.last_accessed_at = chrono::Utc::now();
println!("✅ Session validated: {} for user: {}", session.id, session.user_id);
return Ok(Some(session));
} else {
println!("⏰ Session expired or inactive: {}", session.id);
self.session_repository.revoke_session(&session.id).await?;
}
}
Ok(None)
}
pub async fn revoke_session(&self, session_id: &str) -> Result<()> {
self.session_repository.revoke_session(session_id).await
}
pub async fn revoke_all_user_sessions(&self, user_id: &Uuid) -> Result<()> {
self.session_repository.revoke_all_user_sessions(user_id).await
}
pub async fn revoke_other_sessions(&self, user_id: &Uuid, current_session_id: &str) -> Result<()> {
self.session_repository.revoke_other_sessions(user_id, current_session_id).await
}
pub async fn get_user_sessions(&self, user_id: &Uuid) -> Result<Vec<Session>> {
self.session_repository.find_by_user_id(user_id).await
}
pub async fn cleanup_expired_sessions(&self) -> Result<usize> {
self.session_repository.cleanup_expired_sessions().await
}
pub async fn detect_suspicious_activity(&self, user_id: &Uuid, new_session: &Session) -> Result<Vec<String>> {
let existing_sessions = self.get_user_sessions(user_id).await?;
let mut alerts = Vec::new();
let recent_sessions: Vec<_> = existing_sessions
.iter()
.filter(|s| s.created_at > chrono::Utc::now() - chrono::Duration::hours(24))
.collect();
if recent_sessions.len() >= 3 {
alerts.push("Multiple new devices detected in 24 hours".to_string());
}
for existing_session in &existing_sessions {
if let (Some(existing_loc), Some(new_loc)) = (&existing_session.location, &new_session.location) {
let distance = self.calculate_distance(existing_loc, new_loc);
if distance > 1000.0 { alerts.push(format!("Suspicious location change: {:.1} km distance", distance));
}
}
}
for existing_session in &existing_sessions {
let time_diff = new_session.created_at.signed_duration_since(existing_session.last_accessed_at);
if time_diff.num_minutes() < 60 && new_session.ip_address != existing_session.ip_address {
alerts.push("Impossible travel time detected".to_string());
}
}
Ok(alerts)
}
fn hash_token(&self, token: &str) -> Result<String> {
use sha2::{Sha256, Digest};
let mut hasher = Sha256::new();
hasher.update(token.as_bytes());
Ok(format!("{:x}", hasher.finalize()))
}
async fn revoke_oldest_session(&self, user_id: &Uuid) -> Result<()> {
let sessions = self.get_user_sessions(user_id).await?;
if let Some(oldest_session) = sessions.iter().min_by_key(|s| s.last_accessed_at) {
self.revoke_session(&oldest_session.id).await?;
println!("🔄 Revoked oldest session for user: {}", user_id);
}
Ok(())
}
fn detect_location(&self, ip_address: &str) -> Option<GeoLocation> {
if ip_address.starts_with("127.") || ip_address.starts_with("192.168.") {
Some(GeoLocation {
country: "Local".to_string(),
city: "Private Network".to_string(),
latitude: 0.0,
longitude: 0.0,
})
} else if ip_address.starts_with("8.8.") {
Some(GeoLocation {
country: "United States".to_string(),
city: "Mountain View".to_string(),
latitude: 37.4056,
longitude: -122.0775,
})
} else if ip_address.starts_with("208.67.") {
Some(GeoLocation {
country: "United States".to_string(),
city: "San Francisco".to_string(),
latitude: 37.7749,
longitude: -122.4194,
})
} else {
Some(GeoLocation {
country: "Unknown".to_string(),
city: "Unknown".to_string(),
latitude: 0.0,
longitude: 0.0,
})
}
}
fn calculate_distance(&self, loc1: &GeoLocation, loc2: &GeoLocation) -> f64 {
let r = 6371.0;
let lat1_rad = loc1.latitude.to_radians();
let lat2_rad = loc2.latitude.to_radians();
let delta_lat = (loc2.latitude - loc1.latitude).to_radians();
let delta_lon = (loc2.longitude - loc1.longitude).to_radians();
let a = (delta_lat / 2.0).sin().powi(2) +
lat1_rad.cos() * lat2_rad.cos() *
(delta_lon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
r * c
}
}
pub struct SessionMonitor {
session_repository: Box<dyn SessionRepository + Send + Sync>,
}
impl SessionMonitor {
pub fn new(session_repository: Box<dyn SessionRepository + Send + Sync>) -> Self {
Self { session_repository }
}
pub async fn get_session_statistics(&self, user_id: &Uuid) -> Result<SessionStats> {
let sessions = self.session_repository.find_by_user_id(user_id).await?;
let total_sessions = sessions.len();
let web_sessions = sessions.iter().filter(|s| matches!(s.session_type, SessionType::Web)).count();
let mobile_sessions = sessions.iter().filter(|s| matches!(s.session_type, SessionType::Mobile)).count();
let desktop_sessions = sessions.iter().filter(|s| matches!(s.session_type, SessionType::Desktop)).count();
let api_sessions = sessions.iter().filter(|s| matches!(s.session_type, SessionType::Api)).count();
let cli_sessions = sessions.iter().filter(|s| matches!(s.session_type, SessionType::CLI)).count();
let unique_locations: std::collections::HashSet<String> = sessions
.iter()
.filter_map(|s| s.location.as_ref().map(|l| format!("{}, {}", l.city, l.country)))
.collect();
let unique_ips: std::collections::HashSet<String> = sessions
.iter()
.map(|s| s.ip_address.clone())
.collect();
let oldest_session = sessions.iter().min_by_key(|s| s.created_at);
let newest_session = sessions.iter().max_by_key(|s| s.created_at);
Ok(SessionStats {
total_sessions,
sessions_by_type: SessionTypeStats {
web: web_sessions,
mobile: mobile_sessions,
desktop: desktop_sessions,
api: api_sessions,
cli: cli_sessions,
},
unique_locations: unique_locations.len(),
unique_ips: unique_ips.len(),
oldest_session: oldest_session.map(|s| s.created_at),
newest_session: newest_session.map(|s| s.created_at),
})
}
}
#[derive(Debug, Serialize)]
pub struct SessionStats {
pub total_sessions: usize,
pub sessions_by_type: SessionTypeStats,
pub unique_locations: usize,
pub unique_ips: usize,
pub oldest_session: Option<chrono::DateTime<chrono::Utc>>,
pub newest_session: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug, Serialize)]
pub struct SessionTypeStats {
pub web: usize,
pub mobile: usize,
pub desktop: usize,
pub api: usize,
pub cli: usize,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Backbone Auth Session Management Examples ===\n");
let session_repo = Box::new(InMemorySessionRepository::new());
let session_manager = SessionManager::new(session_repo).with_limits(3, 24);
let session_monitor = SessionMonitor::new(Box::new(InMemorySessionRepository::new()));
let test_user_id = Uuid::new_v4();
let auth_service = AuthService::new(AuthServiceConfig {
jwt_secret: "session_management_secret_change_in_production".to_string(),
token_expiry_hours: 24,
..Default::default()
})?;
println!("🔐 Creating Multiple Sessions");
println!("==============================");
let web_token = auth_service.generate_token(&test_user_id).await?;
let mobile_token = auth_service.generate_token(&test_user_id).await?;
let desktop_token = auth_service.generate_token(&test_user_id).await?;
let web_device_info = DeviceInfo {
device_id: "web_browser_123".to_string(),
user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36".to_string(),
ip_address: Some("192.168.1.100".to_string()),
fingerprint: Some("fp_web_client".to_string()),
};
let web_session_id = session_manager.create_session(
&test_user_id,
&web_token,
web_device_info,
"192.168.1.100",
SessionType::Web,
).await?;
let mobile_device_info = DeviceInfo {
device_id: "mobile_phone_456".to_string(),
user_agent: "MobileApp/1.0 (iOS)".to_string(),
ip_address: "8.8.8.8".to_string(),
fingerprint: Some("fp_mobile_client".to_string()),
};
let mobile_session_id = session_manager.create_session(
&test_user_id,
&mobile_token,
mobile_device_info,
"8.8.8.8",
SessionType::Mobile,
).await?;
let desktop_device_info = DeviceInfo {
device_id: "desktop_app_789".to_string(),
user_agent: "DesktopApp/1.0 (Windows)".to_string(),
ip_address: "208.67.222.222".to_string(),
fingerprint: Some("fp_desktop_client".to_string()),
};
let desktop_session_id = session_manager.create_session(
&test_user_id,
&desktop_token,
desktop_device_info,
"208.67.222.222",
SessionType::Desktop,
).await?;
println!("\n✅ Created 3 sessions:");
println!(" Web: {}", web_session_id);
println!(" Mobile: {}", mobile_session_id);
println!(" Desktop: {}", desktop_session_id);
println!();
println!("🔍 Session Validation");
println!("=====================");
let validation_results = vec![
("Web token", &web_token),
("Mobile token", &mobile_token),
("Desktop token", &desktop_token),
("Invalid token", "invalid_token_here"),
];
for (name, token) in validation_results {
match session_manager.validate_session(token).await {
Ok(Some(session)) => {
println!("✅ {}: Valid session ({})", name, session.session_type as u8);
println!(" Device: {}", session.device_info.device_id);
println!(" Location: {:?}", session.location.as_ref().map(|l| format!("{}, {}", l.city, l.country)));
}
Ok(None) => println!("❌ {}: Invalid or expired session", name),
Err(e) => println!("⚠️ {}: Error - {}", name, e),
}
println!();
}
println!("🔧 Session Management");
println!("=====================");
let user_sessions = session_manager.get_user_sessions(&test_user_id).await?;
println!("📱 Active sessions for user: {}", user_sessions.len());
for (i, session) in user_sessions.iter().enumerate() {
println!(" {}. {} ({})", i + 1, session.session_type as u8, session.ip_address);
println!(" Created: {}", session.created_at.format("%Y-%m-%d %H:%M:%S UTC"));
println!(" Last accessed: {}", session.last_accessed_at.format("%Y-%m-%d %H:%M:%S UTC"));
println!(" Expires: {}", session.expires_at.format("%Y-%m-%d %H:%M:%S UTC"));
}
println!();
println!("🚫 Session Revocation");
println!("=====================");
println!("Revoking mobile session...");
session_manager.revoke_session(&mobile_session_id).await?;
match session_manager.validate_session(&mobile_token).await {
Ok(Some(_)) => println!("❌ Mobile session still active (unexpected)"),
Ok(None) => println!("✅ Mobile session successfully revoked"),
Err(_) => println!("⚠️ Error validating mobile session"),
}
println!();
println!("🔄 Session Rotation");
println!("===================");
let api_token = auth_service.generate_token(&test_user_id).await?;
let api_device_info = DeviceInfo {
device_id: "api_client_999".to_string(),
user_agent: "APIClient/1.0".to_string(),
ip_address: "127.0.0.1".to_string(),
fingerprint: Some("fp_api_client".to_string()),
};
let api_session_id = session_manager.create_session(
&test_user_id,
&api_token,
api_device_info,
"127.0.0.1",
SessionType::Api,
).await?;
println!("Created API session: {}", api_session_id);
let active_sessions = session_manager.get_user_sessions(&test_user_id).await?;
println!("Active sessions after rotation: {}", active_sessions.len());
for session in &active_sessions {
println!(" - {} ({})", session.session_type as u8, session.ip_address);
}
println!();
println!("🚨 Suspicious Activity Detection");
println!("================================");
let suspicious_token = auth_service.generate_token(&test_user_id).await?;
let suspicious_device_info = DeviceInfo {
device_id: "unknown_device_001".to_string(),
user_agent: "SuspiciousBot/1.0".to_string(),
ip_address: "203.0.113.100".to_string(), fingerprint: None,
};
let suspicious_session = Session {
id: Uuid::new_v4().to_string(),
user_id: test_user_id,
token_hash: session_manager.hash_token(&suspicious_token)?,
device_info: suspicious_device_info,
ip_address: "203.0.113.100".to_string(),
user_agent: "SuspiciousBot/1.0".to_string(),
created_at: chrono::Utc::now(),
last_accessed_at: chrono::Utc::now(),
expires_at: chrono::Utc::now() + chrono::Duration::hours(24),
is_active: true,
session_type: SessionType::Web,
location: Some(GeoLocation {
country: "Unknown Country".to_string(),
city: "Unknown City".to_string(),
latitude: 0.0,
longitude: 0.0,
}),
};
let alerts = session_manager.detect_suspicious_activity(&test_user_id, &suspicious_session).await?;
if alerts.is_empty() {
println!("✅ No suspicious activity detected");
} else {
println!("🚨 Suspicious activity detected:");
for alert in alerts {
println!(" - {}", alert);
}
}
println!();
println!("📊 Session Statistics");
println!("=====================");
let stats = session_monitor.get_session_statistics(&test_user_id).await?;
println!("Session Statistics for User: {}", test_user_id);
println!("Total Sessions: {}", stats.total_sessions);
println!("By Type:");
println!(" Web: {}", stats.sessions_by_type.web);
println!(" Mobile: {}", stats.sessions_by_type.mobile);
println!(" Desktop: {}", stats.sessions_by_type.desktop);
println!(" API: {}", stats.sessions_by_type.api);
println!(" CLI: {}", stats.sessions_by_type.cli);
println!("Unique Locations: {}", stats.unique_locations);
println!("Unique IP Addresses: {}", stats.unique_ips);
if let Some(oldest) = stats.oldest_session {
println!("Oldest Session: {}", oldest.format("%Y-%m-%d %H:%M:%S UTC"));
}
if let Some(newest) = stats.newest_session {
println!("Newest Session: {}", newest.format("%Y-%m-%d %H:%M:%S UTC"));
}
println!();
println!("🧹 Session Cleanup");
println!("==================");
let cleaned_count = session_manager.cleanup_expired_sessions().await?;
println!("Cleaned up {} expired sessions", cleaned_count);
let remaining_sessions = session_manager.get_user_sessions(&test_user_id).await?;
println!("Remaining active sessions: {}", remaining_sessions.len());
println!();
println!("🔧 Advanced Session Features");
println!("============================");
println!("📋 Session Management Best Practices:");
println!("✅ Implement session timeout and cleanup");
println!("✅ Limit concurrent sessions per user");
println!("✅ Detect and alert on suspicious activity");
println!("✅ Support session revocation and rotation");
println!("✅ Track device information and geolocation");
println!("✅ Implement impossible travel detection");
println!("✅ Provide session audit logs");
println!();
println!("🛡️ Security Features:");
println!("🔒 Token hashing for secure storage");
println!("🌍 Geolocation tracking");
println!("📱 Device fingerprinting");
println!("⏰ Automatic session expiration");
println!("🚫 Manual session revocation");
println!("🔍 Suspicious activity detection");
println!();
println!("💡 Use Cases:");
println!("👤 User profile management");
println!("📱 Multi-device login management");
println!("🚨 Security incident response");
println!("📊 Session analytics and monitoring");
println!("🔐 Enterprise compliance requirements");
println!();
println!("=== Session Management Examples Complete ===");
println!("🎉 All session management features demonstrated!");
println!("\n📚 Next Steps:");
println!("1. Integrate with your authentication system");
println!("2. Choose appropriate storage backend (Redis/Database)");
println!("3. Configure session limits and timeouts");
println!("4. Set up monitoring and alerting");
println!("5. Implement session cleanup jobs");
println!("6. Add comprehensive audit logging");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_session_creation() -> Result<()> {
let session_repo = Box::new(InMemorySessionRepository::new());
let session_manager = SessionManager::new(session_repo);
let user_id = Uuid::new_v4();
let token = "test_token_123";
let device_info = DeviceInfo {
device_id: "test_device".to_string(),
user_agent: "Test-Agent/1.0".to_string(),
ip_address: Some("127.0.0.1".to_string()),
fingerprint: Some("test_fingerprint".to_string()),
};
let session_id = session_manager.create_session(
&user_id,
token,
device_info,
"127.0.0.1",
SessionType::Web,
).await?;
assert!(!session_id.is_empty());
let session = session_manager.validate_session(token).await?;
assert!(session.is_some());
assert_eq!(session.unwrap().user_id, user_id);
Ok(())
}
#[tokio::test]
async fn test_session_revocation() -> Result<()> {
let session_repo = Box::new(InMemorySessionRepository::new());
let session_manager = SessionManager::new(session_repo);
let user_id = Uuid::new_v4();
let token = "test_token_456";
let device_info = DeviceInfo {
device_id: "test_device_2".to_string(),
user_agent: "Test-Agent/1.0".to_string(),
ip_address: Some("127.0.0.1".to_string()),
fingerprint: Some("test_fingerprint".to_string()),
};
let session_id = session_manager.create_session(
&user_id,
token,
device_info,
"127.0.0.1",
SessionType::Web,
).await?;
let session = session_manager.validate_session(token).await?;
assert!(session.is_some());
session_manager.revoke_session(&session_id).await?;
let session = session_manager.validate_session(token).await?;
assert!(session.is_none());
Ok(())
}
#[tokio::test]
async fn test_multiple_sessions() -> Result<()> {
let session_repo = Box::new(InMemorySessionRepository::new());
let session_manager = SessionManager::new(session_repo).with_limits(2, 24);
let user_id = Uuid::new_v4();
let device_info = DeviceInfo {
device_id: "test_device".to_string(),
user_agent: "Test-Agent/1.0".to_string(),
ip_address: Some("127.0.0.1".to_string()),
fingerprint: Some("test_fingerprint".to_string()),
};
let _session1_id = session_manager.create_session(
&user_id,
"token1",
device_info.clone(),
"127.0.0.1",
SessionType::Web,
).await?;
let _session2_id = session_manager.create_session(
&user_id,
"token2",
device_info.clone(),
"127.0.0.1",
SessionType::Mobile,
).await?;
let _session3_id = session_manager.create_session(
&user_id,
"token3",
device_info,
"127.0.0.1",
SessionType::Desktop,
).await?;
let active_sessions = session_manager.get_user_sessions(&user_id).await?;
assert_eq!(active_sessions.len(), 2);
Ok(())
}
#[tokio::test]
async fn test_session_statistics() -> Result<()> {
let session_repo = Box::new(InMemorySessionRepository::new());
let session_manager = SessionManager::new(session_repo);
let session_monitor = SessionMonitor::new(Box::new(InMemorySessionRepository::new()));
let user_id = Uuid::new_v4();
let device_info = DeviceInfo {
device_id: "test_device".to_string(),
user_agent: "Test-Agent/1.0".to_string(),
ip_address: Some("127.0.0.1".to_string()),
fingerprint: Some("test_fingerprint".to_string()),
};
let _web_session = session_manager.create_session(
&user_id,
"token1",
device_info.clone(),
"127.0.0.1",
SessionType::Web,
).await?;
let _mobile_session = session_manager.create_session(
&user_id,
"token2",
device_info.clone(),
"8.8.8.8",
SessionType::Mobile,
).await?;
let _desktop_session = session_manager.create_session(
&user_id,
"token3",
device_info,
"208.67.222.222",
SessionType::Desktop,
).await?;
let stats = session_monitor.get_session_statistics(&user_id).await?;
assert_eq!(stats.total_sessions, 3);
assert_eq!(stats.sessions_by_type.web, 1);
assert_eq!(stats.sessions_by_type.mobile, 1);
assert_eq!(stats.sessions_by_type.desktop, 1);
assert!(stats.unique_locations > 0);
assert!(stats.unique_ips > 0);
Ok(())
}
#[test]
fn test_token_hashing() -> Result<()> {
let session_repo = Box::new(InMemorySessionRepository::new());
let session_manager = SessionManager::new(session_repo);
let token = "test_token_hashing";
let hash1 = session_manager.hash_token(token)?;
let hash2 = session_manager.hash_token(token)?;
assert_eq!(hash1, hash2);
assert_ne!(hash1, token); assert!(hash1.len() == 64);
Ok(())
}
#[test]
fn test_distance_calculation() -> Result<()> {
let session_repo = Box::new(InMemorySessionRepository::new());
let session_manager = SessionManager::new(session_repo);
let loc1 = GeoLocation {
country: "US".to_string(),
city: "New York".to_string(),
latitude: 40.7128,
longitude: -74.0060,
};
let loc2 = GeoLocation {
country: "US".to_string(),
city: "Los Angeles".to_string(),
latitude: 34.0522,
longitude: -118.2437,
};
assert!(loc1.latitude > 0.0);
assert!(loc2.latitude > 0.0);
assert!(loc1.longitude < 0.0);
assert!(loc2.longitude < 0.0);
Ok(())
}
}