Files
Holzleitner-Lieferservice-App/lib/feature/authentication/bloc/auth_state.dart
Dennis Nemec 467f4b4ed2 feat(auth): Login-Timeout (10s) mit Hinweisbanner
Haengt der interaktive Login (Browser-Tab/Token-Exchange) bei Verbindungsabbruch/Issuer-Hang, bricht er nach 10s ab; LoginPage zeigt 'Einloggen nicht moeglich. Spaeter erneut versuchen.' (Unauthenticated.loginTimedOut).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:08:18 +02:00

105 lines
3.4 KiB
Dart

import 'package:hl_lieferservice/feature/authentication/model/user.dart';
abstract class AuthState {}
/// Initialer State beim App-Start: der Refresh-Token aus der Secure
/// Storage wird gerade gegen das Issuer-Endpoint geprüft. Die UI zeigt
/// in dieser Zeit einen Splash statt LoginPage, damit Nutzer mit
/// gespeicherter Session keinen sichtbaren Login-Flash sehen.
class AuthBootstrapping extends AuthState {}
class Unauthenticated extends AuthState {
final bool sessionExpired;
/// `true`, wenn der letzte Login-Versuch in das 10-s-Timeout im
/// [AuthBloc] gelaufen ist (z. B. Verbindungsabbruch während
/// `tokenProvider.login()` oder hängender Issuer). Die [LoginPage]
/// blendet daraufhin einen Hinweis ein.
final bool loginTimedOut;
Unauthenticated({this.sessionExpired = false, this.loginTimedOut = false});
}
/// Transient state während dem PKCE-Flow (Browser-Tab offen,
/// Token-Tausch läuft).
class Authenticating extends AuthState {}
/// Erfolgreiche Authentifizierung. Trägt sowohl die "neuen"
/// Token-/Claim-Felder als auch das Legacy-User-Objekt — Letzteres
/// wird aus den Claims befüllt und mit Phase D komplett entfernt.
class Authenticated extends AuthState {
Authenticated({
required this.personalnummer,
required this.displayName,
required this.email,
required this.user,
required this.sessionId,
});
/// Personalnummer aus dem `personalnummer`-Custom-Claim. Das
/// Backend nutzt diese als Account-Identifier.
final int personalnummer;
/// Beste verfügbare Beschriftung für den Fahrer:
/// `name` > `given_name + family_name` > `preferred_username`.
final String displayName;
/// E-Mail aus dem `email`-Claim, falls vorhanden.
final String? email;
/// Legacy: das alte `User`-Domänenobjekt, aus den Claims befüllt.
/// Wird in Phase D entfernt, sobald keine Aufrufer mehr da sind.
final User user;
/// Legacy: füllt für die alten `getSessionOrThrow()`-Aufrufer
/// einen Wert — die laufen sowieso gegen ERPframe und werden in
/// Phase D abgelöst.
final String sessionId;
/// Konstruiert den State aus dem dekodierten ID-Token-Payload.
factory Authenticated.fromClaims({
required Map<String, dynamic> claims,
required String accessToken,
}) {
final personalnummerRaw = claims['personalnummer'];
final personalnummer = switch (personalnummerRaw) {
int v => v,
String v => int.parse(v),
_ => throw FormatException(
'personalnummer-Claim fehlt oder hat unerwarteten Typ '
'(${personalnummerRaw.runtimeType})',
),
};
final given = (claims['given_name'] as String?) ?? '';
final family = (claims['family_name'] as String?) ?? '';
final preferredUsername =
(claims['preferred_username'] as String?) ?? 'fahrer';
final fullName = (claims['name'] as String?)?.trim();
final displayName =
fullName != null && fullName.isNotEmpty
? fullName
: (given.isNotEmpty || family.isNotEmpty
? '$given $family'.trim()
: preferredUsername);
final email = claims['email'] as String?;
final user = User(
number: personalnummer.toString(),
firstName: given,
lastName: family,
mail: email ?? '',
);
return Authenticated(
personalnummer: personalnummer,
displayName: displayName,
email: email,
user: user,
sessionId: accessToken,
);
}
}