Files
Client/lib/api/connect/connect_auth_store.dart
T

110 lines
3.4 KiB
Dart

import 'dart:async';
import 'dart:developer';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../../model/account_data.dart';
import 'auth/login/login.dart';
import 'auth/login/login_request.dart';
import 'auth/login/login_response.dart';
import 'errors/connect_exception.dart';
/// Holds the Bearer token issued by `POST /auth/login` so that subsequent
/// RMV calls can attach it without prompting the user. Uses the LDAP
/// credentials already kept in [AccountData], so this is transparent to the
/// user — no extra login UI.
class ConnectAuthStore {
static const _tokenKey = 'connect_bearer_token';
static const _expiresAtKey = 'connect_token_expires_at';
static const _tokenName = 'MarianumMobile App';
static const _expiryGuard = Duration(minutes: 1);
static final ConnectAuthStore _instance = ConnectAuthStore._();
factory ConnectAuthStore() => _instance;
static ConnectAuthStore get instance => _instance;
ConnectAuthStore._();
final FlutterSecureStorage _storage = const FlutterSecureStorage();
String? _token;
DateTime? _expiresAt;
bool _hydrated = false;
Future<String>? _inflightLogin;
Future<void> _hydrate() async {
if (_hydrated) return;
_token = await _storage.read(key: _tokenKey);
final rawExp = await _storage.read(key: _expiresAtKey);
_expiresAt = rawExp == null ? null : DateTime.tryParse(rawExp);
_hydrated = true;
}
bool _isUsable() {
if (_token == null || _token!.isEmpty) return false;
final exp = _expiresAt;
if (exp == null) return true;
return DateTime.now().add(_expiryGuard).isBefore(exp);
}
/// Returns a usable bearer token, logging in if necessary. Concurrent
/// callers share the same in-flight login future so a single 401 doesn't
/// trigger N parallel logins.
Future<String> getToken({bool forceRefresh = false}) async {
await _hydrate();
if (!forceRefresh && _isUsable()) return _token!;
return _inflightLogin ??= _login().whenComplete(() {
_inflightLogin = null;
});
}
Future<String> _login() async {
if (!AccountData().isPopulated()) {
throw ConnectException.notAuthenticated();
}
final username = AccountData().getUsername();
final password = AccountData().getPassword();
final LoginResponse response;
try {
response = await Login(
LoginRequest(
username: username,
password: password,
tokenName: _tokenName,
),
).run();
} on ConnectException {
rethrow;
} catch (e, st) {
log('connect login threw: $e', stackTrace: st);
throw ConnectException(
userMessage:
'Anmeldung am Connect-Server fehlgeschlagen. Bitte später erneut versuchen.',
technicalDetails: 'connect login failed: $e',
);
}
_token = response.token;
_expiresAt = response.expiresAt;
await _storage.write(key: _tokenKey, value: response.token);
if (response.expiresAt != null) {
await _storage.write(
key: _expiresAtKey,
value: response.expiresAt!.toIso8601String(),
);
} else {
await _storage.delete(key: _expiresAtKey);
}
return response.token;
}
Future<void> invalidate() async {
_token = null;
_expiresAt = null;
await _storage.delete(key: _tokenKey);
await _storage.delete(key: _expiresAtKey);
}
/// Same as [invalidate] — separate method to make logout call-sites read
/// clearly.
Future<void> clear() => invalidate();
}