refactor: support locale-based validation messages based on users language (#6819)

#### What type of PR is this?
/kind improvement
/area core
/milestone 2.20.x

#### What this PR does / why we need it:
优化校验提示信息根据用户选择的语言代替 `Locale#getDefault()#getLanguage()`

#### Does this PR introduce a user-facing change?
```release-note
None
```
pull/6822/head
guqing 2024-10-11 15:11:05 +08:00 committed by GitHub
parent 99db7a6101
commit aab8806f0d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 45 additions and 24 deletions

View File

@ -59,7 +59,6 @@ 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;
@ -86,6 +85,7 @@ 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;
@ -298,8 +298,8 @@ public class UserEndpoint implements CustomEndpoint {
() -> new ServerWebInputException("Request body is required."))
)
.doOnNext(emailReq -> {
var bindingResult = new BeanPropertyBindingResult(emailReq, "form");
validator.validate(emailReq, bindingResult);
var bindingResult =
ValidationUtils.validate(emailReq, validator, request.exchange());
if (bindingResult.hasErrors()) {
// only email field is validated
throw new ServerWebInputException("validation.error.email.pattern");

View File

@ -2,6 +2,11 @@ package run.halo.app.infra;
import java.util.regex.Pattern;
import lombok.experimental.UtilityClass;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Validator;
import org.springframework.web.server.ServerWebExchange;
@UtilityClass
public class ValidationUtils {
@ -15,4 +20,24 @@ public class ValidationUtils {
public static final String PASSWORD_REGEX = "^[A-Za-z0-9!@#$%^&*]+$";
public static final Pattern PASSWORD_PATTERN = Pattern.compile(PASSWORD_REGEX);
/**
* Validate the target object with given locale context.
*/
public static BindingResult validate(Object target, String objectName,
Validator validator, ServerWebExchange exchange) {
BindingResult bindingResult = new BeanPropertyBindingResult(target, objectName);
try {
LocaleContextHolder.setLocaleContext(exchange.getLocaleContext());
validator.validate(target, bindingResult);
return bindingResult;
} finally {
LocaleContextHolder.resetLocaleContext();
}
}
public static BindingResult validate(Object target, Validator validator,
ServerWebExchange exchange) {
return validate(target, "form", validator, exchange);
}
}

View File

@ -15,7 +15,6 @@ import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
@ -29,6 +28,7 @@ import run.halo.app.core.user.service.UserService;
import run.halo.app.extension.GroupVersion;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.infra.ExternalUrlSupplier;
import run.halo.app.infra.ValidationUtils;
import run.halo.app.infra.exception.AccessDeniedException;
import run.halo.app.infra.exception.RequestBodyValidationException;
import run.halo.app.security.authentication.twofactor.totp.TotpAuthService;
@ -108,8 +108,9 @@ public class TwoFactorAuthEndpoint implements CustomEndpoint {
private Mono<ServerResponse> deleteTotp(ServerRequest request) {
var totpDeleteRequestMono = request.bodyToMono(PasswordRequest.class)
.switchIfEmpty(Mono.error(() -> new ServerWebInputException("Request body required")))
.doOnNext(
passwordRequest -> this.validateRequest(passwordRequest, "passwordRequest"));
.doOnNext(passwordRequest -> this.validateRequest(passwordRequest, "passwordRequest",
request)
);
var twoFactorAuthSettings =
totpDeleteRequestMono.flatMap(passwordRequest -> getCurrentUser()
@ -148,7 +149,8 @@ public class TwoFactorAuthEndpoint implements CustomEndpoint {
private Mono<ServerResponse> toggleTwoFactor(ServerRequest request, boolean enabled) {
return request.bodyToMono(PasswordRequest.class)
.switchIfEmpty(Mono.error(() -> new ServerWebInputException("Request body required")))
.doOnNext(passwordRequest -> this.validateRequest(passwordRequest, "passwordRequest"))
.doOnNext(passwordRequest -> this.validateRequest(passwordRequest,
"passwordRequest", request))
.flatMap(passwordRequest -> getCurrentUser()
.filter(user -> {
var encodedPassword = user.getSpec().getPassword();
@ -199,7 +201,7 @@ public class TwoFactorAuthEndpoint implements CustomEndpoint {
private Mono<ServerResponse> configureTotp(ServerRequest request) {
var totpRequestMono = request.bodyToMono(TotpRequest.class)
.switchIfEmpty(Mono.error(() -> new ServerWebInputException("Request body required.")))
.doOnNext(totpRequest -> this.validateRequest(totpRequest, "totp"));
.doOnNext(totpRequest -> this.validateRequest(totpRequest, "totp", request));
var configuredUser = totpRequestMono.flatMap(totpRequest -> {
// validate password
@ -235,11 +237,11 @@ public class TwoFactorAuthEndpoint implements CustomEndpoint {
return ServerResponse.ok().body(twoFactorAuthSettings, TwoFactorAuthSettings.class);
}
private void validateRequest(Object target, String name) {
var errors = new BeanPropertyBindingResult(target, name);
validator.validate(target, errors);
if (errors.hasErrors()) {
throw new RequestBodyValidationException(errors);
private void validateRequest(Object target, String name, ServerRequest request) {
var bindingResult =
ValidationUtils.validate(target, name, validator, request.exchange());
if (bindingResult.hasErrors()) {
throw new RequestBodyValidationException(bindingResult);
}
}

View File

@ -4,6 +4,7 @@ import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.contentType;
import static org.springframework.web.reactive.function.server.RequestPredicates.path;
import static run.halo.app.infra.ValidationUtils.validate;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
import io.github.resilience4j.ratelimiter.RequestNotPermitted;
@ -81,10 +82,9 @@ class PreAuthSignUpEndpoint {
.map(SignUpData::of)
.flatMap(signUpData -> {
// sign up
var bindingResult = new BeanPropertyBindingResult(signUpData, "form");
var bindingResult = validate(signUpData, validator, request.exchange());
var model = bindingResult.getModel();
model.put("globalInfo", globalInfoService.getGlobalInfo());
validator.validate(signUpData, bindingResult);
if (bindingResult.hasErrors()) {
return ServerResponse.ok().render("signup", model);
}
@ -120,8 +120,7 @@ class PreAuthSignUpEndpoint {
.POST("/send-email-code", contentType(APPLICATION_JSON),
request -> request.bodyToMono(SendEmailCodeBody.class)
.flatMap(body -> {
var bindingResult = new BeanPropertyBindingResult(body, "body");
validator.validate(body, bindingResult);
var bindingResult = validate(body, "body", validator, request.exchange());
if (bindingResult.hasErrors()) {
return Mono.error(new RequestBodyValidationException(bindingResult));
}

View File

@ -118,8 +118,7 @@ public class SystemSetupEndpoint {
.map(initialized -> !initialized)
)
.flatMap(body -> {
var bindingResult = body.toBindingResult();
validator.validate(body, bindingResult);
var bindingResult = ValidationUtils.validate(body, validator, request.exchange());
if (bindingResult.hasErrors()) {
return handleValidationErrors(bindingResult, request);
}
@ -208,7 +207,7 @@ public class SystemSetupEndpoint {
return redirectToConsole();
}
var body = new SetupRequest(new LinkedMultiValueMap<>());
var bindingResult = body.toBindingResult();
var bindingResult = new BeanPropertyBindingResult(body, "form");
return ServerResponse.ok().render(SETUP_TEMPLATE, bindingResult.getModel());
});
}
@ -243,10 +242,6 @@ public class SystemSetupEndpoint {
public String getSiteTitle() {
return formData.getFirst("siteTitle");
}
public BindingResult toBindingResult() {
return new BeanPropertyBindingResult(this, "form");
}
}
Flux<Unstructured> loadPresetExtensions(String username) {