import 'dart:async'; import 'package:shuffle/features/auth/domain/auth_failure.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; /// Aufruf, bei dem ein Fehler auftrat. Manche Zuordnungen hängen davon ab /// (z. B. HTTP 500 nur beim `signUp`). enum AuthOperation { signUp, signIn, resend, requestPasswordReset, verifyLink, updatePassword, } /// Übersetzt Ausnahmen von `supabase_flutter` in [AuthFailure] /// (Mapping-Tabelle: Spec #1 § 4.4). /// /// Befund zu `gotrue` 2.27.3 (A1-Prüfung der Spec): Jede Antwort mit Status /// >= 500 wird als [AuthRetryableFetchException] geworfen, mit dem rohen Body /// in `message`, dem Status in `statusCode` und `code == null`. Ein /// Trigger-Fehler beim `signUp` (HTTP 500) ist deshalb nur am Status /// erkennbar; die Prüfung muss vor dem Netzwerkfall stehen. AuthFailure mapAuthError(Object error, AuthOperation operation) { if (error is AuthWeakPasswordException) { return AuthFailure.weakPassword(_weakReasons(error.reasons)); } if (error is AuthRetryableFetchException) { if (operation == AuthOperation.signUp && error.statusCode == '500') { return AuthFailure.registrationRejected( reason: registrationRejectionReasonFrom(error.message), ); } // Netzwerkfehler (kein Status) und sonstige 5xx (Gateway, Ausfall). return const AuthFailure.network(); } if (error is AuthSessionMissingException) { return const AuthFailure.unexpected(); } if (error is AuthException) { return _mapApiError(error, operation); } if (error is TimeoutException || _isTransportError(error)) { return const AuthFailure.network(); } return const AuthFailure.unexpected(); } AuthFailure _mapApiError(AuthException error, AuthOperation operation) { switch (error.code) { case 'invalid_credentials': return const AuthFailure.invalidCredentials(); case 'email_not_confirmed': return const AuthFailure.emailNotConfirmed(); case 'over_email_send_rate_limit': return switch (operation) { AuthOperation.signUp || AuthOperation.requestPasswordReset || AuthOperation.resend => const AuthFailure.emailSendLimited(), _ => const AuthFailure.rateLimited(), }; case 'over_request_rate_limit': return const AuthFailure.rateLimited(); case 'weak_password': return const AuthFailure.weakPassword([]); case 'same_password': return const AuthFailure.samePassword(); case 'otp_expired': return const AuthFailure.linkInvalidOrExpired(); case 'email_address_invalid' || 'validation_failed': return const AuthFailure.invalidEmail(); } if (error.statusCode == '429') { return const AuthFailure.rateLimited(); } if (operation == AuthOperation.verifyLink && error.statusCode == '403') { return const AuthFailure.linkInvalidOrExpired(); } return const AuthFailure.unexpected(); } List _weakReasons(List reasons) => [ for (final reason in reasons) switch (reason) { 'length' => WeakPasswordReason.length, 'characters' => WeakPasswordReason.characters, 'pwned' => WeakPasswordReason.pwned, _ => WeakPasswordReason.unknown, }, ]; /// Liest den Grund aus der Trigger-Meldung (Body des HTTP-500). Best effort, /// das Format ist nicht zugesichert. RegistrationRejectionReason registrationRejectionReasonFrom(String message) { if (message.contains('display_name_invalid')) { return RegistrationRejectionReason.displayName; } if (message.contains('age_confirmation_required')) { return RegistrationRejectionReason.ageConfirmation; } if (message.contains('terms_acceptance_required')) { return RegistrationRejectionReason.termsAcceptance; } if (message.contains('terms_version_required')) { return RegistrationRejectionReason.termsVersion; } return RegistrationRejectionReason.unknown; } /// Rohe Transportfehler (`SocketException`, `ClientException`, …). `dart:io` /// und `package:http` werden bewusst nicht importiert (Web-Build, Lint); /// `gotrue` verpackt Transportfehler normalerweise schon selbst. bool _isTransportError(Object error) => switch (error.runtimeType.toString()) { 'SocketException' || 'ClientException' || 'HandshakeException' || 'TlsException' => true, _ => false, };