Skip to content

Commit a77e7c0

Browse files
committed
feat(auth): Implement secure two-step email update flow
Added two new methods to `AuthService` to handle user email changes securely: - `initiateEmailUpdate`: Checks if the new email is available, then generates and sends a verification code to the new address. - `completeEmailUpdate`: Verifies the provided code and, upon success, updates the user's email in the database. This ensures that a user must prove ownership of the new email address before the change is finalized, enhancing account security.
1 parent f61fbf3 commit a77e7c0

File tree

1 file changed

+107
-0
lines changed

1 file changed

+107
-0
lines changed

lib/src/services/auth_service.dart

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,4 +611,111 @@ class AuthService {
611611

612612
return (user: permanentUser, token: newToken);
613613
}
614+
615+
/// Initiates the process of updating a user's email address.
616+
///
617+
/// This is the first step in a two-step verification process. It checks if
618+
/// the new email is already in use, then generates and sends a verification
619+
/// code to that new email address.
620+
///
621+
/// - [user]: The currently authenticated user initiating the change.
622+
/// - [newEmail]: The desired new email address.
623+
///
624+
/// Throws [ConflictException] if the `newEmail` is already taken by another
625+
/// user.
626+
/// Throws [OperationFailedException] for other unexpected errors.
627+
Future<void> initiateEmailUpdate({
628+
required User user,
629+
required String newEmail,
630+
}) async {
631+
_log.info(
632+
'User ${user.id} is initiating an email update to "$newEmail".',
633+
);
634+
635+
try {
636+
// 1. Check if the new email address is already in use.
637+
final existingUser = await _findUserByEmail(newEmail);
638+
if (existingUser != null) {
639+
_log.warning(
640+
'Email update failed for user ${user.id}: new email "$newEmail" is already in use by user ${existingUser.id}.',
641+
);
642+
throw const ConflictException(
643+
'This email address is already registered.',
644+
);
645+
}
646+
_log.finer('New email "$newEmail" is available.');
647+
648+
// 2. Generate and send a verification code to the new email.
649+
// We reuse the sign-in code mechanism for this verification step.
650+
final code = await _verificationCodeStorageService
651+
.generateAndStoreSignInCode(newEmail);
652+
_log.finer('Generated verification code for "$newEmail".');
653+
654+
await _emailRepository.sendOtpEmail(
655+
senderEmail: EnvironmentConfig.defaultSenderEmail,
656+
recipientEmail: newEmail,
657+
templateId: EnvironmentConfig.otpTemplateId,
658+
subject: 'Verify your new email address',
659+
otpCode: code,
660+
);
661+
_log.info('Sent email update verification code to "$newEmail".');
662+
} on HttpException {
663+
// Propagate known exceptions (like ConflictException).
664+
rethrow;
665+
} catch (e, s) {
666+
_log.severe(
667+
'Unexpected error during initiateEmailUpdate for user ${user.id}.',
668+
e,
669+
s,
670+
);
671+
throw const OperationFailedException(
672+
'Failed to initiate email update process.',
673+
);
674+
}
675+
}
676+
677+
/// Completes the email update process by verifying the code and updating
678+
/// the user's record.
679+
///
680+
/// - [user]: The currently authenticated user.
681+
/// - [newEmail]: The new email address being verified.
682+
/// - [code]: The verification code sent to the new email address.
683+
///
684+
/// Returns the updated [User] object upon success.
685+
///
686+
/// Throws [InvalidInputException] if the verification code is invalid.
687+
/// Throws [OperationFailedException] for other unexpected errors.
688+
Future<User> completeEmailUpdate({
689+
required User user,
690+
required String newEmail,
691+
required String code,
692+
}) async {
693+
_log.info('User ${user.id} is completing email update to "$newEmail".');
694+
695+
// 1. Validate the verification code for the new email.
696+
final isValid = await _verificationCodeStorageService.validateSignInCode(
697+
newEmail,
698+
code,
699+
);
700+
if (!isValid) {
701+
_log.warning('Invalid verification code provided for "$newEmail".');
702+
throw const InvalidInputException(
703+
'Invalid or expired verification code.',
704+
);
705+
}
706+
_log.finer('Verification code for "$newEmail" is valid.');
707+
708+
// 2. Clear the used code from storage.
709+
await _verificationCodeStorageService.clearSignInCode(newEmail);
710+
711+
// 3. Update the user's email in the repository.
712+
final updatedUser = user.copyWith(email: newEmail);
713+
final finalUser = await _userRepository.update(
714+
id: user.id,
715+
item: updatedUser,
716+
);
717+
_log.info('Successfully updated email for user ${user.id} to "$newEmail".');
718+
719+
return finalUser;
720+
}
614721
}

0 commit comments

Comments
 (0)