80 lines
2.5 KiB
Dart
80 lines
2.5 KiB
Dart
// 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<String, SessionEntry> _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();
|
|
}
|
|
}
|