App-Code: - KeycloakOidcTokenProvider: PKCE-Login via flutter_appauth, Refresh via Refresh-Token aus flutter_secure_storage, Session-Restore beim App-Start, Logout. - AuthSessionEvent als Provider→Bloc-Brücke (LoggedIn/LoggedOut/ SessionExpired) auf einem Broadcast-Stream. - AuthBloc komplett umgebaut: nimmt jetzt den KeycloakOidcTokenProvider statt UserInfoService, mappt eingehende Provider-Events auf eigene Zustände. Authenticated.fromClaims() liest personalnummer + Name aus dem ID-Token-Payload. - LoginPage: kein Browser+Deep-Link mehr — Button feuert LoginRequested, der Provider übernimmt den restlichen Flow. - network_locator: produktiver KeycloakOidcTokenProvider, doppelt registriert (KeycloakOidcTokenProvider für AuthBloc, AuthTokenProvider für Interceptor). - Auth-State trägt zusätzlich personalnummer/displayName/email; das Legacy-User-Objekt + sessionId bleiben temporär drin, damit die alten ERPframe-Services (Phase D) noch kompilieren. Plattform-Setup: - Android: appAuthRedirectScheme=holzleitner in build.gradle.kts, NetworkSecurityConfig erlaubt HTTP zu localhost/10.0.2.2/127.0.0.1. - iOS: holzleitner als URL-Scheme im Info.plist, ATS-Ausnahme für localhost (HTTP-Keycloak im Dev-Setup). Out of scope: - Keine echte App-Run-Smoke — kommt mit dem User-Test. - iOS-pod-install läuft beim ersten 'flutter run ios' automatisch. - Old ERPframe-Services bleiben aktiv und werfen ab jetzt 401 (kein Cookie-Session-Token mehr) — wird in Phase D entfernt.
92 lines
2.8 KiB
Dart
92 lines
2.8 KiB
Dart
import 'package:hl_lieferservice/feature/authentication/model/user.dart';
|
|
|
|
abstract class 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,
|
|
);
|
|
}
|
|
}
|