Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@
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;
Expand All @@ -86,6 +85,7 @@
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;
Expand Down Expand Up @@ -298,8 +298,8 @@ private Mono<ServerResponse> sendEmailVerificationCode(ServerRequest request) {
() -> 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");
Expand Down
25 changes: 25 additions & 0 deletions application/src/main/java/run/halo/app/infra/ValidationUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

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 {
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
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;
Expand All @@ -29,6 +28,7 @@
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;
Expand Down Expand Up @@ -108,8 +108,9 @@ public RouterFunction<ServerResponse> endpoint() {
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()
Expand Down Expand Up @@ -148,7 +149,8 @@ private Mono<ServerResponse> enableTwoFactor(ServerRequest request) {
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();
Expand Down Expand Up @@ -199,7 +201,7 @@ public static class TotpAuthLinkResponse {
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
Expand Down Expand Up @@ -235,11 +237,11 @@ private Mono<ServerResponse> configureTotp(ServerRequest request) {
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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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;
Expand Down Expand Up @@ -81,10 +82,9 @@ RouterFunction<ServerResponse> preAuthSignUpEndpoints() {
.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);
}
Expand Down Expand Up @@ -120,8 +120,7 @@ RouterFunction<ServerResponse> preAuthSignUpEndpoints() {
.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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,7 @@ private Mono<ServerResponse> setup(ServerRequest request) {
.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);
}
Expand Down Expand Up @@ -208,7 +207,7 @@ private Mono<ServerResponse> setupPage(ServerRequest request) {
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());
});
}
Expand Down Expand Up @@ -243,10 +242,6 @@ public String getEmail() {
public String getSiteTitle() {
return formData.getFirst("siteTitle");
}

public BindingResult toBindingResult() {
return new BeanPropertyBindingResult(this, "form");
}
}

Flux<Unstructured> loadPresetExtensions(String username) {
Expand Down
Loading