feat(authentication): add reauthenticate, updateEmail and updatePassword #62
@ -256,3 +256,24 @@ abstract class GetIdTokenFailureInterface
|
|||||||
|
|
||||||
GetIdTokenFailureInterface.fromCode(super.code) : super.fromCode();
|
GetIdTokenFailureInterface.fromCode(super.code) : super.fromCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract class ReauthenticateFailureInterface
|
||||||
|
extends AuthenticationFailureInterface {
|
||||||
|
ReauthenticateFailureInterface(super.code, super.msg);
|
||||||
|
|
||||||
|
ReauthenticateFailureInterface.fromCode(super.code) : super.fromCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class UpdateEmailFailureInterface
|
||||||
|
extends AuthenticationFailureInterface {
|
||||||
|
UpdateEmailFailureInterface(super.code, super.msg);
|
||||||
|
|
||||||
|
UpdateEmailFailureInterface.fromCode(super.code) : super.fromCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class UpdatePasswordFailureInterface
|
||||||
|
extends AuthenticationFailureInterface {
|
||||||
|
UpdatePasswordFailureInterface(super.code, super.msg);
|
||||||
|
|
||||||
|
UpdatePasswordFailureInterface.fromCode(super.code) : super.fromCode();
|
||||||
|
}
|
||||||
|
@ -277,3 +277,75 @@ class GetIdTokenFailureFirebase extends GetIdTokenFailureInterface {
|
|||||||
|
|
||||||
GetIdTokenFailureFirebase.fromCode(super.code) : super.fromCode();
|
GetIdTokenFailureFirebase.fromCode(super.code) : super.fromCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ReauthenticateFailureFirebase extends ReauthenticateFailureInterface {
|
||||||
|
ReauthenticateFailureFirebase([String? code, String? msg])
|
||||||
|
: super(code ?? 'unknown', msg ?? 'An unknown error occurred.');
|
||||||
|
ReauthenticateFailureFirebase.fromCode(String code) : super.fromCode(code) {
|
||||||
|
switch (code) {
|
||||||
|
case 'user-mismatch':
|
||||||
|
msg = 'Given credential does not correspond to the user.';
|
||||||
|
break;
|
||||||
|
case 'user-not-found':
|
||||||
|
msg = 'User is not found, please create an account.';
|
||||||
|
break;
|
||||||
|
case 'invalid-credential':
|
||||||
|
msg = 'The credential received is malformed or has expired.';
|
||||||
|
break;
|
||||||
|
case 'invalid-email':
|
||||||
|
msg = 'Email is not valid or badly formatted.';
|
||||||
|
break;
|
||||||
|
case 'wrong-password':
|
||||||
|
msg = 'Incorrect password, please try again.';
|
||||||
|
break;
|
||||||
|
case 'invalid-verification-code':
|
||||||
|
msg = 'The credential verification code received is invalid.';
|
||||||
|
break;
|
||||||
|
case 'invalid-verification-id':
|
||||||
|
msg = 'The credential verification ID received is invalid.';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.code = 'unknown';
|
||||||
|
msg = 'An unknown error occurred.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UpdateEmailFailureFirebase extends UpdateEmailFailureInterface {
|
||||||
|
UpdateEmailFailureFirebase([String? code, String? msg])
|
||||||
|
: super(code ?? 'unknown', msg ?? 'An unknown error occurred.');
|
||||||
|
UpdateEmailFailureFirebase.fromCode(String code) : super.fromCode(code) {
|
||||||
|
switch (code) {
|
||||||
|
case 'invalid-email':
|
||||||
|
msg = 'Email is not valid or badly formatted.';
|
||||||
|
break;
|
||||||
|
case 'email-already-in-use':
|
||||||
|
msg = 'An account already exists for that email.';
|
||||||
|
break;
|
||||||
|
case 'requires-recent-login':
|
||||||
|
msg = "User's last sign-in time does not meet the security threshold.";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.code = 'unknown';
|
||||||
|
msg = 'An unknown error occurred.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UpdatePasswordFailureFirebase extends UpdatePasswordFailureInterface {
|
||||||
|
UpdatePasswordFailureFirebase([String? code, String? msg])
|
||||||
|
: super(code ?? 'unknown', msg ?? 'An unknown error occurred.');
|
||||||
|
UpdatePasswordFailureFirebase.fromCode(String code) : super.fromCode(code) {
|
||||||
|
switch (code) {
|
||||||
|
case 'weak-password':
|
||||||
|
msg = 'Please enter a stronger password.';
|
||||||
|
break;
|
||||||
|
case 'requires-recent-login':
|
||||||
|
msg = "User's last sign-in time does not meet the security threshold.";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.code = 'unknown';
|
||||||
|
msg = 'An unknown error occurred.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -21,6 +21,7 @@ import 'package:wyatt_type_utils/wyatt_type_utils.dart';
|
|||||||
class AuthenticationFirebaseDataSourceImpl
|
class AuthenticationFirebaseDataSourceImpl
|
||||||
extends AuthenticationRemoteDataSource {
|
extends AuthenticationRemoteDataSource {
|
||||||
final FirebaseAuth _firebaseAuth;
|
final FirebaseAuth _firebaseAuth;
|
||||||
|
UserCredential? _latestCreds;
|
||||||
|
|
||||||
AuthenticationFirebaseDataSourceImpl({FirebaseAuth? firebaseAuth})
|
AuthenticationFirebaseDataSourceImpl({FirebaseAuth? firebaseAuth})
|
||||||
: _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance;
|
: _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance;
|
||||||
@ -51,6 +52,7 @@ class AuthenticationFirebaseDataSourceImpl
|
|||||||
email: email,
|
email: email,
|
||||||
password: password,
|
password: password,
|
||||||
);
|
);
|
||||||
|
_latestCreds = userCredential;
|
||||||
final user = userCredential.user;
|
final user = userCredential.user;
|
||||||
if (user.isNotNull) {
|
if (user.isNotNull) {
|
||||||
return _mapper(user!);
|
return _mapper(user!);
|
||||||
@ -76,6 +78,7 @@ class AuthenticationFirebaseDataSourceImpl
|
|||||||
email: email,
|
email: email,
|
||||||
password: password,
|
password: password,
|
||||||
);
|
);
|
||||||
|
_latestCreds = userCredential;
|
||||||
final user = userCredential.user;
|
final user = userCredential.user;
|
||||||
if (user.isNotNull) {
|
if (user.isNotNull) {
|
||||||
return _mapper(user!);
|
return _mapper(user!);
|
||||||
@ -92,6 +95,7 @@ class AuthenticationFirebaseDataSourceImpl
|
|||||||
@override
|
@override
|
||||||
Future<void> signOut() async {
|
Future<void> signOut() async {
|
||||||
try {
|
try {
|
||||||
|
_latestCreds = null;
|
||||||
await _firebaseAuth.signOut();
|
await _firebaseAuth.signOut();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
throw SignOutFailureFirebase();
|
throw SignOutFailureFirebase();
|
||||||
@ -164,6 +168,7 @@ class AuthenticationFirebaseDataSourceImpl
|
|||||||
Future<Account> signInAnonymously() async {
|
Future<Account> signInAnonymously() async {
|
||||||
try {
|
try {
|
||||||
final userCredential = await _firebaseAuth.signInAnonymously();
|
final userCredential = await _firebaseAuth.signInAnonymously();
|
||||||
|
_latestCreds = userCredential;
|
||||||
final user = userCredential.user;
|
final user = userCredential.user;
|
||||||
if (user.isNotNull) {
|
if (user.isNotNull) {
|
||||||
return _mapper(user!);
|
return _mapper(user!);
|
||||||
@ -199,4 +204,60 @@ class AuthenticationFirebaseDataSourceImpl
|
|||||||
throw RefreshFailureFirebase();
|
throw RefreshFailureFirebase();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Account> reauthenticateWithCredential() async {
|
||||||
|
try {
|
||||||
|
if (_latestCreds?.credential != null) {
|
||||||
|
await _firebaseAuth.currentUser
|
||||||
|
?.reauthenticateWithCredential(_latestCreds!.credential!);
|
||||||
|
} else {
|
||||||
|
throw Exception(); // Get caught just after.
|
||||||
|
}
|
||||||
|
final user = _firebaseAuth.currentUser;
|
||||||
|
if (user.isNotNull) {
|
||||||
|
return _mapper(user!);
|
||||||
|
} else {
|
||||||
|
throw Exception(); // Get caught just after.
|
||||||
|
}
|
||||||
|
} on FirebaseAuthException catch (e) {
|
||||||
|
throw ReauthenticateFailureFirebase.fromCode(e.code);
|
||||||
|
} catch (_) {
|
||||||
|
throw ReauthenticateFailureFirebase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Account> updateEmail({required String email}) async {
|
||||||
|
try {
|
||||||
|
await _firebaseAuth.currentUser!.updateEmail(email);
|
||||||
|
final user = _firebaseAuth.currentUser;
|
||||||
|
if (user.isNotNull) {
|
||||||
|
return _mapper(user!);
|
||||||
|
} else {
|
||||||
|
throw Exception(); // Get caught just after.
|
||||||
|
}
|
||||||
|
} on FirebaseAuthException catch (e) {
|
||||||
|
throw UpdateEmailFailureFirebase.fromCode(e.code);
|
||||||
|
} catch (_) {
|
||||||
|
throw UpdateEmailFailureFirebase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Account> updatePassword({required String password}) async {
|
||||||
|
try {
|
||||||
|
await _firebaseAuth.currentUser!.updatePassword(password);
|
||||||
|
final user = _firebaseAuth.currentUser;
|
||||||
|
if (user.isNotNull) {
|
||||||
|
return _mapper(user!);
|
||||||
|
} else {
|
||||||
|
throw Exception(); // Get caught just after.
|
||||||
|
}
|
||||||
|
} on FirebaseAuthException catch (e) {
|
||||||
|
throw UpdatePasswordFailureFirebase.fromCode(e.code);
|
||||||
|
} catch (_) {
|
||||||
|
throw UpdatePasswordFailureFirebase();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import 'package:wyatt_type_utils/wyatt_type_utils.dart';
|
|||||||
class AuthenticationMockDataSourceImpl extends AuthenticationRemoteDataSource {
|
class AuthenticationMockDataSourceImpl extends AuthenticationRemoteDataSource {
|
||||||
Pair<Account, String>? _connectedMock;
|
Pair<Account, String>? _connectedMock;
|
||||||
Pair<Account, String>? _registeredMock;
|
Pair<Account, String>? _registeredMock;
|
||||||
|
DateTime _lastSignInTime = DateTime.now();
|
||||||
final StreamController<Account?> _streamAccount = StreamController()
|
final StreamController<Account?> _streamAccount = StreamController()
|
||||||
..add(null);
|
..add(null);
|
||||||
|
|
||||||
@ -118,6 +119,7 @@ class AuthenticationMockDataSourceImpl extends AuthenticationRemoteDataSource {
|
|||||||
);
|
);
|
||||||
_streamAccount.add(mock);
|
_streamAccount.add(mock);
|
||||||
_connectedMock = _connectedMock?.copyWith(left: mock);
|
_connectedMock = _connectedMock?.copyWith(left: mock);
|
||||||
|
_lastSignInTime = DateTime.now();
|
||||||
return Future.value(mock);
|
return Future.value(mock);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,6 +151,7 @@ class AuthenticationMockDataSourceImpl extends AuthenticationRemoteDataSource {
|
|||||||
}
|
}
|
||||||
_streamAccount.add(_registeredMock!.left);
|
_streamAccount.add(_registeredMock!.left);
|
||||||
_connectedMock = _registeredMock!.copyWith();
|
_connectedMock = _registeredMock!.copyWith();
|
||||||
|
_lastSignInTime = DateTime.now();
|
||||||
return _registeredMock!.left!;
|
return _registeredMock!.left!;
|
||||||
}
|
}
|
||||||
throw SignInWithCredentialFailureFirebase();
|
throw SignInWithCredentialFailureFirebase();
|
||||||
@ -193,6 +196,7 @@ class AuthenticationMockDataSourceImpl extends AuthenticationRemoteDataSource {
|
|||||||
);
|
);
|
||||||
_streamAccount.add(mock);
|
_streamAccount.add(mock);
|
||||||
_registeredMock = Pair(mock, password);
|
_registeredMock = Pair(mock, password);
|
||||||
|
_lastSignInTime = DateTime.now();
|
||||||
return Future.value(mock);
|
return Future.value(mock);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,4 +208,45 @@ class AuthenticationMockDataSourceImpl extends AuthenticationRemoteDataSource {
|
|||||||
await _randomDelay();
|
await _randomDelay();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Account> reauthenticateWithCredential() async {
|
||||||
|
await _randomDelay();
|
||||||
|
if (_connectedMock.isNull) {
|
||||||
|
throw ReauthenticateFailureFirebase();
|
||||||
|
}
|
||||||
|
await refresh();
|
||||||
|
_lastSignInTime = DateTime.now();
|
||||||
|
return Future.value(_connectedMock?.left);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Account> updateEmail({required String email}) {
|
||||||
|
final before = DateTime.now().subtract(const Duration(seconds: 10));
|
||||||
|
if (_lastSignInTime.isBefore(before)) {
|
||||||
|
throw UpdateEmailFailureFirebase('requires-recent-login');
|
||||||
|
}
|
||||||
|
final refresh = DateTime.now();
|
||||||
|
final mock = (_connectedMock?.left as AccountModel?)
|
||||||
|
?.copyWith(lastSignInTime: refresh, email: email);
|
||||||
|
_connectedMock = _connectedMock?.copyWith(left: mock);
|
||||||
|
_registeredMock = _registeredMock?.copyWith(left: mock);
|
||||||
|
_streamAccount.add(mock);
|
||||||
|
return Future.value(_connectedMock?.left);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Account> updatePassword({required String password}) {
|
||||||
|
final before = DateTime.now().subtract(const Duration(seconds: 10));
|
||||||
|
if (_lastSignInTime.isBefore(before)) {
|
||||||
|
throw UpdatePasswordFailureFirebase('requires-recent-login');
|
||||||
|
}
|
||||||
|
final refresh = DateTime.now();
|
||||||
|
final mock = (_connectedMock?.left as AccountModel?)
|
||||||
|
?.copyWith(lastSignInTime: refresh);
|
||||||
|
_connectedMock = _connectedMock?.copyWith(left: mock, right: password);
|
||||||
|
_registeredMock = _registeredMock?.copyWith(left: mock, right: password);
|
||||||
|
_streamAccount.add(mock);
|
||||||
|
return Future.value(_connectedMock?.left);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -314,4 +314,38 @@ class AuthenticationRepositoryImpl<T extends Object>
|
|||||||
},
|
},
|
||||||
(error) => error,
|
(error) => error,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOrResult<Account> reauthenticateWithCredential() =>
|
||||||
|
Result.tryCatchAsync<Account, AppException, AppException>(
|
||||||
|
() async {
|
||||||
|
final account = await _authenticationRemoteDataSource
|
||||||
|
.reauthenticateWithCredential();
|
||||||
|
return account;
|
||||||
|
},
|
||||||
|
(error) => error,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOrResult<Account> updateEmail({required String email}) =>
|
||||||
|
Result.tryCatchAsync<Account, AppException, AppException>(
|
||||||
|
() async {
|
||||||
|
final account =
|
||||||
|
await _authenticationRemoteDataSource.updateEmail(email: email);
|
||||||
|
return account;
|
||||||
|
},
|
||||||
|
(error) => error,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOrResult<Account> updatePassword({required String password}) =>
|
||||||
|
Result.tryCatchAsync<Account, AppException, AppException>(
|
||||||
|
() async {
|
||||||
|
final account = await _authenticationRemoteDataSource.updatePassword(
|
||||||
|
password: password,
|
||||||
|
);
|
||||||
|
return account;
|
||||||
|
},
|
||||||
|
(error) => error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -48,4 +48,10 @@ abstract class AuthenticationRemoteDataSource extends BaseRemoteDataSource {
|
|||||||
Future<bool> verifyPasswordResetCode({required String code});
|
Future<bool> verifyPasswordResetCode({required String code});
|
||||||
|
|
||||||
Future<Account> signInAnonymously();
|
Future<Account> signInAnonymously();
|
||||||
|
|
||||||
|
Future<Account> updateEmail({required String email});
|
||||||
|
|
||||||
|
Future<Account> updatePassword({required String password});
|
||||||
|
|
||||||
|
Future<Account> reauthenticateWithCredential();
|
||||||
}
|
}
|
||||||
|
@ -84,12 +84,45 @@ abstract class AuthenticationRepository<T> extends BaseRepository {
|
|||||||
required String password,
|
required String password,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// {@template update_email}
|
||||||
|
/// Update or add [email].
|
||||||
|
///
|
||||||
|
/// Throws a UpdateEmailFailureInterface if
|
||||||
|
/// an exception occurs.
|
||||||
|
/// {@endtemplate}
|
||||||
|
FutureOrResult<Account> updateEmail({
|
||||||
|
required String email,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// {@template update_password}
|
||||||
|
/// Update or add [password].
|
||||||
|
///
|
||||||
|
/// Throws a UpdatePasswordFailureInterface if
|
||||||
|
/// an exception occurs.
|
||||||
|
/// {@endtemplate}
|
||||||
|
FutureOrResult<Account> updatePassword({
|
||||||
|
required String password,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// {@template reauthenticate}
|
||||||
|
/// Some security-sensitive actions—such as deleting an account,
|
||||||
|
/// setting a primary email address, and changing a password—require that
|
||||||
|
/// the user has recently signed in.
|
||||||
|
///
|
||||||
|
/// Throws a ReauthenticateFailureInterface if
|
||||||
|
/// an exception occurs.
|
||||||
|
/// {@endtemplate}
|
||||||
|
FutureOrResult<Account> reauthenticateWithCredential();
|
||||||
|
|
||||||
/// {@template signout}
|
/// {@template signout}
|
||||||
/// Signs out the current user.
|
/// Signs out the current user.
|
||||||
/// It also clears the cache and the associated data.
|
/// It also clears the cache and the associated data.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
FutureOrResult<void> signOut();
|
FutureOrResult<void> signOut();
|
||||||
|
|
||||||
|
/// {@template refresh}
|
||||||
|
/// Refreshes the current user, if signed in.
|
||||||
|
/// {@endtemplate}
|
||||||
FutureOrResult<void> refresh();
|
FutureOrResult<void> refresh();
|
||||||
|
|
||||||
/// {@template stream_account}
|
/// {@template stream_account}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user