- AuthBootstrapping als neuer Initial-State im AuthBloc. Beim Cold-Start bleibt die App im Splash, bis restoreSession entweder Authenticated oder Unauthenticated emittiert — kein sichtbarer LoginPage-Flash mehr für Nutzer mit gespeicherter Session. - LoginEnforcer rendert für AuthBootstrapping ein eigenes Splash-Widget mit Logo + Spinner, für Unauthenticated weiterhin die LoginPage. - AuthBloc._handleRestore emittiert Unauthenticated explizit, wenn restoreSession false liefert oder wirft — sonst bliebe der Bootstrap- State hängen. - HomeAppDrawer zeigt jetzt displayName + Personalnummer aus dem Authenticated-State im Header und bekommt einen Abmelden-Eintrag unten (rot, Confirm-Dialog), der LogoutRequested feuert. Der Provider löscht den Refresh-Token aus der Secure Storage und der LoginEnforcer routet automatisch zurück auf die LoginPage.
98 lines
3.1 KiB
Dart
98 lines
3.1 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;
|
|
Unauthenticated({this.sessionExpired = 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,
|
|
);
|
|
}
|
|
}
|