feat(auth-repository): Refactor AuthRepositoryImpl to remove FirebaseAuth dependency and utilize DataConnectService
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
library;
|
||||
|
||||
import 'package:firebase_auth/firebase_auth.dart' as firebase;
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
@@ -28,12 +27,7 @@ class ClientAuthenticationModule extends Module {
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// Repositories
|
||||
i.addLazySingleton<AuthRepositoryInterface>(
|
||||
() => AuthRepositoryImpl(
|
||||
firebaseAuth: firebase.FirebaseAuth.instance,
|
||||
dataConnect: ExampleConnector.instance,
|
||||
),
|
||||
);
|
||||
i.addLazySingleton<AuthRepositoryInterface>(AuthRepositoryImpl.new);
|
||||
|
||||
// UseCases
|
||||
i.addLazySingleton(
|
||||
|
||||
@@ -12,7 +12,6 @@ import 'package:krow_domain/krow_domain.dart'
|
||||
AccountExistsException,
|
||||
UserNotFoundException,
|
||||
UnauthorizedAppException,
|
||||
UnauthorizedAppException,
|
||||
PasswordMismatchException,
|
||||
NetworkException;
|
||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
||||
@@ -23,18 +22,13 @@ import '../../domain/repositories/auth_repository_interface.dart';
|
||||
///
|
||||
/// This implementation integrates with Firebase Authentication for user
|
||||
/// identity management and Krow's Data Connect SDK for storing user profile data.
|
||||
class AuthRepositoryImpl
|
||||
with dc.DataErrorHandler
|
||||
implements AuthRepositoryInterface {
|
||||
|
||||
class AuthRepositoryImpl implements AuthRepositoryInterface {
|
||||
/// Creates an [AuthRepositoryImpl] with the real dependencies.
|
||||
AuthRepositoryImpl({
|
||||
required firebase.FirebaseAuth firebaseAuth,
|
||||
required dc.ExampleConnector dataConnect,
|
||||
}) : _firebaseAuth = firebaseAuth,
|
||||
_dataConnect = dataConnect;
|
||||
final firebase.FirebaseAuth _firebaseAuth;
|
||||
final dc.ExampleConnector _dataConnect;
|
||||
dc.DataConnectService? service,
|
||||
}) : _service = service ?? dc.DataConnectService.instance;
|
||||
|
||||
final dc.DataConnectService _service;
|
||||
|
||||
@override
|
||||
Future<domain.User> signInWithEmail({
|
||||
@@ -42,7 +36,8 @@ class AuthRepositoryImpl
|
||||
required String password,
|
||||
}) async {
|
||||
try {
|
||||
final firebase.UserCredential credential = await _firebaseAuth.signInWithEmailAndPassword(
|
||||
final firebase.UserCredential credential =
|
||||
await _service.auth.signInWithEmailAndPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
@@ -59,7 +54,6 @@ class AuthRepositoryImpl
|
||||
fallbackEmail: firebaseUser.email ?? email,
|
||||
requireBusinessRole: true,
|
||||
);
|
||||
|
||||
} on firebase.FirebaseAuthException catch (e) {
|
||||
if (e.code == 'invalid-credential' || e.code == 'wrong-password') {
|
||||
throw InvalidCredentialsException(
|
||||
@@ -94,7 +88,8 @@ class AuthRepositoryImpl
|
||||
|
||||
try {
|
||||
// Step 1: Try to create Firebase Auth user
|
||||
final firebase.UserCredential credential = await _firebaseAuth.createUserWithEmailAndPassword(
|
||||
final firebase.UserCredential credential =
|
||||
await _service.auth.createUserWithEmailAndPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
@@ -111,9 +106,9 @@ class AuthRepositoryImpl
|
||||
firebaseUser: firebaseUser,
|
||||
companyName: companyName,
|
||||
email: email,
|
||||
onBusinessCreated: (String businessId) => createdBusinessId = businessId,
|
||||
onBusinessCreated: (String businessId) =>
|
||||
createdBusinessId = businessId,
|
||||
);
|
||||
|
||||
} on firebase.FirebaseAuthException catch (e) {
|
||||
if (e.code == 'weak-password') {
|
||||
throw WeakPasswordException(
|
||||
@@ -137,11 +132,13 @@ class AuthRepositoryImpl
|
||||
}
|
||||
} on domain.AppException {
|
||||
// Rollback for our known exceptions
|
||||
await _rollbackSignUp(firebaseUser: firebaseUser, businessId: createdBusinessId);
|
||||
await _rollbackSignUp(
|
||||
firebaseUser: firebaseUser, businessId: createdBusinessId);
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
// Rollback: Clean up any partially created resources
|
||||
await _rollbackSignUp(firebaseUser: firebaseUser, businessId: createdBusinessId);
|
||||
await _rollbackSignUp(
|
||||
firebaseUser: firebaseUser, businessId: createdBusinessId);
|
||||
throw SignUpFailedException(
|
||||
technicalMessage: 'Unexpected error: $e',
|
||||
);
|
||||
@@ -164,11 +161,13 @@ class AuthRepositoryImpl
|
||||
required String password,
|
||||
required String companyName,
|
||||
}) async {
|
||||
developer.log('Email exists in Firebase, attempting sign-in: $email', name: 'AuthRepository');
|
||||
developer.log('Email exists in Firebase, attempting sign-in: $email',
|
||||
name: 'AuthRepository');
|
||||
|
||||
try {
|
||||
// Try to sign in with the provided password
|
||||
final firebase.UserCredential credential = await _firebaseAuth.signInWithEmailAndPassword(
|
||||
final firebase.UserCredential credential =
|
||||
await _service.auth.signInWithEmailAndPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
@@ -181,28 +180,32 @@ class AuthRepositoryImpl
|
||||
}
|
||||
|
||||
// Sign-in succeeded! Check if user already has a BUSINESS account in PostgreSQL
|
||||
final bool hasBusinessAccount = await _checkBusinessUserExists(firebaseUser.uid);
|
||||
final bool hasBusinessAccount =
|
||||
await _checkBusinessUserExists(firebaseUser.uid);
|
||||
|
||||
if (hasBusinessAccount) {
|
||||
// User already has a KROW Client account
|
||||
developer.log('User already has BUSINESS account: ${firebaseUser.uid}', name: 'AuthRepository');
|
||||
developer.log('User already has BUSINESS account: ${firebaseUser.uid}',
|
||||
name: 'AuthRepository');
|
||||
throw AccountExistsException(
|
||||
technicalMessage: 'User ${firebaseUser.uid} already has BUSINESS role',
|
||||
);
|
||||
}
|
||||
|
||||
// User exists in Firebase but not in KROW PostgreSQL - create the entities
|
||||
developer.log('Creating BUSINESS account for existing Firebase user: ${firebaseUser.uid}', name: 'AuthRepository');
|
||||
developer.log(
|
||||
'Creating BUSINESS account for existing Firebase user: ${firebaseUser.uid}',
|
||||
name: 'AuthRepository');
|
||||
return await _createBusinessAndUser(
|
||||
firebaseUser: firebaseUser,
|
||||
companyName: companyName,
|
||||
email: email,
|
||||
onBusinessCreated: (_) {}, // No rollback needed for existing Firebase user
|
||||
);
|
||||
|
||||
} on firebase.FirebaseAuthException catch (e) {
|
||||
// Sign-in failed - check why
|
||||
developer.log('Sign-in failed with code: ${e.code}', name: 'AuthRepository');
|
||||
developer.log('Sign-in failed with code: ${e.code}',
|
||||
name: 'AuthRepository');
|
||||
|
||||
if (e.code == 'wrong-password' || e.code == 'invalid-credential') {
|
||||
// Password doesn't match - check what providers are available
|
||||
@@ -226,9 +229,11 @@ class AuthRepositoryImpl
|
||||
// We can't distinguish between "wrong password" and "no password provider"
|
||||
// due to Firebase deprecating fetchSignInMethodsForEmail.
|
||||
// The PasswordMismatchException message covers both scenarios.
|
||||
developer.log('Password mismatch or different provider for: $email', name: 'AuthRepository');
|
||||
developer.log('Password mismatch or different provider for: $email',
|
||||
name: 'AuthRepository');
|
||||
throw PasswordMismatchException(
|
||||
technicalMessage: 'Email $email: password mismatch or different auth provider',
|
||||
technicalMessage:
|
||||
'Email $email: password mismatch or different auth provider',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -236,9 +241,11 @@ class AuthRepositoryImpl
|
||||
|
||||
Future<bool> _checkBusinessUserExists(String firebaseUserId) async {
|
||||
final QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> response =
|
||||
await executeProtected(() => _dataConnect.getUserById(id: firebaseUserId).execute());
|
||||
await _service.run(
|
||||
() => _service.connector.getUserById(id: firebaseUserId).execute());
|
||||
final dc.GetUserByIdUser? user = response.data.user;
|
||||
return user != null && (user.userRole == 'BUSINESS' || user.userRole == 'BOTH');
|
||||
return user != null &&
|
||||
(user.userRole == 'BUSINESS' || user.userRole == 'BOTH');
|
||||
}
|
||||
|
||||
/// Creates Business and User entities in PostgreSQL for a Firebase user.
|
||||
@@ -250,38 +257,44 @@ class AuthRepositoryImpl
|
||||
}) async {
|
||||
// Create Business entity in PostgreSQL
|
||||
|
||||
final OperationResult<dc.CreateBusinessData, dc.CreateBusinessVariables> createBusinessResponse =
|
||||
await executeProtected(() => _dataConnect.createBusiness(
|
||||
businessName: companyName,
|
||||
userId: firebaseUser.uid,
|
||||
rateGroup: dc.BusinessRateGroup.STANDARD,
|
||||
status: dc.BusinessStatus.PENDING,
|
||||
).execute());
|
||||
final OperationResult<dc.CreateBusinessData, dc.CreateBusinessVariables>
|
||||
createBusinessResponse = await _service.run(() => _service.connector
|
||||
.createBusiness(
|
||||
businessName: companyName,
|
||||
userId: firebaseUser.uid,
|
||||
rateGroup: dc.BusinessRateGroup.STANDARD,
|
||||
status: dc.BusinessStatus.PENDING,
|
||||
)
|
||||
.execute());
|
||||
|
||||
final dc.CreateBusinessBusinessInsert businessData = createBusinessResponse.data.business_insert;
|
||||
final dc.CreateBusinessBusinessInsert businessData =
|
||||
createBusinessResponse.data.business_insert;
|
||||
onBusinessCreated(businessData.id);
|
||||
|
||||
// Check if User entity already exists in PostgreSQL
|
||||
final QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> userResult =
|
||||
await executeProtected(() => _dataConnect.getUserById(id: firebaseUser.uid).execute());
|
||||
await _service.run(() =>
|
||||
_service.connector.getUserById(id: firebaseUser.uid).execute());
|
||||
final dc.GetUserByIdUser? existingUser = userResult.data.user;
|
||||
|
||||
if (existingUser != null) {
|
||||
// User exists (likely in another app like STAFF). Update role to BOTH.
|
||||
await executeProtected(() => _dataConnect.updateUser(
|
||||
id: firebaseUser.uid,
|
||||
)
|
||||
.userRole('BOTH')
|
||||
.execute());
|
||||
await _service.run(() => _service.connector
|
||||
.updateUser(
|
||||
id: firebaseUser.uid,
|
||||
)
|
||||
.userRole('BOTH')
|
||||
.execute());
|
||||
} else {
|
||||
// Create new User entity in PostgreSQL
|
||||
await executeProtected(() => _dataConnect.createUser(
|
||||
id: firebaseUser.uid,
|
||||
role: dc.UserBaseRole.USER,
|
||||
)
|
||||
.email(email)
|
||||
.userRole('BUSINESS')
|
||||
.execute());
|
||||
await _service.run(() => _service.connector
|
||||
.createUser(
|
||||
id: firebaseUser.uid,
|
||||
role: dc.UserBaseRole.USER,
|
||||
)
|
||||
.email(email)
|
||||
.userRole('BUSINESS')
|
||||
.execute());
|
||||
}
|
||||
|
||||
return _getUserProfile(
|
||||
@@ -298,7 +311,7 @@ class AuthRepositoryImpl
|
||||
// Delete business first (if created)
|
||||
if (businessId != null) {
|
||||
try {
|
||||
await _dataConnect.deleteBusiness(id: businessId).execute();
|
||||
await _service.connector.deleteBusiness(id: businessId).execute();
|
||||
} catch (_) {
|
||||
// Log but don't throw - we're already in error recovery
|
||||
}
|
||||
@@ -316,8 +329,9 @@ class AuthRepositoryImpl
|
||||
@override
|
||||
Future<void> signOut() async {
|
||||
try {
|
||||
await _firebaseAuth.signOut();
|
||||
await _service.auth.signOut();
|
||||
dc.ClientSessionStore.instance.clear();
|
||||
_service.clearCache();
|
||||
} catch (e) {
|
||||
throw Exception('Error signing out: ${e.toString()}');
|
||||
}
|
||||
@@ -325,7 +339,8 @@ class AuthRepositoryImpl
|
||||
|
||||
@override
|
||||
Future<domain.User> signInWithSocial({required String provider}) {
|
||||
throw UnimplementedError('Social authentication with $provider is not yet implemented.');
|
||||
throw UnimplementedError(
|
||||
'Social authentication with $provider is not yet implemented.');
|
||||
}
|
||||
|
||||
Future<domain.User> _getUserProfile({
|
||||
@@ -334,18 +349,24 @@ class AuthRepositoryImpl
|
||||
bool requireBusinessRole = false,
|
||||
}) async {
|
||||
final QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> response =
|
||||
await executeProtected(() => _dataConnect.getUserById(id: firebaseUserId).execute());
|
||||
await _service.run(() =>
|
||||
_service.connector.getUserById(id: firebaseUserId).execute());
|
||||
final dc.GetUserByIdUser? user = response.data.user;
|
||||
if (user == null) {
|
||||
throw UserNotFoundException(
|
||||
technicalMessage: 'Firebase UID $firebaseUserId not found in users table',
|
||||
technicalMessage:
|
||||
'Firebase UID $firebaseUserId not found in users table',
|
||||
);
|
||||
}
|
||||
if (requireBusinessRole && user.userRole != 'BUSINESS' && user.userRole != 'BOTH') {
|
||||
await _firebaseAuth.signOut();
|
||||
if (requireBusinessRole &&
|
||||
user.userRole != 'BUSINESS' &&
|
||||
user.userRole != 'BOTH') {
|
||||
await _service.auth.signOut();
|
||||
dc.ClientSessionStore.instance.clear();
|
||||
_service.clearCache();
|
||||
throw UnauthorizedAppException(
|
||||
technicalMessage: 'User role is ${user.userRole}, expected BUSINESS or BOTH',
|
||||
technicalMessage:
|
||||
'User role is ${user.userRole}, expected BUSINESS or BOTH',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -362,13 +383,17 @@ class AuthRepositoryImpl
|
||||
role: user.role.stringValue,
|
||||
);
|
||||
|
||||
final QueryResult<dc.GetBusinessesByUserIdData, dc.GetBusinessesByUserIdVariables> businessResponse =
|
||||
await executeProtected(() => _dataConnect.getBusinessesByUserId(
|
||||
userId: firebaseUserId,
|
||||
).execute());
|
||||
final dc.GetBusinessesByUserIdBusinesses? business = businessResponse.data.businesses.isNotEmpty
|
||||
? businessResponse.data.businesses.first
|
||||
: null;
|
||||
final QueryResult<dc.GetBusinessesByUserIdData,
|
||||
dc.GetBusinessesByUserIdVariables> businessResponse =
|
||||
await _service.run(() => _service.connector
|
||||
.getBusinessesByUserId(
|
||||
userId: firebaseUserId,
|
||||
)
|
||||
.execute());
|
||||
final dc.GetBusinessesByUserIdBusinesses? business =
|
||||
businessResponse.data.businesses.isNotEmpty
|
||||
? businessResponse.data.businesses.first
|
||||
: null;
|
||||
|
||||
dc.ClientSessionStore.instance.setSession(
|
||||
dc.ClientSession(
|
||||
|
||||
Reference in New Issue
Block a user