From 5b05b54502511b48c396339ab97296013c8e0eed Mon Sep 17 00:00:00 2001 From: John Niang Date: Wed, 2 Apr 2025 16:43:57 +0800 Subject: [PATCH] Fix the repeat registration with the same email verified --- .../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 cc603378b1..d1061b8bab 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 389eb0c697..bc05a016d6 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.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 Mono signUp(SignUpData signUpData) { .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 Flux listByEmail(String email) { 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 0000000000..f83f6f1792 --- /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 62ab62ed03..72e7d2ff18 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.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 @@ RouterFunction preAuthSignUpEndpoints() { "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 81e36aa05b..44e576e21b 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 bdab92ac74..a19a2604e3 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 9b1a5f5571..807262d94c 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.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.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.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 @@ void signUpWhenRegistrationUsernameExists() { .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();