From fb7a09738a783d2c7dcd82e78a960956b2321092 Mon Sep 17 00:00:00 2001 From: John Niang Date: Wed, 2 Apr 2025 18:25:54 +0800 Subject: [PATCH] Fix the repeat registration with the email already verified (#7323) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind bug /area core /milestone 2.20.x #### What this PR does / why we need it: This PR fixes the repeat registration with the email already verified. ![Screenshot From 2025-04-02 16-33-22](https://github.com/user-attachments/assets/1caf0550-f80f-42e4-8db6-747ff1035f63) #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/7308 #### Does this PR introduce a user-facing change? ```release-note 修复注册时未验证邮箱是否已被占用的问题 ``` --- .../app/core/user/service/UserService.java | 2 + .../user/service/impl/UserServiceImpl.java | 16 +++++++- .../exception/EmailAlreadyTakenException.java | 20 ++++++++++ .../preauth/PreAuthSignUpEndpoint.java | 10 +++++ .../resources/config/i18n/messages.properties | 1 + .../config/i18n/messages_zh.properties | 1 + .../service/impl/UserServiceImplTest.java | 38 +++++++++++++++++++ 7 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 application/src/main/java/run/halo/app/infra/exception/EmailAlreadyTakenException.java diff --git a/api/src/main/java/run/halo/app/core/user/service/UserService.java b/api/src/main/java/run/halo/app/core/user/service/UserService.java index cc603378b..d1061b8ba 100644 --- a/api/src/main/java/run/halo/app/core/user/service/UserService.java +++ b/api/src/main/java/run/halo/app/core/user/service/UserService.java @@ -25,6 +25,8 @@ public interface UserService { Flux listByEmail(String email); + Mono checkEmailAlreadyVerified(String email); + String encryptPassword(String rawPassword); Mono disable(String username); diff --git a/application/src/main/java/run/halo/app/core/user/service/impl/UserServiceImpl.java b/application/src/main/java/run/halo/app/core/user/service/impl/UserServiceImpl.java index 389eb0c69..bc05a016d 100644 --- a/application/src/main/java/run/halo/app/core/user/service/impl/UserServiceImpl.java +++ b/application/src/main/java/run/halo/app/core/user/service/impl/UserServiceImpl.java @@ -43,6 +43,7 @@ import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemSetting; import run.halo.app.infra.ValidationUtils; import run.halo.app.infra.exception.DuplicateNameException; +import run.halo.app.infra.exception.EmailAlreadyTakenException; import run.halo.app.infra.exception.EmailVerificationFailed; import run.halo.app.infra.exception.UnsatisfiedAttributeValueException; import run.halo.app.infra.exception.UserNotFoundException; @@ -206,7 +207,12 @@ public class UserServiceImpl implements UserService { .switchIfEmpty(Mono.error(() -> new EmailVerificationFailed("Invalid email captcha.", null) )) - .doOnNext(spec::setEmailVerified) + .then(this.checkEmailAlreadyVerified(signUpData.getEmail())) + .filter(has -> !has) + .switchIfEmpty(Mono.error( + () -> new EmailAlreadyTakenException("Email is already taken") + )) + .doOnNext(v -> spec.setEmailVerified(true)) .then(); } return verifyEmail.then(Mono.defer(() -> { @@ -278,6 +284,14 @@ public class UserServiceImpl implements UserService { return client.listAll(User.class, listOptions, defaultSort()); } + @Override + public Mono checkEmailAlreadyVerified(String email) { + return listByEmail(email) + // TODO Use index query in the future + .filter(u -> u.getSpec().isEmailVerified()) + .hasElements(); + } + @Override public String encryptPassword(String rawPassword) { return passwordEncoder.encode(rawPassword); diff --git a/application/src/main/java/run/halo/app/infra/exception/EmailAlreadyTakenException.java b/application/src/main/java/run/halo/app/infra/exception/EmailAlreadyTakenException.java new file mode 100644 index 000000000..f83f6f179 --- /dev/null +++ b/application/src/main/java/run/halo/app/infra/exception/EmailAlreadyTakenException.java @@ -0,0 +1,20 @@ +package run.halo.app.infra.exception; + +import java.net.URI; +import org.springframework.web.server.ServerWebInputException; + +/** + * Exception thrown when email is already verified and taken. + * + * @author johnniang + */ +public class EmailAlreadyTakenException extends ServerWebInputException { + + public static final URI TYPE = URI.create("https://halo.run/errors/email-already-taken"); + + public EmailAlreadyTakenException(String reason) { + super(reason); + setType(TYPE); + } + +} diff --git a/application/src/main/java/run/halo/app/security/preauth/PreAuthSignUpEndpoint.java b/application/src/main/java/run/halo/app/security/preauth/PreAuthSignUpEndpoint.java index 62ab62ed0..72e7d2ff1 100644 --- a/application/src/main/java/run/halo/app/security/preauth/PreAuthSignUpEndpoint.java +++ b/application/src/main/java/run/halo/app/security/preauth/PreAuthSignUpEndpoint.java @@ -31,6 +31,7 @@ import run.halo.app.core.user.service.SignUpData; import run.halo.app.core.user.service.UserService; import run.halo.app.infra.actuator.GlobalInfoService; import run.halo.app.infra.exception.DuplicateNameException; +import run.halo.app.infra.exception.EmailAlreadyTakenException; import run.halo.app.infra.exception.EmailVerificationFailed; import run.halo.app.infra.exception.RateLimitExceededException; import run.halo.app.infra.exception.RequestBodyValidationException; @@ -111,6 +112,15 @@ class PreAuthSignUpEndpoint { "Invalid Email Code")); } ) + .doOnError(EmailAlreadyTakenException.class, e -> { + bindingResult.addError(new FieldError("form", + "email", + signUpData.getEmail(), + true, + new String[] {"signup.error.email.already-taken"}, + null, + "Email Already Taken")); + }) .doOnError(RateLimitExceededException.class, e -> model.put("error", "rate-limit-exceeded") ) diff --git a/application/src/main/resources/config/i18n/messages.properties b/application/src/main/resources/config/i18n/messages.properties index 81e36aa05..44e576e21 100644 --- a/application/src/main/resources/config/i18n/messages.properties +++ b/application/src/main/resources/config/i18n/messages.properties @@ -88,6 +88,7 @@ problemDetail.comment.waitingForApproval=Comment is awaiting approval. title.visibility.identification.private=(Private) signup.error.confirm-password-not-match=The confirmation password does not match the password. signup.error.email-code.invalid=Invalid email code. +signup.error.email.already-taken=Email address is already taken. validation.error.email.pattern=The email format is incorrect validation.error.username.pattern=The username can only be lowercase and can only contain letters, numbers, hyphens, and dots, starting and ending with characters. diff --git a/application/src/main/resources/config/i18n/messages_zh.properties b/application/src/main/resources/config/i18n/messages_zh.properties index bdab92ac7..a19a2604e 100644 --- a/application/src/main/resources/config/i18n/messages_zh.properties +++ b/application/src/main/resources/config/i18n/messages_zh.properties @@ -61,6 +61,7 @@ problemDetail.comment.waitingForApproval=评论审核中。 title.visibility.identification.private=(私有) signup.error.confirm-password-not-match=确认密码与密码不匹配。 signup.error.email-code.invalid=邮箱验证码无效。 +signup.error.email.already-taken=邮箱地址已被注册。 validation.error.email.pattern=邮箱格式不正确 validation.error.username.pattern=用户名只能小写且只能包含字母、数字、中划线和点,以字符开头和结尾 diff --git a/application/src/test/java/run/halo/app/core/user/service/impl/UserServiceImplTest.java b/application/src/test/java/run/halo/app/core/user/service/impl/UserServiceImplTest.java index 9b1a5f557..807262d94 100644 --- a/application/src/test/java/run/halo/app/core/user/service/impl/UserServiceImplTest.java +++ b/application/src/test/java/run/halo/app/core/user/service/impl/UserServiceImplTest.java @@ -11,6 +11,7 @@ import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.assertArg; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -30,6 +31,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.Sort; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.server.ServerWebInputException; import reactor.core.publisher.Flux; @@ -39,17 +41,20 @@ import run.halo.app.core.extension.Role; import run.halo.app.core.extension.RoleBinding; import run.halo.app.core.extension.RoleBinding.Subject; import run.halo.app.core.extension.User; +import run.halo.app.core.user.service.EmailVerificationService; import run.halo.app.core.user.service.RoleService; import run.halo.app.core.user.service.SignUpData; import run.halo.app.core.user.service.UserPostCreatingHandler; import run.halo.app.core.user.service.UserPreCreatingHandler; import run.halo.app.event.user.PasswordChangedEvent; +import run.halo.app.extension.ListOptions; import run.halo.app.extension.Metadata; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.exception.ExtensionNotFoundException; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemSetting; import run.halo.app.infra.exception.DuplicateNameException; +import run.halo.app.infra.exception.EmailAlreadyTakenException; import run.halo.app.infra.exception.UnsatisfiedAttributeValueException; import run.halo.app.infra.exception.UserNotFoundException; import run.halo.app.plugin.extensionpoint.ExtensionGetter; @@ -75,6 +80,9 @@ class UserServiceImplTest { @Mock ExtensionGetter extensionGetter; + @Mock + EmailVerificationService emailVerificationService; + @InjectMocks UserServiceImpl userService; @@ -375,6 +383,36 @@ class UserServiceImplTest { .verify(); } + @Test + void signUpWhenEmailAlreadyTaken() { + SystemSetting.User userSetting = new SystemSetting.User(); + userSetting.setAllowRegistration(true); + userSetting.setMustVerifyEmailOnRegistration(true); + userSetting.setDefaultRole("fake-role"); + when(environmentFetcher.fetch(eq(SystemSetting.User.GROUP), + eq(SystemSetting.User.class))) + .thenReturn(Mono.just(userSetting)); + when(passwordEncoder.encode(eq("fake-password"))).thenReturn("fake-password"); + when(emailVerificationService.verifyRegisterVerificationCode("fake@example.com", + "fakeCode")) + .thenReturn(Mono.just(true)); + when(client.listAll(same(User.class), any(ListOptions.class), any(Sort.class))) + .thenReturn(Flux.from(Mono.fromSupplier(() -> { + var user = new User(); + user.setSpec(new User.UserSpec()); + user.getSpec().setEmailVerified(true); + return user; + }))); + + var signUpData = createSignUpData("fake-user", "fake-password"); + signUpData.setEmail("fake@example.com"); + signUpData.setEmailCode("fakeCode"); + userService.signUp(signUpData) + .as(StepVerifier::create) + .expectError(EmailAlreadyTakenException.class) + .verify(); + } + @Test void signUpWhenRegistrationSuccessfully() { SystemSetting.User userSetting = new SystemSetting.User();