1use std::collections::HashMap;
7use std::sync::Arc;
8use std::time::{Duration, Instant};
9
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use tokio::sync::RwLock;
13
14#[derive(Debug, Clone)]
20pub struct SessionRecord {
21 pub session_id: String,
22 pub user_id: String,
23 pub username: String,
24 pub role: String,
25 pub created_at: Instant,
26 pub last_activity: Instant,
27 pub absolute_expiry: Instant,
28}
29
30#[derive(Debug, Clone)]
32pub struct SessionConfig {
33 pub idle_timeout: Duration,
34 pub absolute_timeout: Duration,
35 pub max_parallel_sessions: usize,
36 pub renewal_window: Duration,
37}
38
39impl Default for SessionConfig {
40 fn default() -> Self {
41 Self {
42 idle_timeout: Duration::from_secs(30 * 60), absolute_timeout: Duration::from_secs(24 * 3600), max_parallel_sessions: 5,
45 renewal_window: Duration::from_secs(5 * 60), }
47 }
48}
49
50#[derive(Debug)]
52pub struct SessionManager {
53 sessions: HashMap<String, SessionRecord>,
54 config: SessionConfig,
55}
56
57pub type SharedSessionManager = Arc<RwLock<SessionManager>>;
58
59impl SessionManager {
64 pub fn new(config: SessionConfig) -> Self {
66 Self {
67 sessions: HashMap::new(),
68 config,
69 }
70 }
71
72 pub fn create_session(&mut self, user_id: &str, username: &str, role: &str) -> SessionRecord {
74 let now = Instant::now();
75
76 let user_sessions: Vec<String> = self
78 .sessions
79 .iter()
80 .filter(|(_, s)| s.user_id == user_id)
81 .map(|(id, _)| id.clone())
82 .collect();
83
84 if user_sessions.len() >= self.config.max_parallel_sessions {
86 let mut sessions_with_time: Vec<_> = user_sessions
87 .iter()
88 .filter_map(|id| self.sessions.get(id).map(|s| (id.clone(), s.created_at)))
89 .collect();
90 sessions_with_time.sort_by_key(|(_, t)| *t);
91
92 let to_remove = user_sessions.len() - self.config.max_parallel_sessions + 1;
94 for (id, _) in sessions_with_time.iter().take(to_remove) {
95 self.sessions.remove(id);
96 }
97 }
98
99 let session = SessionRecord {
100 session_id: uuid::Uuid::new_v4().to_string(),
101 user_id: user_id.to_string(),
102 username: username.to_string(),
103 role: role.to_string(),
104 created_at: now,
105 last_activity: now,
106 absolute_expiry: now + self.config.absolute_timeout,
107 };
108
109 self.sessions
110 .insert(session.session_id.clone(), session.clone());
111 session
112 }
113
114 pub fn validate_session(&mut self, session_id: &str) -> Option<&SessionRecord> {
116 let now = Instant::now();
117 let config = self.config.clone();
118
119 let session = self.sessions.get_mut(session_id)?;
120
121 if now >= session.absolute_expiry {
123 self.sessions.remove(session_id);
124 return None;
125 }
126
127 if now.duration_since(session.last_activity) > config.idle_timeout {
129 self.sessions.remove(session_id);
130 return None;
131 }
132
133 let session = self.sessions.get_mut(session_id)?;
135 session.last_activity = now;
136 Some(session)
137 }
138
139 pub fn needs_renewal(&self, session_id: &str) -> bool {
141 if let Some(session) = self.sessions.get(session_id) {
142 let now = Instant::now();
143 let time_remaining = session
144 .absolute_expiry
145 .checked_duration_since(now)
146 .unwrap_or(Duration::ZERO);
147 time_remaining <= self.config.renewal_window
148 } else {
149 false
150 }
151 }
152
153 pub fn revoke_session(&mut self, session_id: &str) -> bool {
155 self.sessions.remove(session_id).is_some()
156 }
157
158 pub fn revoke_all_user_sessions(&mut self, user_id: &str) -> usize {
160 let to_remove: Vec<String> = self
161 .sessions
162 .iter()
163 .filter(|(_, s)| s.user_id == user_id)
164 .map(|(id, _)| id.clone())
165 .collect();
166 let count = to_remove.len();
167 for id in to_remove {
168 self.sessions.remove(&id);
169 }
170 count
171 }
172
173 pub fn cleanup_expired(&mut self) -> usize {
175 let now = Instant::now();
176 let config = self.config.clone();
177 let before = self.sessions.len();
178 self.sessions.retain(|_, s| {
179 now < s.absolute_expiry && now.duration_since(s.last_activity) <= config.idle_timeout
180 });
181 before - self.sessions.len()
182 }
183
184 pub const fn session_config(&self) -> &SessionConfig {
186 &self.config
187 }
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct UserSummary {
196 pub id: String,
197 pub username: String,
198 pub display_name: String,
199 pub email: String,
200 pub role: String,
201 pub disabled: bool,
202 pub created_at: DateTime<Utc>,
203}
204
205pub fn hash_password(password: &str) -> Result<String, String> {
210 use argon2::password_hash::rand_core::OsRng;
211 use argon2::password_hash::SaltString;
212 use argon2::{Argon2, PasswordHasher};
213
214 let salt = SaltString::generate(&mut OsRng);
215 let argon2 = Argon2::default(); argon2
218 .hash_password(password.as_bytes(), &salt)
219 .map(|h| h.to_string())
220 .map_err(|e| format!("Password hashing failed: {e}"))
221}
222
223pub fn verify_password(password: &str, hash: &str) -> Result<bool, String> {
224 use argon2::password_hash::PasswordHash;
225 use argon2::{Argon2, PasswordVerifier};
226
227 let parsed_hash = PasswordHash::new(hash).map_err(|e| format!("Invalid password hash: {e}"))?;
228
229 Ok(Argon2::default()
230 .verify_password(password.as_bytes(), &parsed_hash)
231 .is_ok())
232}
233
234#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn test_session_lifecycle() {
244 let mut mgr = SessionManager::new(SessionConfig::default());
245 let session = mgr.create_session("user-1", "bob", "operator");
246 assert!(!session.session_id.is_empty());
247
248 assert!(mgr.validate_session(&session.session_id).is_some());
250
251 assert!(mgr.revoke_session(&session.session_id));
253 assert!(mgr.validate_session(&session.session_id).is_none());
254 }
255
256 #[test]
257 fn test_max_parallel_sessions() {
258 let config = SessionConfig {
259 max_parallel_sessions: 2,
260 ..Default::default()
261 };
262 let mut mgr = SessionManager::new(config);
263
264 let s1 = mgr.create_session("user-1", "carol", "viewer");
265 let s2 = mgr.create_session("user-1", "carol", "viewer");
266 let s3 = mgr.create_session("user-1", "carol", "viewer"); assert!(mgr.validate_session(&s1.session_id).is_none());
270 assert!(mgr.validate_session(&s2.session_id).is_some());
271 assert!(mgr.validate_session(&s3.session_id).is_some());
272 }
273
274 #[test]
275 fn test_revoke_all_user_sessions() {
276 let mut mgr = SessionManager::new(SessionConfig::default());
277 mgr.create_session("user-1", "alice", "admin");
278 mgr.create_session("user-1", "alice", "admin");
279 mgr.create_session("user-1", "alice", "admin");
280
281 let revoked = mgr.revoke_all_user_sessions("user-1");
282 assert_eq!(revoked, 3);
283 }
284
285 #[test]
286 fn test_password_hash_and_verify() {
287 let hash = hash_password("password123").unwrap();
288 assert!(verify_password("password123", &hash).unwrap());
289 assert!(!verify_password("wrong", &hash).unwrap());
290 }
291
292 #[test]
293 fn test_short_password_still_hashes() {
294 let hash = hash_password("short").unwrap();
296 assert!(verify_password("short", &hash).unwrap());
297 }
298}