added support for multiple accounts with an account switcher in settings
This commit is contained in:
@@ -19,6 +19,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(),
|
||||
@@ -81,8 +90,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;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@ import '../queries/auth_verify/auth_verify.dart';
|
||||
|
||||
/// Credential probe. A server-side password rotation forces a re-login on the
|
||||
/// next cold start even when the bearer token would still be accepted.
|
||||
/// 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
|
||||
@@ -24,18 +25,18 @@ class SessionValidator {
|
||||
}
|
||||
} on AuthException catch (e) {
|
||||
if (e.statusCode != 401) return;
|
||||
// The probed account already signed out; the 401 must not sign out
|
||||
// whoever logged in meanwhile.
|
||||
// The probed account is no longer the active one; the 401 must not
|
||||
// sign out whoever took over meanwhile.
|
||||
if (!SessionManager().isCurrentSession(epoch)) 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 '
|
||||
'wurde dein Passwort geändert. 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);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../auth/token_storage.dart';
|
||||
import '../../marianumconnect_api.dart';
|
||||
import '../../marianumconnect_endpoint.dart';
|
||||
import '../../marianumconnect_query.dart';
|
||||
|
||||
/// Revokes the stored MC bearer token both server-side and locally. Best-effort
|
||||
@@ -24,4 +26,17 @@ class AuthLogout extends MarianumConnectQuery {
|
||||
await _tokenStorage.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Revokes the token of an inactive account; the stored (active) token is
|
||||
/// left alone. Best-effort.
|
||||
static Future<void> revoke(String token) async {
|
||||
try {
|
||||
await MarianumConnectApi.plainDio().post<void>(
|
||||
MarianumConnectEndpoint.resolve('auth/logout'),
|
||||
options: Options(headers: {'Authorization': 'Bearer $token'}),
|
||||
);
|
||||
} on DioException catch (_) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../../errors/auth_exception.dart';
|
||||
import '../../auth/token_storage.dart';
|
||||
import '../../marianumconnect_api.dart';
|
||||
import '../../marianumconnect_query.dart';
|
||||
import '../auth_login/auth_login_response.dart';
|
||||
|
||||
/// Reads the user behind the stored bearer token, which also probes that the
|
||||
/// token is still accepted.
|
||||
///
|
||||
/// Bypasses the shared dio singleton so the auth interceptor does not react
|
||||
/// to the 401 this probe is meant to observe.
|
||||
class AuthMe extends MarianumConnectQuery {
|
||||
final MarianumConnectTokenStorage _tokenStorage;
|
||||
|
||||
AuthMe({
|
||||
MarianumConnectTokenStorage tokenStorage =
|
||||
const MarianumConnectTokenStorage(),
|
||||
Dio? dio,
|
||||
}) : _tokenStorage = tokenStorage,
|
||||
super(dio: dio ?? MarianumConnectApi.plainDio());
|
||||
|
||||
/// Throws [AuthException] when the token is missing or rejected.
|
||||
Future<void> run() async {
|
||||
await user();
|
||||
}
|
||||
|
||||
/// The signed-in user (names, type). Throws like [run].
|
||||
Future<AuthLoginUser> user() async {
|
||||
final options = await _tokenStorage.requireBearerOptions('AuthMe');
|
||||
return guard(() async {
|
||||
final response = await dio.get<Map<String, dynamic>>(
|
||||
endpoint('auth/me'),
|
||||
options: options,
|
||||
);
|
||||
return AuthLoginUser.fromJson(response.data!);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user