diff --git a/core/archipelago/src/session.rs b/core/archipelago/src/session.rs index 80a60812..795f6ae2 100644 --- a/core/archipelago/src/session.rs +++ b/core/archipelago/src/session.rs @@ -2,7 +2,7 @@ use sha2::{Digest, Sha256}; use std::collections::HashMap; use std::net::IpAddr; use std::sync::Arc; -use std::time::Instant; +use std::time::SystemTime; use tokio::sync::RwLock; use zeroize::Zeroize; @@ -30,8 +30,8 @@ impl Drop for SessionType { #[derive(Clone)] struct Session { - created_at: Instant, - last_activity: Instant, + created_at: SystemTime, + last_activity: SystemTime, session_type: SessionType, } @@ -53,7 +53,7 @@ impl SessionStore { let token_bytes: [u8; 32] = rand::random(); let token = hex::encode(token_bytes); let hash = hash_token(&token); - let now = Instant::now(); + let now = SystemTime::now(); let session = Session { created_at: now, last_activity: now, @@ -72,7 +72,7 @@ impl SessionStore { let token_bytes: [u8; 32] = rand::random(); let token = hex::encode(token_bytes); let hash = hash_token(&token); - let now = Instant::now(); + let now = SystemTime::now(); let session = Session { created_at: now, last_activity: now, @@ -94,11 +94,11 @@ impl SessionStore { if !matches!(session.session_type, SessionType::Full) { return false; } - if session.last_activity.elapsed().as_secs() >= FULL_SESSION_TTL { + if session.last_activity.elapsed().unwrap_or_default().as_secs() >= FULL_SESSION_TTL { sessions.remove(&hash); return false; } - session.last_activity = Instant::now(); + session.last_activity = SystemTime::now(); true } else { false @@ -111,7 +111,7 @@ impl SessionStore { let hash = hash_token(token); let mut sessions = self.sessions.write().await; if let Some(session) = sessions.get_mut(&hash) { - if session.created_at.elapsed().as_secs() >= PENDING_SESSION_TTL { + if session.created_at.elapsed().unwrap_or_default().as_secs() >= PENDING_SESSION_TTL { sessions.remove(&hash); return None; } @@ -137,7 +137,7 @@ impl SessionStore { let mut sessions = self.sessions.write().await; if let Some(session) = sessions.get_mut(&hash) { session.session_type = SessionType::Full; - let now = Instant::now(); + let now = SystemTime::now(); session.created_at = now; session.last_activity = now; } @@ -163,7 +163,7 @@ impl SessionStore { let new_token_bytes: [u8; 32] = rand::random(); let new_token = hex::encode(new_token_bytes); let new_hash = hash_token(&new_token); - let now = Instant::now(); + let now = SystemTime::now(); let mut sessions = self.sessions.write().await; sessions.remove(&old_hash); @@ -183,9 +183,9 @@ impl SessionStore { let mut sessions = self.sessions.write().await; sessions.retain(|_, session| { match &session.session_type { - SessionType::Full => session.last_activity.elapsed().as_secs() < FULL_SESSION_TTL, + SessionType::Full => session.last_activity.elapsed().unwrap_or_default().as_secs() < FULL_SESSION_TTL, SessionType::PendingTotp { .. } => { - session.created_at.elapsed().as_secs() < PENDING_SESSION_TTL + session.created_at.elapsed().unwrap_or_default().as_secs() < PENDING_SESSION_TTL } } }); @@ -218,7 +218,7 @@ impl SessionStore { .values() .filter(|s| { matches!(s.session_type, SessionType::Full) - && s.last_activity.elapsed().as_secs() < FULL_SESSION_TTL + && s.last_activity.elapsed().unwrap_or_default().as_secs() < FULL_SESSION_TTL }) .count() } @@ -262,7 +262,7 @@ impl LoginRateLimiter { pub async fn check(&self, ip: IpAddr) -> bool { let mut attempts = self.attempts.write().await; - let now = Instant::now(); + let now = SystemTime::now(); let entry = attempts.entry(ip).or_default(); entry.retain(|t| now.duration_since(*t).as_secs() < WINDOW_SECS); entry.len() < MAX_ATTEMPTS @@ -271,7 +271,7 @@ impl LoginRateLimiter { pub async fn record_failure(&self, ip: IpAddr) { let mut attempts = self.attempts.write().await; let entry = attempts.entry(ip).or_default(); - entry.push(Instant::now()); + entry.push(SystemTime::now()); } } @@ -338,7 +338,7 @@ impl EndpointRateLimiter { let key = (method.to_string(), ip); let mut requests = self.requests.write().await; - let now = Instant::now(); + let now = SystemTime::now(); let entry = requests.entry(key).or_default(); entry.retain(|t| now.duration_since(*t).as_secs() < window); entry.len() < max_req @@ -352,13 +352,13 @@ impl EndpointRateLimiter { let key = (method.to_string(), ip); let mut requests = self.requests.write().await; let entry = requests.entry(key).or_default(); - entry.push(Instant::now()); + entry.push(SystemTime::now()); } /// Periodic cleanup of expired entries. pub async fn cleanup(&self) { let mut requests = self.requests.write().await; - let now = Instant::now(); + let now = SystemTime::now(); requests.retain(|(method, _), timestamps| { let window = self .limits diff --git a/loop/plan.md b/loop/plan.md index 138584b5..d7fee15d 100644 --- a/loop/plan.md +++ b/loop/plan.md @@ -72,7 +72,7 @@ ## Phase 6: Backend Critical Fixes -- [ ] **Fix session TTL clock bug — use SystemTime instead of Instant**: Read `core/archipelago/src/session.rs`. Find where `Instant::now()` is used for session TTL/expiry (around line 97). `Instant` is monotonic but can drift on sleep/hibernate — common on NUC/Pi hardware. Replace with `SystemTime::now()` for absolute time comparison. The `FULL_SESSION_TTL` (24 hours) and `PENDING_TOTP_TTL` (5 minutes) checks should use `SystemTime::elapsed()` or store `SystemTime` timestamps and compare with `SystemTime::now()`. Run `cargo test --all-features` in `core/` on the dev server. +- [x] **Fix session TTL clock bug — use SystemTime instead of Instant**: Read `core/archipelago/src/session.rs`. Find where `Instant::now()` is used for session TTL/expiry (around line 97). `Instant` is monotonic but can drift on sleep/hibernate — common on NUC/Pi hardware. Replace with `SystemTime::now()` for absolute time comparison. The `FULL_SESSION_TTL` (24 hours) and `PENDING_TOTP_TTL` (5 minutes) checks should use `SystemTime::elapsed()` or store `SystemTime` timestamps and compare with `SystemTime::now()`. Run `cargo test --all-features` in `core/` on the dev server. - [ ] **Enforce RBAC in RPC handler**: Read `core/archipelago/src/auth.rs` — find the `UserRole` enum and `can_access()` method. Then read `core/archipelago/src/api/rpc/mod.rs` — find where authenticated requests are dispatched to handlers. Add a role check before dispatching: after validating the session, get the user's role, call `role.can_access(method_name)`, and return an authorization error if denied. For now, all users created via onboarding should default to `Admin` role (single-user system), but this lays the groundwork for multi-user. Run `cargo clippy --all-targets --all-features && cargo test --all-features` on the dev server.