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