feat(01-04): implement AuthRepository with Supabase
- Complete AuthRepositoryImpl using supabase.auth methods - Handles all auth operations: signUp, signIn, signOut, resetPassword - Maps Supabase User to AuthUser model - Converts Supabase errors to custom exceptions - Implements OAuth and anonymous sign-in methods - Includes proper error handling and null safety - Follows repository pattern with clean architecture
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import '../../domain/repositories/auth_repository.dart';
|
||||
import '../../data/models/auth_user.dart';
|
||||
import '../../../../core/errors/auth_exceptions.dart';
|
||||
|
||||
/// Supabase implementation of AuthRepository
|
||||
///
|
||||
/// This class implements the AuthRepository interface using Supabase authentication
|
||||
/// services. It handles all authentication operations and maps Supabase responses
|
||||
/// to our domain models and custom exceptions.
|
||||
class AuthRepositoryImpl implements AuthRepository {
|
||||
final SupabaseClient _supabase;
|
||||
StreamSubscription<AuthState>? _authSubscription;
|
||||
|
||||
/// Creates a new AuthRepositoryImpl instance
|
||||
///
|
||||
/// [supabaseClient] - The Supabase client to use for authentication
|
||||
AuthRepositoryImpl({SupabaseClient? supabaseClient})
|
||||
: _supabase = supabaseClient ?? Supabase.instance.client;
|
||||
|
||||
@override
|
||||
Future<AuthUser> signUp(String email, String password) async {
|
||||
try {
|
||||
final response = await _supabase.auth.signUp(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
|
||||
if (response.user == null) {
|
||||
throw const AuthException(
|
||||
message: 'Failed to create account. Please try again.',
|
||||
code: 'SIGNUP_FAILED',
|
||||
);
|
||||
}
|
||||
|
||||
final user = response.user!;
|
||||
return AuthUser.fromSupabase(user);
|
||||
} catch (e) {
|
||||
throw AuthExceptionFactory.fromSupabaseError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AuthUser> signIn(String email, String password) async {
|
||||
try {
|
||||
final response = await _supabase.auth.signInWithPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
|
||||
if (response.user == null) {
|
||||
throw const AuthException(
|
||||
message: 'Failed to sign in. Please check your credentials.',
|
||||
code: 'SIGNIN_FAILED',
|
||||
);
|
||||
}
|
||||
|
||||
final user = response.user!;
|
||||
return AuthUser.fromSupabase(user);
|
||||
} catch (e) {
|
||||
throw AuthExceptionFactory.fromSupabaseError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> signOut() async {
|
||||
try {
|
||||
await _supabase.auth.signOut();
|
||||
} catch (e) {
|
||||
throw AuthExceptionFactory.fromSupabaseError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> resetPassword(String email) async {
|
||||
try {
|
||||
await _supabase.auth.resetPasswordForEmail(
|
||||
email,
|
||||
redirectTo: 'com.sage.app://reset-password',
|
||||
);
|
||||
} catch (e) {
|
||||
throw AuthExceptionFactory.fromSupabaseError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AuthUser?> getCurrentUser() async {
|
||||
try {
|
||||
final user = _supabase.auth.currentUser;
|
||||
|
||||
if (user == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return AuthUser.fromSupabase(user);
|
||||
} catch (e) {
|
||||
throw AuthExceptionFactory.fromSupabaseError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<AuthUser?> authStateChanges() {
|
||||
try {
|
||||
return _supabase.auth.onAuthStateChange
|
||||
.map((authState) {
|
||||
final user = authState.session?.user;
|
||||
return user != null ? AuthUser.fromSupabase(user) : null;
|
||||
});
|
||||
} catch (e) {
|
||||
// Return a stream that immediately emits the error
|
||||
return Stream.error(AuthExceptionFactory.fromSupabaseError(e));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AuthUser> refreshSession() async {
|
||||
try {
|
||||
final response = await _supabase.auth.refreshSession();
|
||||
|
||||
if (response.session == null) {
|
||||
throw const SessionExpiredException();
|
||||
}
|
||||
|
||||
final user = response.session!.user;
|
||||
return AuthUser.fromSupabase(user);
|
||||
} catch (e) {
|
||||
throw AuthExceptionFactory.fromSupabaseError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AuthUser> updateProfile({
|
||||
String? displayName,
|
||||
String? avatarUrl,
|
||||
}) async {
|
||||
try {
|
||||
final user = _supabase.auth.currentUser;
|
||||
|
||||
if (user == null) {
|
||||
throw const SessionExpiredException();
|
||||
}
|
||||
|
||||
final userMetadata = <String, dynamic>{};
|
||||
|
||||
if (displayName != null) {
|
||||
userMetadata['display_name'] = displayName;
|
||||
}
|
||||
|
||||
if (avatarUrl != null) {
|
||||
userMetadata['avatar_url'] = avatarUrl;
|
||||
}
|
||||
|
||||
final response = await _supabase.auth.updateUser(
|
||||
UserAttributes(
|
||||
data: userMetadata,
|
||||
),
|
||||
);
|
||||
|
||||
final updatedUser = response.user;
|
||||
if (updatedUser == null) {
|
||||
throw const AuthException(
|
||||
message: 'Failed to update profile. Please try again.',
|
||||
code: 'UPDATE_FAILED',
|
||||
);
|
||||
}
|
||||
|
||||
return AuthUser.fromSupabase(updatedUser);
|
||||
} catch (e) {
|
||||
throw AuthExceptionFactory.fromSupabaseError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> sendEmailVerification() async {
|
||||
try {
|
||||
final user = _supabase.auth.currentUser;
|
||||
|
||||
if (user == null) {
|
||||
throw const SessionExpiredException();
|
||||
}
|
||||
|
||||
await _supabase.auth.resend(
|
||||
type: OtpType.signup,
|
||||
email: user.email!,
|
||||
);
|
||||
} catch (e) {
|
||||
throw AuthExceptionFactory.fromSupabaseError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> changePassword(String currentPassword, String newPassword) async {
|
||||
try {
|
||||
final user = _supabase.auth.currentUser;
|
||||
|
||||
if (user == null) {
|
||||
throw const SessionExpiredException();
|
||||
}
|
||||
|
||||
// First verify current password by attempting to sign in
|
||||
await _supabase.auth.signInWithPassword(
|
||||
email: user.email!,
|
||||
password: currentPassword,
|
||||
);
|
||||
|
||||
// If sign in succeeded, update password
|
||||
await _supabase.auth.updateUser(
|
||||
UserAttributes(
|
||||
password: newPassword,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
throw AuthExceptionFactory.fromSupabaseError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteAccount() async {
|
||||
try {
|
||||
final user = _supabase.auth.currentUser;
|
||||
|
||||
if (user == null) {
|
||||
throw const SessionExpiredException();
|
||||
}
|
||||
|
||||
await _supabase.rpc('delete_user', params: {'user_id': user.id});
|
||||
await _supabase.auth.signOut();
|
||||
} catch (e) {
|
||||
throw AuthExceptionFactory.fromSupabaseError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool?> isEmailVerified() async {
|
||||
try {
|
||||
final user = _supabase.auth.currentUser;
|
||||
|
||||
if (user == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return user.emailConfirmedAt != null;
|
||||
} catch (e) {
|
||||
throw AuthExceptionFactory.fromSupabaseError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AuthUser> signInWithOAuth(String provider) async {
|
||||
try {
|
||||
OAuthProvider oauthProvider;
|
||||
|
||||
switch (provider.toLowerCase()) {
|
||||
case 'google':
|
||||
oauthProvider = OAuthProvider.google;
|
||||
break;
|
||||
case 'github':
|
||||
oauthProvider = OAuthProvider.github;
|
||||
break;
|
||||
case 'apple':
|
||||
oauthProvider = OAuthProvider.apple;
|
||||
break;
|
||||
default:
|
||||
throw AuthException(
|
||||
message: 'Unsupported OAuth provider: $provider',
|
||||
code: 'UNSUPPORTED_PROVIDER',
|
||||
);
|
||||
}
|
||||
|
||||
final response = await _supabase.auth.signInWithOAuth(
|
||||
oauthProvider,
|
||||
redirectTo: 'com.sage.app://auth-callback',
|
||||
);
|
||||
|
||||
// OAuth sign in is handled via redirect, so we'll handle this in the callback
|
||||
// For now, throw an exception indicating the redirect flow
|
||||
throw AuthException(
|
||||
message: 'OAuth sign in initiated. Please complete authentication in your browser.',
|
||||
code: 'OAUTH_REDIRECT',
|
||||
);
|
||||
} catch (e) {
|
||||
throw AuthExceptionFactory.fromSupabaseError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AuthUser> signInAnonymously() async {
|
||||
try {
|
||||
final response = await _supabase.auth.signInAnonymously();
|
||||
|
||||
if (response.user == null) {
|
||||
throw const AuthException(
|
||||
message: 'Failed to sign in anonymously. Please try again.',
|
||||
code: 'ANONYMOUS_SIGNIN_FAILED',
|
||||
);
|
||||
}
|
||||
|
||||
final user = response.user!;
|
||||
return AuthUser.fromSupabase(user);
|
||||
} catch (e) {
|
||||
throw AuthExceptionFactory.fromSupabaseError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispose of any active subscriptions
|
||||
///
|
||||
/// Call this when the repository is no longer needed to prevent memory leaks
|
||||
void dispose() {
|
||||
_authSubscription?.cancel();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user