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