Finish the recovery guard only after the password change, mark the change in the repository so a late userUpdated event is harmless, keep the form locked after success, carry from through the reset check-email page, share the password error texts and focus the continue button. Refs #1 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
87 lines
3 KiB
Dart
87 lines
3 KiB
Dart
import 'package:shuffle/features/auth/domain/auth_status.dart';
|
|
import 'package:shuffle/features/auth/domain/auth_user.dart';
|
|
// `AuthUser` heißt auch in supabase_flutter; hier ist die Domänenklasse gemeint.
|
|
import 'package:supabase_flutter/supabase_flutter.dart' hide AuthUser;
|
|
|
|
/// Bildet `AuthChangeEvent`s auf [AuthStatus] ab (Spec #1 § 5.5).
|
|
///
|
|
/// Abweichung von der Wortlaut-Zuordnung in § 5.5: Die Recovery bleibt
|
|
/// bestehen, bis [finishRecovery] oder `signedOut` eintrifft. Ein
|
|
/// `tokenRefreshed` oder `signedIn` während der Recovery hebt den Guard nicht
|
|
/// auf. `userUpdated` (Passwort geändert) beendet ihn ebenfalls **nicht**
|
|
/// mehr, sondern setzt nur `passwordChanged`: Sonst würde der Router den
|
|
/// Nutzer von `/reset-password` wegleiten, bevor die Erfolgsmeldung
|
|
/// (KTO-01 Kriterium 9) zu sehen ist. Erst die ausdrückliche Bestätigung des
|
|
/// Nutzers beendet den Guard. Der Zustand lebt nur im Prozess
|
|
/// (Spec #1 § 5.4, R12): Aus dem `amr`-Claim lässt er sich nicht ableiten.
|
|
class AuthStatusTracker {
|
|
bool _recovery = false;
|
|
bool _passwordChanged = false;
|
|
|
|
/// Status für [session] unter Berücksichtigung der laufenden Recovery.
|
|
AuthStatus statusFor(Session? session) {
|
|
final user = session?.user;
|
|
if (user == null) {
|
|
return const AuthStatus.signedOut();
|
|
}
|
|
final authUser = AuthUser(
|
|
id: user.id,
|
|
email: user.email,
|
|
emailConfirmed: user.emailConfirmedAt != null,
|
|
);
|
|
return _recovery
|
|
? AuthStatus.passwordRecovery(
|
|
authUser,
|
|
passwordChanged: _passwordChanged,
|
|
)
|
|
: AuthStatus.signedIn(authUser);
|
|
}
|
|
|
|
/// Verarbeitet ein Ereignis und liefert den neuen Status.
|
|
AuthStatus handle(AuthChangeEvent event, Session? session) {
|
|
switch (event) {
|
|
case AuthChangeEvent.passwordRecovery:
|
|
_recovery = session != null;
|
|
_passwordChanged = false;
|
|
case AuthChangeEvent.userUpdated:
|
|
// The password change itself: stay in the guard (see class doc).
|
|
if (_recovery) {
|
|
_passwordChanged = true;
|
|
}
|
|
case AuthChangeEvent.signedOut:
|
|
_end();
|
|
default:
|
|
break;
|
|
}
|
|
if (session == null) {
|
|
_end();
|
|
}
|
|
return statusFor(session);
|
|
}
|
|
|
|
/// The password was set during a running recovery (successful
|
|
/// `updateUser`). Idempotent to the later `userUpdated` event, which gotrue
|
|
/// delivers asynchronously, possibly after `updateUser` returned. Without a
|
|
/// running recovery nothing changes.
|
|
AuthStatus markPasswordChanged(Session? session) {
|
|
if (_recovery) {
|
|
_passwordChanged = true;
|
|
}
|
|
return statusFor(session);
|
|
}
|
|
|
|
/// Ends the recovery after the user confirmed the success message. Only
|
|
/// possible once the password is changed; before that the status stays
|
|
/// unchanged (the guard cannot be skipped).
|
|
AuthStatus finishRecovery(Session? session) {
|
|
if (_recovery && _passwordChanged) {
|
|
_end();
|
|
}
|
|
return statusFor(session);
|
|
}
|
|
|
|
void _end() {
|
|
_recovery = false;
|
|
_passwordChanged = false;
|
|
}
|
|
}
|