Fix the repeat registration with the email already verified (#7323)

#### 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
修复注册时未验证邮箱是否已被占用的问题
```
pull/7328/head
John Niang 2025-04-02 18:25:54 +08:00 committed by GitHub
parent 2a6bedc73d
commit fb7a09738a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 87 additions and 1 deletions

View File

@ -25,6 +25,8 @@ public interface UserService {
Flux<User> listByEmail(String email); Flux<User> listByEmail(String email);
Mono<Boolean> checkEmailAlreadyVerified(String email);
String encryptPassword(String rawPassword); String encryptPassword(String rawPassword);
Mono<User> disable(String username); Mono<User> disable(String username);

View File

@ -43,6 +43,7 @@ import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.SystemSetting; import run.halo.app.infra.SystemSetting;
import run.halo.app.infra.ValidationUtils; import run.halo.app.infra.ValidationUtils;
import run.halo.app.infra.exception.DuplicateNameException; 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.EmailVerificationFailed;
import run.halo.app.infra.exception.UnsatisfiedAttributeValueException; import run.halo.app.infra.exception.UnsatisfiedAttributeValueException;
import run.halo.app.infra.exception.UserNotFoundException; import run.halo.app.infra.exception.UserNotFoundException;
@ -206,7 +207,12 @@ public class UserServiceImpl implements UserService {
.switchIfEmpty(Mono.error(() -> .switchIfEmpty(Mono.error(() ->
new EmailVerificationFailed("Invalid email captcha.", null) 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(); .then();
} }
return verifyEmail.then(Mono.defer(() -> { return verifyEmail.then(Mono.defer(() -> {
@ -278,6 +284,14 @@ public class UserServiceImpl implements UserService {
return client.listAll(User.class, listOptions, defaultSort()); return client.listAll(User.class, listOptions, defaultSort());
} }
@Override
public Mono<Boolean> checkEmailAlreadyVerified(String email) {
return listByEmail(email)
// TODO Use index query in the future
.filter(u -> u.getSpec().isEmailVerified())
.hasElements();
}
@Override @Override
public String encryptPassword(String rawPassword) { public String encryptPassword(String rawPassword) {
return passwordEncoder.encode(rawPassword); return passwordEncoder.encode(rawPassword);

View File

@ -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);
}
}

View File

@ -31,6 +31,7 @@ import run.halo.app.core.user.service.SignUpData;
import run.halo.app.core.user.service.UserService; import run.halo.app.core.user.service.UserService;
import run.halo.app.infra.actuator.GlobalInfoService; import run.halo.app.infra.actuator.GlobalInfoService;
import run.halo.app.infra.exception.DuplicateNameException; 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.EmailVerificationFailed;
import run.halo.app.infra.exception.RateLimitExceededException; import run.halo.app.infra.exception.RateLimitExceededException;
import run.halo.app.infra.exception.RequestBodyValidationException; import run.halo.app.infra.exception.RequestBodyValidationException;
@ -111,6 +112,15 @@ class PreAuthSignUpEndpoint {
"Invalid Email Code")); "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, .doOnError(RateLimitExceededException.class,
e -> model.put("error", "rate-limit-exceeded") e -> model.put("error", "rate-limit-exceeded")
) )

View File

@ -88,6 +88,7 @@ problemDetail.comment.waitingForApproval=Comment is awaiting approval.
title.visibility.identification.private=(Private) title.visibility.identification.private=(Private)
signup.error.confirm-password-not-match=The confirmation password does not match the password. 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-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.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. validation.error.username.pattern=The username can only be lowercase and can only contain letters, numbers, hyphens, and dots, starting and ending with characters.

View File

@ -61,6 +61,7 @@ problemDetail.comment.waitingForApproval=评论审核中。
title.visibility.identification.private=(私有) title.visibility.identification.private=(私有)
signup.error.confirm-password-not-match=确认密码与密码不匹配。 signup.error.confirm-password-not-match=确认密码与密码不匹配。
signup.error.email-code.invalid=邮箱验证码无效。 signup.error.email-code.invalid=邮箱验证码无效。
signup.error.email.already-taken=邮箱地址已被注册。
validation.error.email.pattern=邮箱格式不正确 validation.error.email.pattern=邮箱格式不正确
validation.error.username.pattern=用户名只能小写且只能包含字母、数字、中划线和点,以字符开头和结尾 validation.error.username.pattern=用户名只能小写且只能包含字母、数字、中划线和点,以字符开头和结尾

View File

@ -11,6 +11,7 @@ import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.assertArg; import static org.mockito.ArgumentMatchers.assertArg;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA; import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
@ -30,6 +31,7 @@ import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Sort;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.ServerWebInputException;
import reactor.core.publisher.Flux; 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;
import run.halo.app.core.extension.RoleBinding.Subject; import run.halo.app.core.extension.RoleBinding.Subject;
import run.halo.app.core.extension.User; 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.RoleService;
import run.halo.app.core.user.service.SignUpData; import run.halo.app.core.user.service.SignUpData;
import run.halo.app.core.user.service.UserPostCreatingHandler; import run.halo.app.core.user.service.UserPostCreatingHandler;
import run.halo.app.core.user.service.UserPreCreatingHandler; import run.halo.app.core.user.service.UserPreCreatingHandler;
import run.halo.app.event.user.PasswordChangedEvent; import run.halo.app.event.user.PasswordChangedEvent;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.Metadata; import run.halo.app.extension.Metadata;
import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.exception.ExtensionNotFoundException; import run.halo.app.extension.exception.ExtensionNotFoundException;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.SystemSetting; import run.halo.app.infra.SystemSetting;
import run.halo.app.infra.exception.DuplicateNameException; 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.UnsatisfiedAttributeValueException;
import run.halo.app.infra.exception.UserNotFoundException; import run.halo.app.infra.exception.UserNotFoundException;
import run.halo.app.plugin.extensionpoint.ExtensionGetter; import run.halo.app.plugin.extensionpoint.ExtensionGetter;
@ -75,6 +80,9 @@ class UserServiceImplTest {
@Mock @Mock
ExtensionGetter extensionGetter; ExtensionGetter extensionGetter;
@Mock
EmailVerificationService emailVerificationService;
@InjectMocks @InjectMocks
UserServiceImpl userService; UserServiceImpl userService;
@ -375,6 +383,36 @@ class UserServiceImplTest {
.verify(); .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 @Test
void signUpWhenRegistrationSuccessfully() { void signUpWhenRegistrationSuccessfully() {
SystemSetting.User userSetting = new SystemSetting.User(); SystemSetting.User userSetting = new SystemSetting.User();