Final commit.
This commit is contained in:
@ -53,4 +53,44 @@ class BackendConfig {
|
||||
keycloakClientId: 'holzleitner-app',
|
||||
keycloakRedirectUrl: 'holzleitner://oauth2redirect',
|
||||
);
|
||||
|
||||
/// Konfiguration für USB-Tunnel via `adb reverse` — gedacht für Tests in
|
||||
/// fremden Netzwerken, in denen das Gerät den Mac nicht über eine LAN-IP
|
||||
/// erreicht. Alles zeigt auf `localhost`; der Traffic wird über den
|
||||
/// USB-Bus zum Host getunnelt.
|
||||
///
|
||||
/// **Setup vor dem Start (Gerät per USB angesteckt):**
|
||||
/// ```
|
||||
/// adb reverse tcp:3000 tcp:3000 # Rust-API
|
||||
/// adb reverse tcp:8080 tcp:8080 # Keycloak
|
||||
/// ```
|
||||
///
|
||||
/// **Backend-Voraussetzungen**, damit das OIDC-Login funktioniert:
|
||||
/// * Backend-Env `KEYCLOAK_ISSUER_URL=http://localhost:8080/realms/holzleitner`
|
||||
/// (muss exakt mit [keycloakIssuerUrl] matchen, sonst 401 `invalid issuer`).
|
||||
/// * Keycloak muss den Issuer als `localhost` ausgeben — z. B. via
|
||||
/// `KC_HOSTNAME_URL=http://localhost:8080` (oder Frontend-URL im Realm),
|
||||
/// sonst prägt es den Container-Hostnamen ins `iss`-Claim.
|
||||
/// * Der `holzleitner://oauth2redirect`-Redirect bleibt unverändert (das
|
||||
/// Custom-Scheme ist netzwerk-unabhängig).
|
||||
///
|
||||
/// Aktivieren ohne Code-Edit:
|
||||
/// ```
|
||||
/// flutter run --dart-define=HL_BACKEND=usb
|
||||
/// ```
|
||||
static const BackendConfig usbReverse = BackendConfig(
|
||||
apiBaseUrl: 'http://localhost:3000',
|
||||
keycloakIssuerUrl: 'http://localhost:8080/realms/holzleitner',
|
||||
keycloakClientId: 'holzleitner-app',
|
||||
keycloakRedirectUrl: 'holzleitner://oauth2redirect',
|
||||
);
|
||||
|
||||
/// Wählt die Config anhand des Compile-Time-Flags `HL_BACKEND`:
|
||||
/// * `usb` → [usbReverse] (adb-reverse-Tunnel über localhost)
|
||||
/// * sonst → [localDev] (LAN-IP, Default)
|
||||
///
|
||||
/// So muss für einen Netzwerkwechsel nur das Build-Flag gesetzt werden,
|
||||
/// nicht der Quellcode angefasst.
|
||||
static const BackendConfig fromEnvironment =
|
||||
String.fromEnvironment('HL_BACKEND') == 'usb' ? usbReverse : localDev;
|
||||
}
|
||||
|
||||
@ -55,6 +55,13 @@ class KeycloakOidcTokenProvider implements AuthTokenProvider {
|
||||
String? _refreshToken;
|
||||
Map<String, dynamic>? _idTokenClaims;
|
||||
|
||||
/// Single-flight-Guard: hält den gerade laufenden Refresh, damit mehrere
|
||||
/// gleichzeitige Aufrufer (Bootstrap: Restore + PaymentMethodsCubit +
|
||||
/// Folge-Requests) sich EINEN Refresh teilen statt parallele
|
||||
/// `flutter_appauth.token()`-Calls auszulösen (die nativ blockieren/haken
|
||||
/// können → App hängt nach Hot-Restart am Splash/Login).
|
||||
Future<String?>? _refreshInFlight;
|
||||
|
||||
final StreamController<AuthSessionEvent> _events =
|
||||
StreamController<AuthSessionEvent>.broadcast();
|
||||
|
||||
@ -166,6 +173,19 @@ class KeycloakOidcTokenProvider implements AuthTokenProvider {
|
||||
final rt = _refreshToken;
|
||||
if (rt == null) return null;
|
||||
|
||||
// Single-flight: läuft bereits ein Refresh, hängen wir uns dran, statt
|
||||
// einen zweiten `flutter_appauth.token()`-Call zu starten. `??=`
|
||||
// evaluiert die rechte Seite nur, wenn noch kein Refresh läuft.
|
||||
return _refreshInFlight ??= _performRefresh(rt).whenComplete(() {
|
||||
_refreshInFlight = null;
|
||||
});
|
||||
}
|
||||
|
||||
/// Führt EINEN Token-Refresh aus. Bei Erfolg werden die Tokens übernommen
|
||||
/// und der neue Access-Token zurückgegeben (ohne Event — stiller Refresh).
|
||||
/// Bei Fehler ist die Session tot: lokal aufräumen, `AuthSessionExpired`
|
||||
/// emittieren, `null` zurück.
|
||||
Future<String?> _performRefresh(String rt) async {
|
||||
try {
|
||||
final result = await _appAuth.token(
|
||||
TokenRequest(
|
||||
@ -187,11 +207,17 @@ class KeycloakOidcTokenProvider implements AuthTokenProvider {
|
||||
return _accessToken;
|
||||
} on Exception {
|
||||
// Refresh hat nicht funktioniert — Session ist tot, nicht
|
||||
// wiederherstellbar. Aufrufer kriegen null zurück, AuthBloc
|
||||
// bekommt SessionExpired.
|
||||
// wiederherstellbar. Reihenfolge bewusst: erst State leeren + Event
|
||||
// feuern, DANN best-effort den Storage löschen — so kann ein
|
||||
// werfendes `delete` weder das Event verschlucken noch eine Exception
|
||||
// aus `currentAccessToken()` leaken.
|
||||
_clearSession();
|
||||
await _storage.delete(key: _refreshTokenStorageKey);
|
||||
_events.add(const AuthSessionExpired());
|
||||
try {
|
||||
await _storage.delete(key: _refreshTokenStorageKey);
|
||||
} catch (e) {
|
||||
debugPrint('currentAccessToken: Refresh-Token-Delete fehlgeschlagen: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ import 'keycloak_oidc_token_provider.dart';
|
||||
/// das reine dart-Smoke-Tool, siehe `tool/smoke_test_api.dart`).
|
||||
void registerNetworking({
|
||||
required GetIt locator,
|
||||
BackendConfig config = BackendConfig.localDev,
|
||||
BackendConfig config = BackendConfig.fromEnvironment,
|
||||
}) {
|
||||
locator.registerSingleton<BackendConfig>(config);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user