mirror of https://github.com/halo-dev/halo
Merge pull request #6804 from guqing/refactor/user-validation
refactor: unified validation for username and password formatpull/6806/head
commit
158c3e8a9e
|
@ -24,6 +24,7 @@ import io.github.resilience4j.ratelimiter.RequestNotPermitted;
|
|||
import io.github.resilience4j.reactor.ratelimiter.operator.RateLimiterOperator;
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import java.security.Principal;
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
|
@ -58,6 +59,8 @@ import org.springframework.util.Assert;
|
|||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
import org.springframework.validation.BeanPropertyBindingResult;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.web.reactive.function.BodyExtractors;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
|
@ -65,7 +68,6 @@ import org.springframework.web.reactive.function.server.ServerResponse;
|
|||
import org.springframework.web.server.ServerWebInputException;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.function.Tuples;
|
||||
import reactor.util.retry.Retry;
|
||||
import run.halo.app.core.extension.Role;
|
||||
import run.halo.app.core.extension.User;
|
||||
|
@ -84,7 +86,6 @@ import run.halo.app.extension.ReactiveExtensionClient;
|
|||
import run.halo.app.extension.router.SortableRequest;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.infra.ValidationUtils;
|
||||
import run.halo.app.infra.exception.RateLimitExceededException;
|
||||
import run.halo.app.infra.exception.UnsatisfiedAttributeValueException;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
@ -104,6 +105,7 @@ public class UserEndpoint implements CustomEndpoint {
|
|||
private final EmailVerificationService emailVerificationService;
|
||||
private final RateLimiterRegistry rateLimiterRegistry;
|
||||
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
private final Validator validator;
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> endpoint() {
|
||||
|
@ -280,7 +282,9 @@ public class UserEndpoint implements CustomEndpoint {
|
|||
.onErrorMap(RequestNotPermitted.class, RateLimitExceededException::new);
|
||||
}
|
||||
|
||||
public record EmailVerifyRequest(@Schema(requiredMode = REQUIRED) String email) {
|
||||
public record EmailVerifyRequest(@Schema(requiredMode = REQUIRED)
|
||||
@Email
|
||||
String email) {
|
||||
}
|
||||
|
||||
public record VerifyCodeRequest(
|
||||
|
@ -289,25 +293,23 @@ public class UserEndpoint implements CustomEndpoint {
|
|||
}
|
||||
|
||||
private Mono<ServerResponse> sendEmailVerificationCode(ServerRequest request) {
|
||||
return request.bodyToMono(EmailVerifyRequest.class)
|
||||
var emailMono = request.bodyToMono(EmailVerifyRequest.class)
|
||||
.switchIfEmpty(Mono.error(
|
||||
() -> new ServerWebInputException("Request body is required."))
|
||||
)
|
||||
.doOnNext(emailRequest -> {
|
||||
if (!ValidationUtils.isValidEmail(emailRequest.email())) {
|
||||
throw new ServerWebInputException("Invalid email address.");
|
||||
.doOnNext(emailReq -> {
|
||||
var bindingResult = new BeanPropertyBindingResult(emailReq, "form");
|
||||
validator.validate(emailReq, bindingResult);
|
||||
if (bindingResult.hasErrors()) {
|
||||
// only email field is validated
|
||||
throw new ServerWebInputException("validation.error.email.pattern");
|
||||
}
|
||||
})
|
||||
.flatMap(emailRequest -> {
|
||||
var email = emailRequest.email();
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map(SecurityContext::getAuthentication)
|
||||
.map(Principal::getName)
|
||||
.map(username -> Tuples.of(username, email));
|
||||
})
|
||||
.map(EmailVerifyRequest::email);
|
||||
return Mono.zip(emailMono, getAuthenticatedUserName())
|
||||
.flatMap(tuple -> {
|
||||
var username = tuple.getT1();
|
||||
var email = tuple.getT2();
|
||||
var email = tuple.getT1();
|
||||
var username = tuple.getT2();
|
||||
return Mono.just(username)
|
||||
.transformDeferred(sendEmailVerificationCodeRateLimiter(username, email))
|
||||
.flatMap(u -> emailVerificationService.sendVerificationCode(username, email))
|
||||
|
@ -346,9 +348,7 @@ public class UserEndpoint implements CustomEndpoint {
|
|||
if (!SELF_USER.equals(name)) {
|
||||
return client.get(User.class, name);
|
||||
}
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map(SecurityContext::getAuthentication)
|
||||
.map(Authentication::getName)
|
||||
return getAuthenticatedUserName()
|
||||
.flatMap(currentUserName -> client.get(User.class, currentUserName));
|
||||
}
|
||||
|
||||
|
@ -501,9 +501,7 @@ public class UserEndpoint implements CustomEndpoint {
|
|||
}
|
||||
|
||||
private Mono<ServerResponse> updateProfile(ServerRequest request) {
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map(SecurityContext::getAuthentication)
|
||||
.map(Authentication::getName)
|
||||
return getAuthenticatedUserName()
|
||||
.flatMap(currentUserName -> client.get(User.class, currentUserName))
|
||||
.flatMap(currentUser -> request.bodyToMono(User.class)
|
||||
.filter(user -> user.getMetadata() != null
|
||||
|
@ -538,6 +536,12 @@ public class UserEndpoint implements CustomEndpoint {
|
|||
.flatMap(updatedUser -> ServerResponse.ok().bodyValue(updatedUser));
|
||||
}
|
||||
|
||||
private static Mono<String> getAuthenticatedUserName() {
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map(SecurityContext::getAuthentication)
|
||||
.map(Authentication::getName);
|
||||
}
|
||||
|
||||
Mono<ServerResponse> changeAnyonePasswordForAdmin(ServerRequest request) {
|
||||
final var nameInPath = request.pathVariable("name");
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
|
|
|
@ -6,6 +6,7 @@ import jakarta.validation.ConstraintValidatorContext;
|
|||
import jakarta.validation.Payload;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
@ -15,6 +16,7 @@ import java.util.Optional;
|
|||
import lombok.Data;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import run.halo.app.infra.ValidationUtils;
|
||||
|
||||
/**
|
||||
* Sign up data.
|
||||
|
@ -27,6 +29,8 @@ import org.springframework.util.StringUtils;
|
|||
public class SignUpData {
|
||||
|
||||
@NotBlank
|
||||
@Pattern(regexp = ValidationUtils.NAME_REGEX,
|
||||
message = "{validation.error.username.pattern}")
|
||||
private String username;
|
||||
|
||||
@NotBlank
|
||||
|
@ -38,6 +42,8 @@ public class SignUpData {
|
|||
private String emailCode;
|
||||
|
||||
@NotBlank
|
||||
@Pattern(regexp = ValidationUtils.PASSWORD_REGEX,
|
||||
message = "{validation.error.password.pattern}")
|
||||
private String password;
|
||||
|
||||
@NotBlank
|
||||
|
@ -79,9 +85,9 @@ public class SignUpData {
|
|||
|
||||
String message() default "";
|
||||
|
||||
Class<?>[] groups() default { };
|
||||
Class<?>[] groups() default {};
|
||||
|
||||
Class<? extends Payload>[] payload() default { };
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -33,8 +33,10 @@ import run.halo.app.extension.exception.ExtensionNotFoundException;
|
|||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
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.EmailVerificationFailed;
|
||||
import run.halo.app.infra.exception.UnsatisfiedAttributeValueException;
|
||||
import run.halo.app.infra.exception.UserNotFoundException;
|
||||
|
||||
@Service
|
||||
|
@ -86,6 +88,10 @@ public class UserServiceImpl implements UserService {
|
|||
|
||||
@Override
|
||||
public Mono<User> updateWithRawPassword(String username, String rawPassword) {
|
||||
if (!ValidationUtils.PASSWORD_PATTERN.matcher(rawPassword).matches()) {
|
||||
return Mono.error(
|
||||
new UnsatisfiedAttributeValueException("validation.error.password.pattern"));
|
||||
}
|
||||
return getUser(username)
|
||||
.filter(user -> {
|
||||
if (!StringUtils.hasText(user.getSpec().getPassword())) {
|
||||
|
|
|
@ -2,7 +2,6 @@ package run.halo.app.infra;
|
|||
|
||||
import java.util.regex.Pattern;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
@UtilityClass
|
||||
public class ValidationUtils {
|
||||
|
@ -11,36 +10,9 @@ public class ValidationUtils {
|
|||
public static final Pattern NAME_PATTERN = Pattern.compile(NAME_REGEX);
|
||||
|
||||
/**
|
||||
* No Chinese, no spaces.
|
||||
* A-Z, a-z, 0-9, !@#$%^&* are allowed.
|
||||
*/
|
||||
public static final String PASSWORD_REGEX = "^(?!.*[\\u4e00-\\u9fa5])(?=\\S+$).+$";
|
||||
public static final String PASSWORD_REGEX = "^[A-Za-z0-9!@#$%^&*]+$";
|
||||
|
||||
public static final String EMAIL_REGEX =
|
||||
"^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
|
||||
|
||||
public static final String NAME_VALIDATION_MESSAGE = """
|
||||
Super administrator username must be a valid subdomain name, the name must:
|
||||
1. contain no more than 63 characters
|
||||
2. contain only lowercase alphanumeric characters, '-' or '.'
|
||||
3. start with an alphanumeric character
|
||||
4. end with an alphanumeric character
|
||||
""";
|
||||
|
||||
/**
|
||||
* Validates the name.
|
||||
*
|
||||
* @param name name for validation
|
||||
* @return true if the name is valid
|
||||
*/
|
||||
public static boolean validateName(String name) {
|
||||
if (StringUtils.isBlank(name)) {
|
||||
return false;
|
||||
}
|
||||
boolean matches = NAME_PATTERN.matcher(name).matches();
|
||||
return matches && name.length() <= 63;
|
||||
}
|
||||
|
||||
public static boolean isValidEmail(String email) {
|
||||
return StringUtils.isNotBlank(email) && email.matches(EMAIL_REGEX);
|
||||
}
|
||||
public static final Pattern PASSWORD_PATTERN = Pattern.compile(PASSWORD_REGEX);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,10 @@ import org.springframework.web.server.ServerWebInputException;
|
|||
*/
|
||||
public class UnsatisfiedAttributeValueException extends ServerWebInputException {
|
||||
|
||||
public UnsatisfiedAttributeValueException(String reason) {
|
||||
super(reason);
|
||||
}
|
||||
|
||||
public UnsatisfiedAttributeValueException(String reason, @Nullable String messageDetailCode,
|
||||
@Null Object[] messageDetailArguments) {
|
||||
super(reason, null, null, messageDetailCode, messageDetailArguments);
|
||||
|
|
|
@ -88,5 +88,6 @@ 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.
|
||||
|
||||
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.password.pattern=The password cannot contain Chinese characters and spaces.
|
||||
validation.error.password.pattern=The password can only use uppercase and lowercase letters (A-Z, a-z), numbers (0-9), and the following special characters: !@#$%^&*
|
||||
|
|
|
@ -61,5 +61,6 @@ title.visibility.identification.private=(私有)
|
|||
signup.error.confirm-password-not-match=确认密码与密码不匹配。
|
||||
signup.error.email-code.invalid=邮箱验证码无效。
|
||||
|
||||
validation.error.email.pattern=邮箱格式不正确
|
||||
validation.error.username.pattern=用户名只能小写且只能包含字母、数字、中划线和点,以字符开头和结尾
|
||||
validation.error.password.pattern=密码不能包含中文和空格
|
||||
validation.error.password.pattern=密码只能使用大小写字母 (A-Z, a-z)、数字 (0-9),以及以下特殊字符: !@#$%^&*
|
|
@ -17,6 +17,7 @@ import org.springframework.http.HttpStatus;
|
|||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.validation.Validator;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.User;
|
||||
import run.halo.app.core.user.service.EmailVerificationService;
|
||||
|
@ -50,6 +51,9 @@ class EmailVerificationCodeTest {
|
|||
@Mock
|
||||
RateLimiterRegistry rateLimiterRegistry;
|
||||
|
||||
@Mock
|
||||
Validator validator;
|
||||
|
||||
@InjectMocks
|
||||
UserEndpoint endpoint;
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ 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.UnsatisfiedAttributeValueException;
|
||||
import run.halo.app.infra.exception.UserNotFoundException;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
|
@ -116,25 +117,25 @@ class UserServiceImplTest {
|
|||
|
||||
@Test
|
||||
void shouldUpdatePasswordWithDifferentPassword() {
|
||||
var oldUser = createUser("fake-password");
|
||||
var newUser = createUser("new-password");
|
||||
var oldUser = createUser("fake@password");
|
||||
var newUser = createUser("new@password");
|
||||
|
||||
when(client.get(User.class, "fake-user")).thenReturn(
|
||||
Mono.just(oldUser));
|
||||
when(client.update(eq(oldUser))).thenReturn(Mono.just(newUser));
|
||||
when(passwordEncoder.matches("new-password", "fake-password")).thenReturn(false);
|
||||
when(passwordEncoder.encode("new-password")).thenReturn("encoded-new-password");
|
||||
when(passwordEncoder.matches("new@password", "fake@password")).thenReturn(false);
|
||||
when(passwordEncoder.encode("new@password")).thenReturn("encoded@new@password");
|
||||
|
||||
StepVerifier.create(userService.updateWithRawPassword("fake-user", "new-password"))
|
||||
StepVerifier.create(userService.updateWithRawPassword("fake-user", "new@password"))
|
||||
.expectNext(newUser)
|
||||
.verifyComplete();
|
||||
|
||||
verify(passwordEncoder).matches("new-password", "fake-password");
|
||||
verify(passwordEncoder).encode("new-password");
|
||||
verify(passwordEncoder).matches("new@password", "fake@password");
|
||||
verify(passwordEncoder).encode("new@password");
|
||||
verify(client).get(User.class, "fake-user");
|
||||
verify(client).update(argThat(extension -> {
|
||||
var user = (User) extension;
|
||||
return "encoded-new-password".equals(user.getSpec().getPassword());
|
||||
return "encoded@new@password".equals(user.getSpec().getPassword());
|
||||
}));
|
||||
verify(eventPublisher).publishEvent(any(PasswordChangedEvent.class));
|
||||
}
|
||||
|
@ -142,21 +143,21 @@ class UserServiceImplTest {
|
|||
@Test
|
||||
void shouldUpdatePasswordIfNoPasswordBefore() {
|
||||
var oldUser = createUser(null);
|
||||
var newUser = createUser("new-password");
|
||||
var newUser = createUser("new@password");
|
||||
|
||||
when(client.get(User.class, "fake-user")).thenReturn(Mono.just(oldUser));
|
||||
when(client.update(oldUser)).thenReturn(Mono.just(newUser));
|
||||
when(passwordEncoder.encode("new-password")).thenReturn("encoded-new-password");
|
||||
when(passwordEncoder.encode("new@password")).thenReturn("encoded@new@password");
|
||||
|
||||
StepVerifier.create(userService.updateWithRawPassword("fake-user", "new-password"))
|
||||
StepVerifier.create(userService.updateWithRawPassword("fake-user", "new@password"))
|
||||
.expectNext(newUser)
|
||||
.verifyComplete();
|
||||
|
||||
verify(passwordEncoder, never()).matches("new-password", null);
|
||||
verify(passwordEncoder).encode("new-password");
|
||||
verify(passwordEncoder, never()).matches("new@password", null);
|
||||
verify(passwordEncoder).encode("new@password");
|
||||
verify(client).update(argThat(extension -> {
|
||||
var user = (User) extension;
|
||||
return "encoded-new-password".equals(user.getSpec().getPassword());
|
||||
return "encoded@new@password".equals(user.getSpec().getPassword());
|
||||
}));
|
||||
verify(client).get(User.class, "fake-user");
|
||||
verify(eventPublisher).publishEvent(any(PasswordChangedEvent.class));
|
||||
|
@ -166,16 +167,16 @@ class UserServiceImplTest {
|
|||
void shouldDoNothingIfPasswordNotChanged() {
|
||||
userService = spy(userService);
|
||||
|
||||
var oldUser = createUser("fake-password");
|
||||
var newUser = createUser("new-password");
|
||||
var oldUser = createUser("fake@password");
|
||||
var newUser = createUser("new@password");
|
||||
when(client.get(User.class, "fake-user")).thenReturn(Mono.just(oldUser));
|
||||
when(passwordEncoder.matches("fake-password", "fake-password")).thenReturn(true);
|
||||
when(passwordEncoder.matches("fake@password", "fake@password")).thenReturn(true);
|
||||
|
||||
StepVerifier.create(userService.updateWithRawPassword("fake-user", "fake-password"))
|
||||
StepVerifier.create(userService.updateWithRawPassword("fake-user", "fake@password"))
|
||||
.expectNextCount(0)
|
||||
.verifyComplete();
|
||||
|
||||
verify(passwordEncoder, times(1)).matches("fake-password", "fake-password");
|
||||
verify(passwordEncoder, times(1)).matches("fake@password", "fake@password");
|
||||
verify(passwordEncoder, never()).encode(any());
|
||||
verify(client, never()).update(any());
|
||||
verify(client).get(User.class, "fake-user");
|
||||
|
@ -188,7 +189,7 @@ class UserServiceImplTest {
|
|||
.thenReturn(Mono.error(
|
||||
new ExtensionNotFoundException(fromExtension(User.class), "fake-user")));
|
||||
|
||||
StepVerifier.create(userService.updateWithRawPassword("fake-user", "new-password"))
|
||||
StepVerifier.create(userService.updateWithRawPassword("fake-user", "new@password"))
|
||||
.verifyError(UserNotFoundException.class);
|
||||
|
||||
verify(passwordEncoder, never()).matches(anyString(), anyString());
|
||||
|
@ -197,6 +198,16 @@ class UserServiceImplTest {
|
|||
verify(client).get(User.class, "fake-user");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowWhenPwdContainsInvalidChars() {
|
||||
StepVerifier.create(userService.updateWithRawPassword("fake-user", "new-password"))
|
||||
.expectError(UnsatisfiedAttributeValueException.class)
|
||||
.verify();
|
||||
|
||||
verify(passwordEncoder, never()).encode(anyString());
|
||||
verify(client, never()).update(any());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
User createUser(String password) {
|
||||
|
|
|
@ -2,7 +2,6 @@ package run.halo.app.infra;
|
|||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.HashMap;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -19,74 +18,50 @@ class ValidationUtilsTest {
|
|||
class NameValidationTest {
|
||||
@Test
|
||||
void nullName() {
|
||||
assertThat(ValidationUtils.validateName(null)).isFalse();
|
||||
assertThat(validateName(null)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyUsername() {
|
||||
assertThat(ValidationUtils.validateName("")).isFalse();
|
||||
assertThat(validateName("")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void startWithIllegalCharacter() {
|
||||
assertThat(ValidationUtils.validateName("-abc")).isFalse();
|
||||
assertThat(validateName("-abc")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void endWithIllegalCharacter() {
|
||||
assertThat(ValidationUtils.validateName("abc-")).isFalse();
|
||||
assertThat(ValidationUtils.validateName("abcD")).isFalse();
|
||||
assertThat(validateName("abc-")).isFalse();
|
||||
assertThat(validateName("abcD")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void middleWithIllegalCharacter() {
|
||||
assertThat(ValidationUtils.validateName("ab?c")).isFalse();
|
||||
assertThat(validateName("ab?c")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void moreThan63Characters() {
|
||||
assertThat(ValidationUtils.validateName(StringUtils.repeat('a', 64))).isFalse();
|
||||
assertThat(validateName(StringUtils.repeat('a', 64))).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void correctUsername() {
|
||||
assertThat(ValidationUtils.validateName("abc")).isTrue();
|
||||
assertThat(ValidationUtils.validateName("ab-c")).isTrue();
|
||||
assertThat(ValidationUtils.validateName("1st")).isTrue();
|
||||
assertThat(ValidationUtils.validateName("ast1")).isTrue();
|
||||
assertThat(ValidationUtils.validateName("ast-1")).isTrue();
|
||||
assertThat(validateName("abc")).isTrue();
|
||||
assertThat(validateName("ab-c")).isTrue();
|
||||
assertThat(validateName("1st")).isTrue();
|
||||
assertThat(validateName("ast1")).isTrue();
|
||||
assertThat(validateName("ast-1")).isTrue();
|
||||
}
|
||||
|
||||
static boolean validateName(String name) {
|
||||
if (StringUtils.isBlank(name)) {
|
||||
return false;
|
||||
}
|
||||
boolean matches = ValidationUtils.NAME_PATTERN.matcher(name).matches();
|
||||
return matches && name.length() <= 63;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateEmailTest() {
|
||||
var cases = new HashMap<String, Boolean>();
|
||||
// Valid cases
|
||||
cases.put("simple@example.com", true);
|
||||
cases.put("very.common@example.com", true);
|
||||
cases.put("disposable.style.email.with+symbol@example.com", true);
|
||||
cases.put("other.email-with-hyphen@example.com", true);
|
||||
cases.put("fully-qualified-domain@example.com", true);
|
||||
cases.put("user.name+tag+sorting@example.com", true);
|
||||
cases.put("x@example.com", true);
|
||||
cases.put("example-indeed@strange-example.com", true);
|
||||
cases.put("example@s.example", true);
|
||||
cases.put("john.doe@example.com", true);
|
||||
cases.put("a.little.lengthy.but.fine@dept.example.com", true);
|
||||
cases.put("123ada@halo.co", true);
|
||||
cases.put("23ad@halo.top", true);
|
||||
|
||||
// Invalid cases
|
||||
cases.put("Abc.example.com", false);
|
||||
cases.put("admin@mailserver1", false);
|
||||
cases.put("\" \"@example.org", false);
|
||||
cases.put("A@b@c@example.com", false);
|
||||
cases.put("a\"b(c)d,e:f;g<h>i[j\\k]l@example.com", false);
|
||||
cases.put("just\"not\"right@example.com", false);
|
||||
cases.put("this is\"not\\allowed@example.com", false);
|
||||
cases.put("this\\ still\\\"not\\\\allowed@example.com", false);
|
||||
cases.put("123456789012345678901234567890123456789012345", false);
|
||||
cases.forEach((email, expected) -> assertThat(ValidationUtils.isValidEmail(email))
|
||||
.isEqualTo(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { Toast, VButton, VModal, VSpace } from "@halo-dev/components";
|
|||
import { setFocus } from "@/formkit/utils/focus";
|
||||
import { useQueryClient } from "@tanstack/vue-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { PASSWORD_REGEX } from "@/constants/regex";
|
||||
|
||||
const { t } = useI18n();
|
||||
const queryClient = useQueryClient();
|
||||
|
@ -112,9 +113,13 @@ const handleCreateUser = async () => {
|
|||
:label="$t('core.user.change_password_modal.fields.new_password.label')"
|
||||
type="password"
|
||||
name="password"
|
||||
validation="required:trim|length:5,100|matches:/^\S.*\S$/"
|
||||
:validation="[
|
||||
['required'],
|
||||
['length', 5, 257],
|
||||
['matches', PASSWORD_REGEX],
|
||||
]"
|
||||
:validation-messages="{
|
||||
matches: $t('core.formkit.validation.trim'),
|
||||
matches: $t('core.formkit.validation.password'),
|
||||
}"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts" setup>
|
||||
import SubmitButton from "@/components/button/SubmitButton.vue";
|
||||
import { PASSWORD_REGEX } from "@/constants/regex";
|
||||
import { setFocus } from "@/formkit/utils/focus";
|
||||
import type { User } from "@halo-dev/api-client";
|
||||
import { consoleApiClient } from "@halo-dev/api-client";
|
||||
|
@ -81,9 +82,13 @@ const handleChangePassword = async () => {
|
|||
:label="$t('core.user.change_password_modal.fields.new_password.label')"
|
||||
name="password"
|
||||
type="password"
|
||||
validation="required:trim|length:5,100|matches:/^\S.*\S$/"
|
||||
:validation="[
|
||||
['required'],
|
||||
['length', 5, 257],
|
||||
['matches', PASSWORD_REGEX],
|
||||
]"
|
||||
:validation-messages="{
|
||||
matches: $t('core.formkit.validation.trim'),
|
||||
matches: $t('core.formkit.validation.password'),
|
||||
}"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
|
@ -92,10 +97,7 @@ const handleChangePassword = async () => {
|
|||
"
|
||||
name="password_confirm"
|
||||
type="password"
|
||||
validation="confirm|required:trim|length:5,100|matches:/^\S.*\S$/"
|
||||
:validation-messages="{
|
||||
matches: $t('core.formkit.validation.trim'),
|
||||
}"
|
||||
validation="confirm|required"
|
||||
></FormKit>
|
||||
</FormKit>
|
||||
<template #footer>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export const PASSWORD_REGEX = /^[A-Za-z0-9!@#$%^&*]+$/;
|
|
@ -1664,6 +1664,7 @@ core:
|
|||
creation_label: Create {text} tag
|
||||
validation:
|
||||
trim: Please remove the leading and trailing spaces
|
||||
password: "The password can only use uppercase and lowercase letters (A-Z, a-z), numbers (0-9), and the following special characters: !{'@'}#$%^&*"
|
||||
verification_form:
|
||||
no_action_defined: "{label} interface not defined"
|
||||
verify_success: "{label} successful"
|
||||
|
|
|
@ -1557,6 +1557,7 @@ core:
|
|||
creation_label: 创建 {text} 标签
|
||||
validation:
|
||||
trim: 不能以空格开头或结尾
|
||||
password: "密码只能使用大小写字母 (A-Z, a-z)、数字 (0-9),以及以下特殊字符: !{'@'}#$%^&*"
|
||||
verification_form:
|
||||
no_action_defined: 未定义{label}接口
|
||||
verify_success: "{label}成功"
|
||||
|
|
|
@ -1535,6 +1535,7 @@ core:
|
|||
creation_label: 創建 {text} 標籤
|
||||
validation:
|
||||
trim: 不能以空格開頭或結尾
|
||||
password: "密碼只能使用大小寫字母 (A-Z, a-z)、數字 (0-9),以及以下特殊字符: !{'@'}#$%^&*"
|
||||
verification_form:
|
||||
no_action_defined: 未定義{label}介面
|
||||
verify_success: "{label}成功"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts" setup>
|
||||
import SubmitButton from "@/components/button/SubmitButton.vue";
|
||||
import { PASSWORD_REGEX } from "@/constants/regex";
|
||||
import { setFocus } from "@/formkit/utils/focus";
|
||||
import { consoleApiClient } from "@halo-dev/api-client";
|
||||
import { VButton, VModal, VSpace } from "@halo-dev/components";
|
||||
|
@ -81,9 +82,13 @@ const handleChangePassword = async () => {
|
|||
"
|
||||
name="password"
|
||||
type="password"
|
||||
validation="required:trim|length:5,100|matches:/^\S.*\S$/"
|
||||
:validation="[
|
||||
['required'],
|
||||
['length', 5, 257],
|
||||
['matches', PASSWORD_REGEX],
|
||||
]"
|
||||
:validation-messages="{
|
||||
matches: $t('core.formkit.validation.trim'),
|
||||
matches: $t('core.formkit.validation.password'),
|
||||
}"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
|
@ -94,10 +99,7 @@ const handleChangePassword = async () => {
|
|||
"
|
||||
name="password_confirm"
|
||||
type="password"
|
||||
validation="confirm|required:trim|length:5,100|matches:/^\S.*\S$/"
|
||||
:validation-messages="{
|
||||
matches: $t('core.formkit.validation.trim'),
|
||||
}"
|
||||
validation="confirm|required"
|
||||
></FormKit>
|
||||
</FormKit>
|
||||
<template #footer>
|
||||
|
|
Loading…
Reference in New Issue