communitas_core/encrypted_storage/
session.rs1use serde::{Deserialize, Serialize};
7use std::time::{Duration, SystemTime, UNIX_EPOCH};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Session {
12 pub id: String,
13 pub four_words: String,
14 pub display_name: String,
15 pub created_at: u64,
16 pub last_activity: u64,
17 pub expires_at: u64,
18 pub auth_method: AuthMethod,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub enum AuthMethod {
24 Password,
25 PasswordOnly, Passkey,
27 Combined, }
29
30impl Session {
31 pub fn new(four_words: String, display_name: String, timeout_seconds: u64) -> Self {
33 let now = current_timestamp();
34 Self {
35 id: generate_session_id(),
36 four_words,
37 display_name,
38 created_at: now,
39 last_activity: now,
40 expires_at: now + timeout_seconds,
41 auth_method: AuthMethod::Password,
42 }
43 }
44
45 pub fn new_password_only(
47 four_words: String,
48 display_name: String,
49 timeout_seconds: u64,
50 ) -> Self {
51 let mut session = Self::new(four_words, display_name, timeout_seconds);
52 session.auth_method = AuthMethod::PasswordOnly;
53 session
54 }
55
56 pub fn is_expired(&self) -> bool {
58 current_timestamp() > self.expires_at
59 }
60
61 pub fn touch(&mut self) {
63 self.last_activity = current_timestamp();
64 }
65
66 pub fn extend(&mut self, additional_seconds: u64) {
68 self.expires_at = current_timestamp() + additional_seconds;
69 self.touch();
70 }
71
72 pub fn time_remaining(&self) -> Duration {
74 let now = current_timestamp();
75 if now >= self.expires_at {
76 Duration::from_secs(0)
77 } else {
78 Duration::from_secs(self.expires_at - now)
79 }
80 }
81}
82
83pub struct SessionManager {
85 sessions: std::sync::Arc<tokio::sync::RwLock<Vec<Session>>>,
86 max_sessions: usize,
87}
88
89impl SessionManager {
90 pub fn new(max_sessions: usize) -> Self {
92 Self {
93 sessions: std::sync::Arc::new(tokio::sync::RwLock::new(Vec::new())),
94 max_sessions,
95 }
96 }
97
98 pub async fn add_session(&self, session: Session) -> Result<String, anyhow::Error> {
100 let mut sessions = self.sessions.write().await;
101
102 if let Some(existing) = sessions
104 .iter_mut()
105 .find(|s| s.four_words == session.four_words)
106 {
107 existing.extend(session.expires_at - session.created_at);
109 return Ok(existing.id.clone());
110 }
111
112 if sessions.len() >= self.max_sessions {
114 sessions.sort_by_key(|s| s.created_at);
116 sessions.remove(0);
117 }
118
119 let session_id = session.id.clone();
120 sessions.push(session);
121
122 Ok(session_id)
123 }
124
125 pub async fn get_session(&self, session_id: &str) -> Option<Session> {
127 let sessions = self.sessions.read().await;
128 sessions
129 .iter()
130 .find(|s| s.id == session_id && !s.is_expired())
131 .cloned()
132 }
133
134 pub async fn get_active_sessions(&self) -> Vec<Session> {
136 let sessions = self.sessions.read().await;
137 sessions
138 .iter()
139 .filter(|s| !s.is_expired())
140 .cloned()
141 .collect()
142 }
143
144 pub async fn remove_session(&self, session_id: &str) -> bool {
146 let mut sessions = self.sessions.write().await;
147 if let Some(pos) = sessions.iter().position(|s| s.id == session_id) {
148 sessions.remove(pos);
149 true
150 } else {
151 false
152 }
153 }
154
155 pub async fn cleanup_expired(&self) -> usize {
157 let mut sessions = self.sessions.write().await;
158 let original_len = sessions.len();
159 sessions.retain(|s| !s.is_expired());
160 original_len - sessions.len()
161 }
162
163 pub async fn switch_session(&self, session_id: &str) -> Option<Session> {
165 let mut sessions = self.sessions.write().await;
166 if let Some(session) = sessions.iter_mut().find(|s| s.id == session_id)
167 && !session.is_expired()
168 {
169 session.touch();
170 return Some(session.clone());
171 }
172 None
173 }
174}
175
176pub struct SessionStorage {
178 storage_path: std::path::PathBuf,
179}
180
181impl SessionStorage {
182 pub fn new(base_path: &std::path::Path) -> Self {
183 let storage_path = base_path.join("sessions.enc");
184 Self { storage_path }
185 }
186
187 pub async fn save_sessions(&self, sessions: &[Session]) -> anyhow::Result<()> {
189 let active_sessions: Vec<_> = sessions
191 .iter()
192 .filter(|s| !s.is_expired())
193 .cloned()
194 .collect();
195
196 let json = serde_json::to_vec(&active_sessions)?;
197
198 tokio::fs::write(&self.storage_path, json).await?;
200
201 Ok(())
202 }
203
204 pub async fn load_sessions(&self) -> anyhow::Result<Vec<Session>> {
206 if !self.storage_path.exists() {
207 return Ok(Vec::new());
208 }
209
210 let data = tokio::fs::read(&self.storage_path).await?;
211
212 let sessions: Vec<Session> = serde_json::from_slice(&data)?;
214
215 Ok(sessions.into_iter().filter(|s| !s.is_expired()).collect())
217 }
218
219 pub async fn clear(&self) -> anyhow::Result<()> {
221 if self.storage_path.exists() {
222 tokio::fs::remove_file(&self.storage_path).await?;
223 }
224 Ok(())
225 }
226}
227
228fn current_timestamp() -> u64 {
229 SystemTime::now()
230 .duration_since(UNIX_EPOCH)
231 .map(|d| d.as_secs())
232 .unwrap_or(0) }
234
235fn generate_session_id() -> String {
236 use rand::{Rng, SeedableRng};
237 let mut rng = rand::rngs::StdRng::from_entropy();
238 let random_bytes: Vec<u8> = (0..16).map(|_| rng.r#gen::<u8>()).collect();
239 hex::encode(random_bytes)
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn test_session_creation() {
248 let session = Session::new(
249 "test-four-words".to_string(),
250 "Test User".to_string(),
251 300, );
253
254 assert!(!session.is_expired());
255 assert_eq!(session.four_words, "test-four-words");
256 assert_eq!(session.display_name, "Test User");
257 }
258
259 #[test]
260 fn test_session_expiration() {
261 let mut session = Session::new(
262 "test-four-words".to_string(),
263 "Test User".to_string(),
264 1, );
266
267 assert!(!session.is_expired());
268
269 session.expires_at = current_timestamp() - 10;
271 assert!(session.is_expired());
272 }
273
274 #[tokio::test]
275 async fn test_session_manager() {
276 let manager = SessionManager::new(3);
277
278 let session1 = Session::new("user1".to_string(), "User 1".to_string(), 300);
280 let session2 = Session::new("user2".to_string(), "User 2".to_string(), 300);
281 let session3 = Session::new("user3".to_string(), "User 3".to_string(), 300);
282
283 let id1 = manager.add_session(session1).await.unwrap();
284 let id2 = manager.add_session(session2).await.unwrap();
285 let _id3 = manager.add_session(session3).await.unwrap();
286
287 let active = manager.get_active_sessions().await;
289 assert_eq!(active.len(), 3);
290
291 let switched = manager.switch_session(&id2).await;
293 assert!(switched.is_some());
294 assert_eq!(switched.unwrap().four_words, "user2");
295
296 assert!(manager.remove_session(&id1).await);
298 let active_after = manager.get_active_sessions().await;
299 assert_eq!(active_after.len(), 2);
300 }
301
302 #[tokio::test]
303 async fn test_session_storage() {
304 use tempfile::TempDir;
305
306 let temp_dir = TempDir::new().unwrap();
307 let storage = SessionStorage::new(temp_dir.path());
308
309 let sessions = vec![
310 Session::new("user1".to_string(), "User 1".to_string(), 300),
311 Session::new("user2".to_string(), "User 2".to_string(), 300),
312 ];
313
314 storage.save_sessions(&sessions).await.unwrap();
316
317 let loaded = storage.load_sessions().await.unwrap();
319 assert_eq!(loaded.len(), 2);
320
321 storage.clear().await.unwrap();
323 let after_clear = storage.load_sessions().await.unwrap();
324 assert_eq!(after_clear.len(), 0);
325 }
326}