added support for multiple accounts, guardian login bugfixes, ui changes

This commit is contained in:
2026-09-23 20:50:12 +02:00
parent 630497abdd
commit 84098af7e2
37 changed files with 1759 additions and 376 deletions
@@ -20,6 +20,15 @@ class MarianumConnectAuthInterceptor extends Interceptor {
// each spawning a fresh row in api_tokens.
Future<bool>? _pendingReLogin;
static Future<bool>? _anyPendingReLogin;
/// Resolves once no silent re-login is running. An account switch waits for
/// it — the renewed token would otherwise land in the next account's slot.
static Future<void> idle() async {
final pending = _anyPendingReLogin;
if (pending != null) await pending;
}
MarianumConnectAuthInterceptor({
MarianumConnectTokenStorage tokenStorage =
const MarianumConnectTokenStorage(),
@@ -85,8 +94,10 @@ class MarianumConnectAuthInterceptor extends Interceptor {
if (inFlight != null) return inFlight;
final fresh = _performReLogin();
_pendingReLogin = fresh;
_anyPendingReLogin = fresh;
fresh.whenComplete(() {
if (identical(_pendingReLogin, fresh)) _pendingReLogin = null;
if (identical(_anyPendingReLogin, fresh)) _anyPendingReLogin = null;
});
return fresh;
}
@@ -10,10 +10,10 @@ import '../queries/auth_verify/auth_verify.dart';
/// Credential probe. For password accounts a server-side password rotation
/// forces a re-login on the next cold start even when the bearer token would
/// still be accepted; for guardians it confirms a rejected token before the
/// session is dropped.
/// session is dropped. Another stored account then takes over.
class SessionValidator {
static Future<void> probeStored({
required Future<void> Function() onInvalidated,
required Future<void> Function(String? nextAccountId) onInvalidated,
}) async {
final session = SessionManager().current;
// The probes use their own dio (bypassing the demo interceptor), so a demo
@@ -29,7 +29,7 @@ class SessionValidator {
} on AuthException catch (e) {
if (e.statusCode != 401) return;
log('MC: stored session rejected — forcing re-login');
await SessionLifecycle.signOut(
final next = await SessionLifecycle.signOut(
notice: switch (session) {
CredentialSession() =>
'Deine Zugangsdaten wurden vom Server abgelehnt. Vermutlich '
@@ -38,7 +38,7 @@ class SessionValidator {
'Deine Anmeldung ist abgelaufen. Bitte melde dich erneut an.',
},
);
await onInvalidated();
await onInvalidated(next);
} catch (e) {
log('MC: background session check failed (transient): $e');
}
@@ -60,6 +60,26 @@ class MarianumConnectTokenStorage {
);
}
static const bearerKey = _tokenKey;
static const fieldKeys = [_tokenKey, _tokenIdKey, _expiresAtKey];
/// Raw stored fields, for parking the token of an inactive account.
Future<Map<String, String>> readAll() async => {
for (final key in fieldKeys) key: ?await _storage.read(key: key),
};
/// Restores fields from [readAll]; missing ones are deleted.
Future<void> writeAll(Map<String, String> fields) async {
for (final key in fieldKeys) {
final value = fields[key];
if (value == null) {
await _storage.delete(key: key);
} else {
await _storage.write(key: key, value: value);
}
}
}
Future<void> clear() async {
await _storage.delete(key: _tokenKey);
await _storage.delete(key: _tokenIdKey);