From aab8806f0d64b4c4cd009ff0a7d9687c9207ab2a Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Fri, 11 Oct 2024 15:11:05 +0800 Subject: [PATCH] refactor: support locale-based validation messages based on users language (#6819) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### 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 ``` --- .../core/endpoint/console/UserEndpoint.java | 6 ++--- .../run/halo/app/infra/ValidationUtils.java | 25 +++++++++++++++++++ .../twofactor/TwoFactorAuthEndpoint.java | 22 ++++++++-------- .../preauth/PreAuthSignUpEndpoint.java | 7 +++--- .../security/preauth/SystemSetupEndpoint.java | 9 ++----- 5 files changed, 45 insertions(+), 24 deletions(-) diff --git a/application/src/main/java/run/halo/app/core/endpoint/console/UserEndpoint.java b/application/src/main/java/run/halo/app/core/endpoint/console/UserEndpoint.java index a998ad2d1..662e6f20e 100644 --- a/application/src/main/java/run/halo/app/core/endpoint/console/UserEndpoint.java +++ b/application/src/main/java/run/halo/app/core/endpoint/console/UserEndpoint.java @@ -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"); diff --git a/application/src/main/java/run/halo/app/infra/ValidationUtils.java b/application/src/main/java/run/halo/app/infra/ValidationUtils.java index 01f20e567..9252e2298 100644 --- a/application/src/main/java/run/halo/app/infra/ValidationUtils.java +++ b/application/src/main/java/run/halo/app/infra/ValidationUtils.java @@ -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); + } } diff --git a/application/src/main/java/run/halo/app/security/authentication/twofactor/TwoFactorAuthEndpoint.java b/application/src/main/java/run/halo/app/security/authentication/twofactor/TwoFactorAuthEndpoint.java index f874f5eb7..31e919878 100644 --- a/application/src/main/java/run/halo/app/security/authentication/twofactor/TwoFactorAuthEndpoint.java +++ b/application/src/main/java/run/halo/app/security/authentication/twofactor/TwoFactorAuthEndpoint.java @@ -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 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 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 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); } } 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 4566b03a8..a43072397 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 @@ -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)); } diff --git a/application/src/main/java/run/halo/app/security/preauth/SystemSetupEndpoint.java b/application/src/main/java/run/halo/app/security/preauth/SystemSetupEndpoint.java index afe07f8ff..a3bd6572b 100644 --- a/application/src/main/java/run/halo/app/security/preauth/SystemSetupEndpoint.java +++ b/application/src/main/java/run/halo/app/security/preauth/SystemSetupEndpoint.java @@ -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 loadPresetExtensions(String username) {