implemented RMV public transit module including trip search, station departures, and nearby stop lookup, added "Marianum Connect" API integration with bearer token authentication and auto-refresh logic, integrated geolocator for location-based station search, added persistent storage for favorite stations and recent trip queries, and implemented comprehensive UI for journey details, trip results, and disruption alerts
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user