163 lines
5.2 KiB
Dart
163 lines
5.2 KiB
Dart
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<AuthStatus>.broadcast(sync: true);
|
|
final _log = AppLogger('AuthRepository');
|
|
late AuthStatus _lastStatus;
|
|
StreamSubscription<AuthState>? _subscription;
|
|
|
|
void _onAuthState(AuthState state) {
|
|
final next = _tracker.handle(state.event, state.session);
|
|
if (next != _lastStatus) {
|
|
_lastStatus = next;
|
|
_changes.add(next);
|
|
}
|
|
}
|
|
|
|
/// Beendet das Abonnieren der Auth-Ereignisse.
|
|
Future<void> dispose() async {
|
|
await _subscription?.cancel();
|
|
_subscription = null;
|
|
await _changes.close();
|
|
}
|
|
|
|
@override
|
|
AuthStatus get currentStatus => _lastStatus;
|
|
|
|
@override
|
|
Stream<AuthStatus> watchStatus() {
|
|
// ignore: close_sinks, wird vom Abonnenten über cancel geschlossen.
|
|
late StreamController<AuthStatus> controller;
|
|
StreamSubscription<AuthStatus>? inner;
|
|
controller = StreamController<AuthStatus>(
|
|
onListen: () {
|
|
controller.add(_lastStatus);
|
|
inner = _changes.stream.listen(controller.add);
|
|
},
|
|
onCancel: () => inner?.cancel(),
|
|
);
|
|
return controller.stream;
|
|
}
|
|
|
|
@override
|
|
Future<Result<void, AuthFailure>> signUp(RegistrationInput input) => _guard(
|
|
AuthOperation.signUp,
|
|
() => _auth.signUp(
|
|
email: input.email.trim(),
|
|
password: input.password,
|
|
data: <String, dynamic>{
|
|
'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<Result<void, AuthFailure>> resendConfirmation(String email) => _guard(
|
|
AuthOperation.resend,
|
|
() => _auth.resend(type: OtpType.signup, email: email.trim()),
|
|
);
|
|
|
|
@override
|
|
Future<Result<void, AuthFailure>> signIn(String email, String password) =>
|
|
_guard(
|
|
AuthOperation.signIn,
|
|
() => _auth.signInWithPassword(email: email.trim(), password: password),
|
|
);
|
|
|
|
@override
|
|
Future<Result<void, AuthFailure>> requestPasswordReset(String email) =>
|
|
_guard(
|
|
AuthOperation.requestPasswordReset,
|
|
() => _auth.resetPasswordForEmail(email.trim()),
|
|
);
|
|
|
|
@override
|
|
Future<Result<void, AuthFailure>> 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<Result<void, AuthFailure>> setNewPassword(String password) => _guard(
|
|
AuthOperation.updatePassword,
|
|
() => _auth.updateUser(UserAttributes(password: password)),
|
|
);
|
|
|
|
@override
|
|
Future<void> signOut() async {
|
|
try {
|
|
await _auth.signOut();
|
|
} on Object catch (error) {
|
|
_log.warning('signOut failed: ${error.runtimeType}');
|
|
}
|
|
}
|
|
|
|
Future<Result<void, AuthFailure>> _guard(
|
|
AuthOperation operation,
|
|
Future<void> 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',
|
|
);
|
|
}
|
|
}
|