// Session token management — mirrors k_proxy_app.py session logic. // Tokens are 32-byte hex strings; stored in memory only. import 'dart:math'; class SessionEntry { final String username; final DateTime expires; SessionEntry({required this.username, required this.expires}); } class SessionManager { final Map _sessions = {}; static const int ttlSeconds = 300; static const Duration _ttl = Duration(seconds: ttlSeconds); /// Issue a new session token for [username]. /// _purgeExpired is only called here, not on every lookup, so tokens accumulate /// until the next login — acceptable for the low-traffic embedded use case. String issue(String username) { _purgeExpired(); final token = _randomToken(); _sessions[token] = SessionEntry( username: username, expires: DateTime.now().add(_ttl), ); return token; } /// Returns the session entry for [token], or null if missing/expired. SessionEntry? getSession(String token) { final s = _sessions[token]; if (s == null) return null; if (DateTime.now().isAfter(s.expires)) { _sessions.remove(token); return null; } return s; } /// Returns true if [token] is known and not expired. bool isValid(String token) => getSession(token) != null; /// Revoke [token] immediately. void revoke(String token) => _sessions.remove(token); /// Returns true if at least one session is currently active (not expired). /// Used by gated-proxy forwarding: personal-device model means any live /// login counts as authorisation for the proxied request. bool hasAnyActiveSession() { _purgeExpired(); return _sessions.isNotEmpty; } /// Returns the token and session entry of any currently active session. /// Used by /auth/get-token to return an existing token without card interaction. (String token, SessionEntry session)? anyActive() { _purgeExpired(); if (_sessions.isEmpty) return null; final e = _sessions.entries.first; return (e.key, e.value); } /// Revoke all sessions for [username]. void revokeAll(String username) { _sessions.removeWhere((_, s) => s.username == username); } void _purgeExpired() { final now = DateTime.now(); _sessions.removeWhere((_, s) => now.isAfter(s.expires)); } String _randomToken() { final rng = Random.secure(); final bytes = List.generate(32, (_) => rng.nextInt(256)); return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); } }