mirror of https://github.com/halo-dev/halo
Add globalInfo into templates model (#6823)
#### What type of PR is this? /kind improvement /area core /milestone 2.20.x #### What this PR does / why we need it: This PR adds globalInfo into template models and refactors password reset to adapt data binding. Fixes https://github.com/halo-dev/halo/issues/6821 #### Does this PR introduce a user-facing change? ```release-note None ```pull/6829/head v2.20.0-rc.1
parent
d63eaed10f
commit
98a131309c
|
@ -2,28 +2,35 @@ package run.halo.app.security.preauth;
|
||||||
|
|
||||||
import static org.springframework.web.reactive.function.server.RequestPredicates.contentType;
|
import static org.springframework.web.reactive.function.server.RequestPredicates.contentType;
|
||||||
import static org.springframework.web.reactive.function.server.RequestPredicates.path;
|
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.RateLimiterRegistry;
|
||||||
import io.github.resilience4j.ratelimiter.RequestNotPermitted;
|
import io.github.resilience4j.ratelimiter.RequestNotPermitted;
|
||||||
import io.github.resilience4j.reactor.ratelimiter.operator.RateLimiterOperator;
|
import io.github.resilience4j.reactor.ratelimiter.operator.RateLimiterOperator;
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.Locale;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import lombok.Data;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.springframework.context.MessageSource;
|
import org.springframework.context.MessageSource;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.validation.Validator;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.user.service.EmailPasswordRecoveryService;
|
import run.halo.app.core.user.service.EmailPasswordRecoveryService;
|
||||||
import run.halo.app.core.user.service.InvalidResetTokenException;
|
import run.halo.app.core.user.service.InvalidResetTokenException;
|
||||||
import run.halo.app.infra.exception.RateLimitExceededException;
|
import run.halo.app.infra.ValidationUtils;
|
||||||
|
import run.halo.app.infra.actuator.GlobalInfoService;
|
||||||
import run.halo.app.infra.utils.IpAddressUtils;
|
import run.halo.app.infra.utils.IpAddressUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -38,151 +45,133 @@ class PreAuthEmailPasswordResetEndpoint {
|
||||||
private static final String SEND_TEMPLATE = "password-reset/email/send";
|
private static final String SEND_TEMPLATE = "password-reset/email/send";
|
||||||
private static final String RESET_TEMPLATE = "password-reset/email/reset";
|
private static final String RESET_TEMPLATE = "password-reset/email/reset";
|
||||||
|
|
||||||
private final EmailPasswordRecoveryService emailPasswordRecoveryService;
|
|
||||||
|
|
||||||
private final MessageSource messageSource;
|
|
||||||
|
|
||||||
private final RateLimiterRegistry rateLimiterRegistry;
|
private final RateLimiterRegistry rateLimiterRegistry;
|
||||||
|
|
||||||
private final PasswordResetAvailabilityProviders passwordResetAvailabilityProviders;
|
|
||||||
|
|
||||||
public PreAuthEmailPasswordResetEndpoint(
|
public PreAuthEmailPasswordResetEndpoint(
|
||||||
EmailPasswordRecoveryService emailPasswordRecoveryService,
|
RateLimiterRegistry rateLimiterRegistry
|
||||||
MessageSource messageSource,
|
|
||||||
RateLimiterRegistry rateLimiterRegistry,
|
|
||||||
PasswordResetAvailabilityProviders passwordResetAvailabilityProviders
|
|
||||||
) {
|
) {
|
||||||
this.emailPasswordRecoveryService = emailPasswordRecoveryService;
|
|
||||||
this.messageSource = messageSource;
|
|
||||||
this.rateLimiterRegistry = rateLimiterRegistry;
|
this.rateLimiterRegistry = rateLimiterRegistry;
|
||||||
this.passwordResetAvailabilityProviders = passwordResetAvailabilityProviders;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
RouterFunction<ServerResponse> preAuthPasswordResetEndpoints() {
|
RouterFunction<ServerResponse> preAuthPasswordResetEndpoints(
|
||||||
|
GlobalInfoService globalInfoService,
|
||||||
|
PasswordResetAvailabilityProviders availabilityProviders,
|
||||||
|
MessageSource messageSource,
|
||||||
|
EmailPasswordRecoveryService emailService,
|
||||||
|
Validator validator
|
||||||
|
) {
|
||||||
return RouterFunctions.nest(path("/password-reset/email"), RouterFunctions.route()
|
return RouterFunctions.nest(path("/password-reset/email"), RouterFunctions.route()
|
||||||
.GET("", request -> {
|
.GET("", request -> request.bind(SendForm.class)
|
||||||
return ServerResponse.ok().render(SEND_TEMPLATE, Map.of(
|
.flatMap(sendForm -> ServerResponse.ok().render(SEND_TEMPLATE, Map.of(
|
||||||
"otherMethods",
|
"otherMethods", availabilityProviders.getOtherAvailableMethods("email"),
|
||||||
passwordResetAvailabilityProviders.getOtherAvailableMethods("email")
|
"globalInfo", globalInfoService.getGlobalInfo(),
|
||||||
));
|
"form", sendForm
|
||||||
})
|
)))
|
||||||
|
)
|
||||||
.GET("/{resetToken}",
|
.GET("/{resetToken}",
|
||||||
request -> {
|
request -> {
|
||||||
var token = request.pathVariable("resetToken");
|
var token = request.pathVariable("resetToken");
|
||||||
return emailPasswordRecoveryService.getValidResetToken(token)
|
return request.bind(ResetForm.class)
|
||||||
.flatMap(resetToken -> {
|
.flatMap(resetForm -> {
|
||||||
// TODO Check the 2FA of the user
|
var model = new HashMap<String, Object>();
|
||||||
return ServerResponse.ok().render(RESET_TEMPLATE, Map.of(
|
model.put("form", resetForm);
|
||||||
"username", resetToken.username()
|
model.put("globalInfo", globalInfoService.getGlobalInfo());
|
||||||
));
|
return emailService.getValidResetToken(token)
|
||||||
})
|
.flatMap(resetToken -> {
|
||||||
.onErrorResume(InvalidResetTokenException.class,
|
// TODO Check the 2FA of the user
|
||||||
e -> ServerResponse.status(HttpStatus.FOUND)
|
model.put("username", resetToken.username());
|
||||||
.location(
|
return ServerResponse.ok().render(RESET_TEMPLATE, model);
|
||||||
URI.create("/password-reset/email?error=invalid_reset_token")
|
})
|
||||||
)
|
|
||||||
.build()
|
|
||||||
.transformDeferred(rateLimiterForPasswordResetVerification(
|
.transformDeferred(rateLimiterForPasswordResetVerification(
|
||||||
request.exchange().getRequest()
|
request.exchange().getRequest()
|
||||||
))
|
))
|
||||||
.onErrorMap(
|
.onErrorResume(InvalidResetTokenException.class, e ->
|
||||||
RequestNotPermitted.class, RateLimitExceededException::new
|
ServerResponse.status(HttpStatus.FOUND)
|
||||||
|
.location(URI.create(
|
||||||
|
"/password-reset/email?error=invalid_reset_token")
|
||||||
|
)
|
||||||
|
.build()
|
||||||
)
|
)
|
||||||
);
|
.onErrorResume(RequestNotPermitted.class, e -> {
|
||||||
|
model.put("error", "rate_limit_exceeded");
|
||||||
|
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
|
||||||
|
.render(RESET_TEMPLATE, model);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.POST("/{resetToken}", request -> {
|
.POST("/{resetToken}", request -> {
|
||||||
var token = request.pathVariable("resetToken");
|
var token = request.pathVariable("resetToken");
|
||||||
return request.formData()
|
return request.bind(ResetForm.class)
|
||||||
.flatMap(formData -> {
|
.flatMap(resetForm -> emailService.getValidResetToken(token)
|
||||||
var locale = Optional.ofNullable(
|
.flatMap(resetToken -> {
|
||||||
request.exchange().getLocaleContext().getLocale()
|
var bindingResult = validate(resetForm, validator, request.exchange());
|
||||||
)
|
var model = bindingResult.getModel();
|
||||||
.orElseGet(Locale::getDefault);
|
model.put("globalInfo", globalInfoService.getGlobalInfo());
|
||||||
var password = formData.getFirst("password");
|
model.put("username", resetToken.username());
|
||||||
var confirmPassword = formData.getFirst("confirmPassword");
|
if (!Objects.equals(
|
||||||
if (StringUtils.isBlank(password)) {
|
resetForm.getPassword(), resetForm.getConfirmPassword()
|
||||||
var error = messageSource.getMessage(
|
)) {
|
||||||
"passwordReset.password.blank",
|
bindingResult.rejectValue(
|
||||||
null,
|
"confirmPassword",
|
||||||
"Password can't be blank",
|
"validation.error.password.confirmPassword.mismatch",
|
||||||
locale
|
"Password and confirm password mismatch"
|
||||||
);
|
|
||||||
return ServerResponse.ok().render(RESET_TEMPLATE, Map.of(
|
|
||||||
"error", error
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if (!Objects.equals(password, confirmPassword)) {
|
|
||||||
var error = messageSource.getMessage(
|
|
||||||
"passwordReset.confirmPassword.mismatch",
|
|
||||||
null,
|
|
||||||
"Password and confirm password mismatch",
|
|
||||||
locale
|
|
||||||
);
|
|
||||||
return ServerResponse.ok().render(RESET_TEMPLATE, Map.of(
|
|
||||||
"error", error
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return emailPasswordRecoveryService.changePassword(password, token)
|
|
||||||
.then(ServerResponse.status(HttpStatus.FOUND)
|
|
||||||
.location(URI.create("/login?password_reset"))
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.onErrorResume(InvalidResetTokenException.class, e -> {
|
|
||||||
var error = messageSource.getMessage(
|
|
||||||
"passwordReset.resetToken.invalid",
|
|
||||||
null,
|
|
||||||
"Invalid reset token",
|
|
||||||
locale
|
|
||||||
);
|
);
|
||||||
return ServerResponse.ok().render(RESET_TEMPLATE, Map.of(
|
|
||||||
"error", error
|
|
||||||
)).transformDeferred(rateLimiterForPasswordResetVerification(
|
|
||||||
request.exchange().getRequest()
|
|
||||||
)).onErrorMap(
|
|
||||||
RequestNotPermitted.class, RateLimitExceededException::new
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.POST("", contentType(MediaType.APPLICATION_FORM_URLENCODED),
|
|
||||||
request -> {
|
|
||||||
// get username and email
|
|
||||||
return request.formData()
|
|
||||||
.flatMap(formData -> {
|
|
||||||
var locale = Optional.ofNullable(
|
|
||||||
request.exchange().getLocaleContext().getLocale()
|
|
||||||
)
|
|
||||||
.orElseGet(Locale::getDefault);
|
|
||||||
var email = formData.getFirst("email");
|
|
||||||
if (StringUtils.isBlank(email)) {
|
|
||||||
var error = messageSource.getMessage(
|
|
||||||
"passwordReset.email.blank",
|
|
||||||
null,
|
|
||||||
"Email can't be blank",
|
|
||||||
locale
|
|
||||||
);
|
|
||||||
return ServerResponse.ok().render(SEND_TEMPLATE, Map.of(
|
|
||||||
"error", error
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
return emailPasswordRecoveryService.sendPasswordResetEmail(email)
|
if (bindingResult.hasErrors()) {
|
||||||
.then(ServerResponse.ok().render(SEND_TEMPLATE, Map.of(
|
return ServerResponse.badRequest().render(RESET_TEMPLATE, model);
|
||||||
"sent", true
|
}
|
||||||
)))
|
return emailService.changePassword(resetForm.getPassword(), token)
|
||||||
.transformDeferred(rateLimiterForSendPasswordResetEmail(
|
.then(ServerResponse.status(HttpStatus.FOUND)
|
||||||
|
.location(URI.create("/login?password_reset"))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.transformDeferred(rateLimiterForPasswordResetVerification(
|
||||||
request.exchange().getRequest()
|
request.exchange().getRequest()
|
||||||
))
|
))
|
||||||
.onErrorMap(
|
.onErrorResume(RequestNotPermitted.class, e -> {
|
||||||
RequestNotPermitted.class, RateLimitExceededException::new
|
model.put("error", "rate_limit_exceeded");
|
||||||
);
|
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
|
||||||
});
|
.render(RESET_TEMPLATE, model);
|
||||||
})
|
});
|
||||||
|
})
|
||||||
|
.onErrorResume(InvalidResetTokenException.class,
|
||||||
|
e -> ServerResponse.status(HttpStatus.FOUND)
|
||||||
|
.location(URI.create(
|
||||||
|
"/password-reset/email?error=invalid_reset_token"
|
||||||
|
))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.POST("", contentType(MediaType.APPLICATION_FORM_URLENCODED),
|
||||||
|
request -> request.bind(SendForm.class)
|
||||||
|
.flatMap(sendForm -> {
|
||||||
|
// validate the send form
|
||||||
|
var bindingResult = validate(sendForm, validator, request.exchange());
|
||||||
|
var model = bindingResult.getModel();
|
||||||
|
model.put("globalInfo", globalInfoService.getGlobalInfo());
|
||||||
|
if (bindingResult.hasErrors()) {
|
||||||
|
return ServerResponse.badRequest().render(SEND_TEMPLATE, model);
|
||||||
|
}
|
||||||
|
return emailService.sendPasswordResetEmail(sendForm.getEmail())
|
||||||
|
.then(Mono.defer(() -> {
|
||||||
|
model.put("sent", true);
|
||||||
|
return ServerResponse.ok().render(SEND_TEMPLATE, model);
|
||||||
|
}))
|
||||||
|
.transformDeferred(rateLimiterForSendPasswordResetEmail(
|
||||||
|
request.exchange().getRequest()
|
||||||
|
))
|
||||||
|
.onErrorResume(RequestNotPermitted.class, e -> {
|
||||||
|
model.put("error", "rate_limit_exceeded");
|
||||||
|
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
|
||||||
|
.render(SEND_TEMPLATE, model);
|
||||||
|
});
|
||||||
|
}))
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
<T> RateLimiterOperator<T> rateLimiterForSendPasswordResetEmail(ServerHttpRequest request) {
|
<T> RateLimiterOperator<T> rateLimiterForSendPasswordResetEmail(ServerHttpRequest request) {
|
||||||
var clientIp = IpAddressUtils.getClientIp(request);
|
var clientIp = IpAddressUtils.getClientIp(request);
|
||||||
var rateLimiterKey = "send-password-reset-email-from-" + clientIp;
|
var rateLimiterKey = "send-password-reset-email-from-" + clientIp;
|
||||||
|
@ -198,4 +187,29 @@ class PreAuthEmailPasswordResetEndpoint {
|
||||||
rateLimiterRegistry.rateLimiter(rateLimiterKey, "password-reset-verification");
|
rateLimiterRegistry.rateLimiter(rateLimiterKey, "password-reset-verification");
|
||||||
return RateLimiterOperator.of(rateLimiter);
|
return RateLimiterOperator.of(rateLimiter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
static class ResetForm {
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Pattern(
|
||||||
|
regexp = ValidationUtils.PASSWORD_REGEX,
|
||||||
|
message = "{validation.error.password.pattern}"
|
||||||
|
)
|
||||||
|
@Size(min = 5, max = 257)
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String confirmPassword;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
static class SendForm {
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Email
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
package run.halo.app.security.preauth;
|
package run.halo.app.security.preauth;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import run.halo.app.infra.actuator.GlobalInfoService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pre-auth two-factor endpoints.
|
* Pre-auth two-factor endpoints.
|
||||||
|
@ -16,10 +18,12 @@ import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
class PreAuthTwoFactorEndpoint {
|
class PreAuthTwoFactorEndpoint {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
RouterFunction<ServerResponse> preAuthTwoFactorEndpoints() {
|
RouterFunction<ServerResponse> preAuthTwoFactorEndpoints(GlobalInfoService globalInfoService) {
|
||||||
return RouterFunctions.route()
|
return RouterFunctions.route()
|
||||||
.GET("/challenges/two-factor/totp",
|
.GET("/challenges/two-factor/totp",
|
||||||
request -> ServerResponse.ok().render("challenges/two-factor/totp")
|
request -> ServerResponse.ok().render("challenges/two-factor/totp", Map.of(
|
||||||
|
"globalInfo", globalInfoService.getGlobalInfo()
|
||||||
|
))
|
||||||
)
|
)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,21 +3,24 @@
|
||||||
class="halo-form"
|
class="halo-form"
|
||||||
th:action="@{/password-reset/email/{resetToken}(resetToken=${resetToken})}"
|
th:action="@{/password-reset/email/{resetToken}(resetToken=${resetToken})}"
|
||||||
method="post"
|
method="post"
|
||||||
|
th:object="${form}"
|
||||||
>
|
>
|
||||||
<div class="alert alert-error" role="alert" th:if="${error}">
|
<div class="alert alert-error" role="alert" th:if="${error}">
|
||||||
<strong th:text="${error}"></strong>
|
<strong th:if="${error == 'rate_limit_exceeded'}" th:text="#{error.rate_limit_exceeded}"></strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<label for="password" th:text="#{form.password.label}">Password</label>
|
<label for="password" th:text="#{form.password.label}">Password</label>
|
||||||
<th:block
|
<th:block
|
||||||
th:replace="~{gateway_fragments/input :: password(id = 'password', name = 'password', required = 'true', minlength = 5, maxlength = 257, enableToggle = true)}"
|
th:replace="~{gateway_fragments/input :: password(id = 'password', name = 'password', required = 'true', minlength = 5, maxlength = 257, enableToggle = true)}"
|
||||||
></th:block>
|
></th:block>
|
||||||
|
<p class="alert alert-error" th:if="${#fields.hasErrors('password')}" th:errors="*{password}"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<label for="confirmPassword" th:text="#{form.confirmPassword.label}">Confirm Password</label>
|
<label for="confirmPassword" th:text="#{form.confirmPassword.label}">Confirm Password</label>
|
||||||
<th:block
|
<th:block
|
||||||
th:replace="~{gateway_fragments/input :: password(id = 'confirmPassword', name = 'confirmPassword', required = 'true', minlength = 5, maxlength = 257, enableToggle = true)}"
|
th:replace="~{gateway_fragments/input :: password(id = 'confirmPassword', name = 'confirmPassword', required = 'true', minlength = 5, maxlength = 257, enableToggle = true)}"
|
||||||
></th:block>
|
></th:block>
|
||||||
|
<p class="alert alert-error" th:if="${#fields.hasErrors('confirmPassword')}" th:errors="*{confirmPassword}"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<div class="alert" th:text="#{form.password.tips}"></div>
|
<div class="alert" th:text="#{form.password.tips}"></div>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
form.password.label=密码
|
form.password.label=密码
|
||||||
form.confirmPassword.label=确认密码
|
form.confirmPassword.label=确认密码
|
||||||
form.password.tips=密码只能使用大小写字母 (A-Z, a-z)、数字 (0-9),以及以下特殊字符: !@#$%^&*。
|
form.password.tips=密码只能使用大小写字母 (A-Z, a-z)、数字 (0-9),以及以下特殊字符: !@#$%^&*。
|
||||||
form.submit=修改密码
|
form.submit=修改密码
|
||||||
|
error.rate_limit_exceeded=您的请求过于频繁,请稍后再试。
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
form.password.label=Password
|
form.password.label=Password
|
||||||
form.confirmPassword.label=Confirm Password
|
form.confirmPassword.label=Confirm Password
|
||||||
form.password.tips=The password can only use uppercase and lowercase letters (A-Z, a-z), numbers (0-9), and the following special characters: !@#$%^&*
|
form.password.tips=The password can only use uppercase and lowercase letters (A-Z, a-z), numbers (0-9), and the following special characters: !@#$%^&*
|
||||||
form.submit=Change password
|
form.submit=Change password
|
||||||
|
error.rate_limit_exceeded=Your request is too frequent, please try again later.
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
form.password.label=Contraseña
|
form.password.label=Contraseña
|
||||||
form.confirmPassword.label=Confirmar Contraseña
|
form.confirmPassword.label=Confirmar Contraseña
|
||||||
form.password.tips=La contraseña solo puede usar letras mayúsculas y minúsculas (A-Z, a-z), números (0-9) y los siguientes caracteres especiales: !@#$%^&*.
|
form.password.tips=La contraseña solo puede usar letras mayúsculas y minúsculas (A-Z, a-z), números (0-9) y los siguientes caracteres especiales: !@#$%^&*.
|
||||||
form.submit=Cambiar Contraseña
|
form.submit=Cambiar Contraseña
|
||||||
|
error.rate_limit_exceeded=Su solicitud es demasiado frecuente, por favor intente nuevamente más tarde.
|
|
@ -1,4 +1,5 @@
|
||||||
form.password.label=密碼
|
form.password.label=密碼
|
||||||
form.confirmPassword.label=確認密碼
|
form.confirmPassword.label=確認密碼
|
||||||
form.password.tips=密碼只能使用大小寫字母 (A-Z, a-z)、數字 (0-9),以及以下特殊字符: !@#$%^&*。
|
form.password.tips=密碼只能使用大小寫字母 (A-Z, a-z)、數字 (0-9),以及以下特殊字符: !@#$%^&*。
|
||||||
form.submit=修改密碼
|
form.submit=修改密碼
|
||||||
|
error.rate_limit_exceeded=您的請求過於頻繁,請稍後再試。
|
||||||
|
|
|
@ -7,18 +7,22 @@
|
||||||
<button type="submit" th:text="#{form.sent.submit}"></button>
|
<button type="submit" th:text="#{form.sent.submit}"></button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<form th:unless="${sent}" class="halo-form" th:action="@{/password-reset/email}" method="post">
|
<form th:unless="${sent}" class="halo-form" th:action="@{/password-reset/email}" method="post" th:object="${form}">
|
||||||
|
<div class="alert alert-error" th:if="${param.error.size() > 0}">
|
||||||
|
<strong th:if="${param.error[0] == 'invalid_reset_token'}" th:text="#{error.invalid_reset_token}"></strong>
|
||||||
|
</div>
|
||||||
<div class="alert alert-error" th:if="${error}">
|
<div class="alert alert-error" th:if="${error}">
|
||||||
<strong th:text="${error}"></strong>
|
<strong th:if="${error == 'rate_limit_exceeded'}" th:text="#{error.rate_limit_exceeded}"></strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<label for="email" th:text="#{form.email.label}"></label>
|
<label for="email" th:text="#{form.email.label}"></label>
|
||||||
<div class="form-input">
|
<div class="form-input">
|
||||||
<input type="email" id="email" name="email" autofocus required />
|
<input type="email" id="email" name="email" autofocus required th:field="*{email}"/>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="alert alert-error" role="alert" th:if="${#fields.hasErrors('email')}" th:errors="*{email}"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<button type="submit" th:text="#{form.submit}"></button>
|
<button type="submit" th:text="#{form.submit}"></button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
form.email.label=电子邮箱
|
form.email.label=电子邮箱
|
||||||
form.submit=提交
|
form.submit=提交
|
||||||
form.sent.submit=返回到登录页面
|
form.sent.submit=返回到登录页面
|
||||||
form.message.success=检查您的电子邮件中是否有重置密码的链接。如果几分钟内没有出现,请检查您的垃圾邮件文件夹。
|
form.message.success=检查您的电子邮件中是否有重置密码的链接。如果几分钟内没有出现,请检查您的垃圾邮件文件夹。
|
||||||
|
error.rate_limit_exceeded=您的请求速度太快。请稍后再试。
|
||||||
|
error.invalid_reset_token=重置密码令牌无效。请重试。
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
form.email.label=Email
|
form.email.label=Email
|
||||||
form.submit=Submit
|
form.submit=Submit
|
||||||
form.sent.submit=Return to login
|
form.sent.submit=Return to login
|
||||||
form.message.success=Check your email for a link to reset your password. If it doesn’t appear within a few minutes, check your spam folder.
|
form.message.success=Check your email for a link to reset your password. If it doesn’t appear within a few minutes, check your spam folder.
|
||||||
|
error.rate_limit_exceeded=You are making requests too quickly. Please try again later.
|
||||||
|
error.invalid_reset_token=The reset password token is invalid. Please try again.
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
form.email.label=Correo Electrónico
|
form.email.label=Correo Electrónico
|
||||||
form.submit=Enviar
|
form.submit=Enviar
|
||||||
form.sent.submit=Volver a la Página de Inicio de Sesión
|
form.sent.submit=Volver a la Página de Inicio de Sesión
|
||||||
form.message.success=Revisa tu correo electrónico para ver el enlace de restablecimiento de contraseña. Si no aparece en unos minutos, revisa tu carpeta de spam.
|
form.message.success=Revisa tu correo electrónico para ver el enlace de restablecimiento de contraseña. Si no aparece en unos minutos, revisa tu carpeta de spam.
|
||||||
|
error.rate_limit_exceeded=Se ha superado el límite de intentos de restablecimiento de contraseña. Por favor, inténtalo de nuevo más tarde.
|
||||||
|
error.invalid_reset_token=El enlace de restablecimiento de contraseña no es válido o ha expirado. Por favor, solicita un nuevo enlace.
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
form.email.label=電子郵件
|
form.email.label=電子郵件
|
||||||
form.submit=提交
|
form.submit=提交
|
||||||
form.sent.submit=返回到登入頁面
|
form.sent.submit=返回到登入頁面
|
||||||
form.message.success=檢查您的電子郵件中是否有重置密碼的連結。如果幾分鐘內沒有出現,請檢查您的垃圾郵件資料夾。
|
form.message.success=檢查您的電子郵件中是否有重置密碼的連結。如果幾分鐘內沒有出現,請檢查您的垃圾郵件資料夾。
|
||||||
|
error.rate_limit_exceeded=您的請求過於頻繁。請稍後再試。
|
||||||
|
error.invalid_reset_token=重置密碼連結無效。請重試。
|
||||||
|
|
Loading…
Reference in New Issue