Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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)

AltStyle によって変換されたページ (->オリジナル) /