import 'dart:async'; import 'package:shuffle/core/logging/app_logger.dart'; import 'package:shuffle/core/result/result.dart'; import 'package:shuffle/features/auth/data/auth_failure_mapper.dart'; import 'package:shuffle/features/auth/data/auth_status_tracker.dart'; import 'package:shuffle/features/auth/domain/auth_failure.dart'; import 'package:shuffle/features/auth/domain/auth_repository.dart'; import 'package:shuffle/features/auth/domain/auth_status.dart'; import 'package:shuffle/features/auth/domain/email_link_type.dart'; import 'package:shuffle/features/auth/domain/registration_input.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; /// [AuthRepository] gegen Supabase Auth (GoTrue). /// /// Logs enthalten weder E-Mail-Adressen noch Passwörter noch Tokens, nur /// Operation, Fehlerart, HTTP-Status und Fehlercode. class SupabaseAuthRepository implements AuthRepository { SupabaseAuthRepository(this._auth) { _lastStatus = _tracker.statusFor(_auth.currentSession); _subscription = _auth.onAuthStateChange.listen( _onAuthState, onError: (Object error, StackTrace stackTrace) { _log.warning('auth state stream error: ${error.runtimeType}'); }, ); } final GoTrueClient _auth; final _tracker = AuthStatusTracker(); final _changes = StreamController.broadcast(sync: true); final _log = AppLogger('AuthRepository'); late AuthStatus _lastStatus; StreamSubscription? _subscription; void _onAuthState(AuthState state) => _publish(_tracker.handle(state.event, state.session)); void _publish(AuthStatus next) { if (next != _lastStatus) { _lastStatus = next; _changes.add(next); } } /// Beendet das Abonnieren der Auth-Ereignisse. Future dispose() async { await _subscription?.cancel(); _subscription = null; await _changes.close(); } @override AuthStatus get currentStatus => _lastStatus; @override Stream watchStatus() { // ignore: close_sinks, wird vom Abonnenten über cancel geschlossen. late StreamController controller; StreamSubscription? inner; controller = StreamController( onListen: () { controller.add(_lastStatus); inner = _changes.stream.listen(controller.add); }, onCancel: () => inner?.cancel(), ); return controller.stream; } @override Future> signUp(RegistrationInput input) => _guard( AuthOperation.signUp, () => _auth.signUp( email: input.email.trim(), password: input.password, data: { 'display_name': input.displayName.trim(), // JSON-Booleans: Der Trigger vergleicht `meta ->> '…'` mit 'true'. 'age_confirmed': input.ageConfirmed, 'terms_accepted': input.termsAccepted, 'terms_version': input.termsVersion, 'locale': input.locale, }, ), ); @override Future> resendConfirmation(String email) => _guard( AuthOperation.resend, () => _auth.resend(type: OtpType.signup, email: email.trim()), ); @override Future> signIn(String email, String password) => _guard( AuthOperation.signIn, () => _auth.signInWithPassword(email: email.trim(), password: password), ); @override Future> requestPasswordReset(String email) => _guard( AuthOperation.requestPasswordReset, () => _auth.resetPasswordForEmail(email.trim()), ); @override Future> verifyEmailLink( String tokenHash, EmailLinkType type, ) => _guard(AuthOperation.verifyLink, () async { final response = await _auth.verifyOTP( tokenHash: tokenHash, type: switch (type) { EmailLinkType.confirmation => OtpType.email, EmailLinkType.recovery => OtpType.recovery, }, ); if (response.session == null) { throw StateError('verifyOTP returned no session'); } }); @override Future> setNewPassword(String password) => _guard(AuthOperation.updatePassword, () async { await _auth.updateUser(UserAttributes(password: password)); // gotrue delivers `userUpdated` asynchronously, possibly after this // call returned. Mark the change here as well (idempotent), so the // status never lags behind the successful result. _publish(_tracker.markPasswordChanged(_auth.currentSession)); }); @override void finishPasswordRecovery() { if (_lastStatus case PasswordRecovery(passwordChanged: true)) { _publish(_tracker.finishRecovery(_auth.currentSession)); } } @override Future signOut() async { try { await _auth.signOut(); } on Object catch (error) { _log.warning('signOut failed: ${error.runtimeType}'); } } Future> _guard( AuthOperation operation, Future Function() action, ) async { try { await action(); return const Result.success(null); } on Object catch (error) { final failure = mapAuthError(error, operation); _logFailure(operation, error, failure); return Result.failure(failure); } } void _logFailure(AuthOperation operation, Object error, AuthFailure failure) { final status = error is AuthException ? error.statusCode : null; final code = error is AuthException ? error.code : null; final reason = failure is RegistrationRejected ? ' reason=${failure.reason.name}' : ''; _log.warning( '${operation.name} failed: ${failure.runtimeType} ' 'error=${error.runtimeType} status=$status code=$code$reason', ); } }