Merge pull request #6804 from guqing/refactor/user-validation

refactor: unified validation for username and password format
pull/6806/head
John Niang 2024-10-10 10:45:38 +08:00 committed by GitHub
commit 158c3e8a9e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 134 additions and 137 deletions

View File

@ -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()

View File

@ -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

View File

@ -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())) {

View File

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

View File

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

View File

@ -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: !@#$%^&*

View File

@ -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),以及以下特殊字符: !@#$%^&*

View File

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

View File

@ -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) {

View File

@ -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();
}
@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));
static boolean validateName(String name) {
if (StringUtils.isBlank(name)) {
return false;
}
boolean matches = ValidationUtils.NAME_PATTERN.matcher(name).matches();
return matches && name.length() <= 63;
}
}
}

View File

@ -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

View File

@ -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>

View File

@ -0,0 +1 @@
export const PASSWORD_REGEX = /^[A-Za-z0-9!@#$%^&*]+$/;

View File

@ -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"

View File

@ -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}成功"

View File

@ -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}成功"

View File

@ -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>