mirror of https://github.com/halo-dev/halo
Refactor password reset for extensibility (#6803)
#### What type of PR is this? /kind improvement /area core /milestone 2.20.x #### What this PR does / why we need it: This PR refactors password reset for extensibility. If we want to add another password reset method, first thing we need to do is adding a new password reset method into `halo.security.password-reset-methods[]` and then defining PasswordResetAvailabilityProvider bean. #### Does this PR introduce a user-facing change? ```release-note None ```pull/6806/head
parent
0e4a19d182
commit
ec75564f37
|
@ -115,7 +115,7 @@ public class EmailPasswordRecoveryServiceImpl implements EmailPasswordRecoverySe
|
||||||
var tokenHash = hashToken(token);
|
var tokenHash = hashToken(token);
|
||||||
var expiresAt = clock.instant().plus(RESET_TOKEN_LIFE_TIME);
|
var expiresAt = clock.instant().plus(RESET_TOKEN_LIFE_TIME);
|
||||||
var uri = UriComponentsBuilder.fromUriString("/")
|
var uri = UriComponentsBuilder.fromUriString("/")
|
||||||
.pathSegment("password-reset", token)
|
.pathSegment("password-reset", "email", token)
|
||||||
.build(true)
|
.build(true)
|
||||||
.toUri();
|
.toUri();
|
||||||
var resetToken = new ResetToken(tokenHash, username, expiresAt);
|
var resetToken = new ResetToken(tokenHash, username, expiresAt);
|
||||||
|
|
|
@ -2,7 +2,10 @@ package run.halo.app.infra.properties;
|
||||||
|
|
||||||
import static org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN;
|
import static org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy;
|
import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy;
|
||||||
import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter.Mode;
|
import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter.Mode;
|
||||||
|
@ -20,6 +23,8 @@ public class SecurityProperties {
|
||||||
|
|
||||||
private final BasicAuthOptions basicAuth = new BasicAuthOptions();
|
private final BasicAuthOptions basicAuth = new BasicAuthOptions();
|
||||||
|
|
||||||
|
private final List<PasswordResetMethod> passwordResetMethods = new ArrayList<>();
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class BasicAuthOptions {
|
public static class BasicAuthOptions {
|
||||||
/**
|
/**
|
||||||
|
@ -57,4 +62,15 @@ public class SecurityProperties {
|
||||||
public static class RememberMeOptions {
|
public static class RememberMeOptions {
|
||||||
private Duration tokenValidity = Duration.ofDays(14);
|
private Duration tokenValidity = Duration.ofDays(14);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class PasswordResetMethod {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private URI href;
|
||||||
|
|
||||||
|
private URI icon;
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
package run.halo.app.security.preauth;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.infra.properties.HaloProperties;
|
||||||
|
import run.halo.app.infra.properties.SecurityProperties;
|
||||||
|
import run.halo.app.infra.properties.SecurityProperties.PasswordResetMethod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default password reset availability providers.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
* @since 2.20.0
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class DefaultPasswordResetAvailabilityProviders
|
||||||
|
implements PasswordResetAvailabilityProviders {
|
||||||
|
|
||||||
|
private final SecurityProperties securityProperties;
|
||||||
|
|
||||||
|
private final List<PasswordResetAvailabilityProvider> providers;
|
||||||
|
|
||||||
|
public DefaultPasswordResetAvailabilityProviders(HaloProperties haloProperties,
|
||||||
|
ObjectProvider<PasswordResetAvailabilityProvider> providers) {
|
||||||
|
this.securityProperties = haloProperties.getSecurity();
|
||||||
|
this.providers = providers.orderedStream().toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flux<PasswordResetMethod> getAvailableMethods() {
|
||||||
|
return Flux.fromIterable(securityProperties.getPasswordResetMethods())
|
||||||
|
.filterWhen(method -> providers.stream()
|
||||||
|
.filter(provider -> provider.support(method.getName()))
|
||||||
|
.findFirst()
|
||||||
|
.map(provider -> provider.isAvailable(method))
|
||||||
|
.orElseGet(() -> Mono.just(false))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package run.halo.app.security.preauth;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.infra.properties.SecurityProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Email password reset availability provider.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
* @since 2.20.0
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class EmailPasswordResetAvailabilityProvider implements PasswordResetAvailabilityProvider {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Boolean> isAvailable(SecurityProperties.PasswordResetMethod method) {
|
||||||
|
// TODO Check the email notifier is available in the future
|
||||||
|
return Mono.just(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean support(String name) {
|
||||||
|
return "email".equals(name);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package run.halo.app.security.preauth;
|
||||||
|
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.infra.properties.SecurityProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Password reset availability provider.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
* @since 2.20.0
|
||||||
|
*/
|
||||||
|
public interface PasswordResetAvailabilityProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the password reset method is available.
|
||||||
|
*
|
||||||
|
* @param method password reset method
|
||||||
|
* @return true if available, false otherwise
|
||||||
|
*/
|
||||||
|
Mono<Boolean> isAvailable(SecurityProperties.PasswordResetMethod method);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the provider supports the name.
|
||||||
|
*
|
||||||
|
* @param name password reset method name
|
||||||
|
* @return true if supports, false otherwise
|
||||||
|
*/
|
||||||
|
boolean support(String name);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package run.halo.app.security.preauth;
|
||||||
|
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import run.halo.app.infra.properties.SecurityProperties.PasswordResetMethod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Password reset availability providers.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
* @since 2.20.0
|
||||||
|
*/
|
||||||
|
public interface PasswordResetAvailabilityProviders {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available password reset methods.
|
||||||
|
*
|
||||||
|
* @return available password reset methods
|
||||||
|
*/
|
||||||
|
Flux<PasswordResetMethod> getAvailableMethods();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get other available password reset methods.
|
||||||
|
*
|
||||||
|
* @param methodName method name
|
||||||
|
* @return other available password reset methods
|
||||||
|
*/
|
||||||
|
default Flux<PasswordResetMethod> getOtherAvailableMethods(String methodName) {
|
||||||
|
return getAvailableMethods().filter(method -> !method.getName().equals(methodName));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -33,7 +33,10 @@ import run.halo.app.infra.utils.IpAddressUtils;
|
||||||
* @since 2.20.0
|
* @since 2.20.0
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
class PreAuthPasswordResetEndpoint {
|
class PreAuthEmailPasswordResetEndpoint {
|
||||||
|
|
||||||
|
private static final String SEND_TEMPLATE = "password-reset/email/send";
|
||||||
|
private static final String RESET_TEMPLATE = "password-reset/email/reset";
|
||||||
|
|
||||||
private final EmailPasswordRecoveryService emailPasswordRecoveryService;
|
private final EmailPasswordRecoveryService emailPasswordRecoveryService;
|
||||||
|
|
||||||
|
@ -41,32 +44,44 @@ class PreAuthPasswordResetEndpoint {
|
||||||
|
|
||||||
private final RateLimiterRegistry rateLimiterRegistry;
|
private final RateLimiterRegistry rateLimiterRegistry;
|
||||||
|
|
||||||
public PreAuthPasswordResetEndpoint(EmailPasswordRecoveryService emailPasswordRecoveryService,
|
private final PasswordResetAvailabilityProviders passwordResetAvailabilityProviders;
|
||||||
|
|
||||||
|
public PreAuthEmailPasswordResetEndpoint(
|
||||||
|
EmailPasswordRecoveryService emailPasswordRecoveryService,
|
||||||
MessageSource messageSource,
|
MessageSource messageSource,
|
||||||
RateLimiterRegistry rateLimiterRegistry
|
RateLimiterRegistry rateLimiterRegistry,
|
||||||
|
PasswordResetAvailabilityProviders passwordResetAvailabilityProviders
|
||||||
) {
|
) {
|
||||||
this.emailPasswordRecoveryService = emailPasswordRecoveryService;
|
this.emailPasswordRecoveryService = emailPasswordRecoveryService;
|
||||||
this.messageSource = messageSource;
|
this.messageSource = messageSource;
|
||||||
this.rateLimiterRegistry = rateLimiterRegistry;
|
this.rateLimiterRegistry = rateLimiterRegistry;
|
||||||
|
this.passwordResetAvailabilityProviders = passwordResetAvailabilityProviders;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
RouterFunction<ServerResponse> preAuthPasswordResetEndpoints() {
|
RouterFunction<ServerResponse> preAuthPasswordResetEndpoints() {
|
||||||
return RouterFunctions.nest(path("/password-reset"), RouterFunctions.route()
|
return RouterFunctions.nest(path("/password-reset/email"), RouterFunctions.route()
|
||||||
.GET("", request -> ServerResponse.ok().render("password-reset"))
|
.GET("", request -> {
|
||||||
|
return ServerResponse.ok().render(SEND_TEMPLATE, Map.of(
|
||||||
|
"otherMethods",
|
||||||
|
passwordResetAvailabilityProviders.getOtherAvailableMethods("email")
|
||||||
|
));
|
||||||
|
})
|
||||||
.GET("/{resetToken}",
|
.GET("/{resetToken}",
|
||||||
request -> {
|
request -> {
|
||||||
var token = request.pathVariable("resetToken");
|
var token = request.pathVariable("resetToken");
|
||||||
return emailPasswordRecoveryService.getValidResetToken(token)
|
return emailPasswordRecoveryService.getValidResetToken(token)
|
||||||
.flatMap(resetToken -> {
|
.flatMap(resetToken -> {
|
||||||
// TODO Check the 2FA of the user
|
// TODO Check the 2FA of the user
|
||||||
return ServerResponse.ok().render("password-reset-link", Map.of(
|
return ServerResponse.ok().render(RESET_TEMPLATE, Map.of(
|
||||||
"username", resetToken.username()
|
"username", resetToken.username()
|
||||||
));
|
));
|
||||||
})
|
})
|
||||||
.onErrorResume(InvalidResetTokenException.class,
|
.onErrorResume(InvalidResetTokenException.class,
|
||||||
e -> ServerResponse.status(HttpStatus.FOUND)
|
e -> ServerResponse.status(HttpStatus.FOUND)
|
||||||
.location(URI.create("/password-reset"))
|
.location(
|
||||||
|
URI.create("/password-reset/email?error=invalid_reset_token")
|
||||||
|
)
|
||||||
.build()
|
.build()
|
||||||
.transformDeferred(rateLimiterForPasswordResetVerification(
|
.transformDeferred(rateLimiterForPasswordResetVerification(
|
||||||
request.exchange().getRequest()
|
request.exchange().getRequest()
|
||||||
|
@ -94,7 +109,7 @@ class PreAuthPasswordResetEndpoint {
|
||||||
"Password can't be blank",
|
"Password can't be blank",
|
||||||
locale
|
locale
|
||||||
);
|
);
|
||||||
return ServerResponse.ok().render("password-reset-link", Map.of(
|
return ServerResponse.ok().render(RESET_TEMPLATE, Map.of(
|
||||||
"error", error
|
"error", error
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -105,13 +120,13 @@ class PreAuthPasswordResetEndpoint {
|
||||||
"Password and confirm password mismatch",
|
"Password and confirm password mismatch",
|
||||||
locale
|
locale
|
||||||
);
|
);
|
||||||
return ServerResponse.ok().render("password-reset-link", Map.of(
|
return ServerResponse.ok().render(RESET_TEMPLATE, Map.of(
|
||||||
"error", error
|
"error", error
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return emailPasswordRecoveryService.changePassword(password, token)
|
return emailPasswordRecoveryService.changePassword(password, token)
|
||||||
.then(ServerResponse.status(HttpStatus.FOUND)
|
.then(ServerResponse.status(HttpStatus.FOUND)
|
||||||
.location(URI.create("/login?passwordReset"))
|
.location(URI.create("/login?password_reset"))
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.onErrorResume(InvalidResetTokenException.class, e -> {
|
.onErrorResume(InvalidResetTokenException.class, e -> {
|
||||||
|
@ -121,7 +136,7 @@ class PreAuthPasswordResetEndpoint {
|
||||||
"Invalid reset token",
|
"Invalid reset token",
|
||||||
locale
|
locale
|
||||||
);
|
);
|
||||||
return ServerResponse.ok().render("password-reset-link", Map.of(
|
return ServerResponse.ok().render(RESET_TEMPLATE, Map.of(
|
||||||
"error", error
|
"error", error
|
||||||
)).transformDeferred(rateLimiterForPasswordResetVerification(
|
)).transformDeferred(rateLimiterForPasswordResetVerification(
|
||||||
request.exchange().getRequest()
|
request.exchange().getRequest()
|
||||||
|
@ -148,12 +163,12 @@ class PreAuthPasswordResetEndpoint {
|
||||||
"Email can't be blank",
|
"Email can't be blank",
|
||||||
locale
|
locale
|
||||||
);
|
);
|
||||||
return ServerResponse.ok().render("password-reset", Map.of(
|
return ServerResponse.ok().render(SEND_TEMPLATE, Map.of(
|
||||||
"error", error
|
"error", error
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return emailPasswordRecoveryService.sendPasswordResetEmail(email)
|
return emailPasswordRecoveryService.sendPasswordResetEmail(email)
|
||||||
.then(ServerResponse.ok().render("password-reset", Map.of(
|
.then(ServerResponse.ok().render(SEND_TEMPLATE, Map.of(
|
||||||
"sent", true
|
"sent", true
|
||||||
)))
|
)))
|
||||||
.transformDeferred(rateLimiterForSendPasswordResetEmail(
|
.transformDeferred(rateLimiterForSendPasswordResetEmail(
|
|
@ -42,6 +42,11 @@ halo:
|
||||||
- pathPattern: /upload/**
|
- pathPattern: /upload/**
|
||||||
locations:
|
locations:
|
||||||
- migrate-from-1.x
|
- migrate-from-1.x
|
||||||
|
security:
|
||||||
|
password-reset-methods:
|
||||||
|
- name: email
|
||||||
|
href: /password-reset/email
|
||||||
|
icon: /images/password-reset-methods/email.svg
|
||||||
|
|
||||||
springdoc:
|
springdoc:
|
||||||
api-docs:
|
api-docs:
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgba(75,85,99,1)"><path d="M3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3ZM20 7.23792L12.0718 14.338L4 7.21594V19H20V7.23792ZM4.51146 5L12.0619 11.662L19.501 5H4.51146Z"></path></svg>
|
After Width: | Height: | Size: 321 B |
|
@ -283,7 +283,7 @@
|
||||||
background: #f9fafb;
|
background: #f9fafb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-provider-items {
|
.pill-items {
|
||||||
all: unset;
|
all: unset;
|
||||||
gap: var(--spacing-sm);
|
gap: var(--spacing-sm);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -292,7 +292,7 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-provider-items li {
|
.pill-items li {
|
||||||
all: unset;
|
all: unset;
|
||||||
border-radius: var(--rounded-lg);
|
border-radius: var(--rounded-lg);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -302,7 +302,7 @@
|
||||||
transition-duration: 0.15s;
|
transition-duration: 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-provider-items li a {
|
.pill-items li a {
|
||||||
gap: var(--spacing-sm);
|
gap: var(--spacing-sm);
|
||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
padding: 0.7em 1em;
|
padding: 0.7em 1em;
|
||||||
|
@ -312,21 +312,21 @@
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-provider-items li img {
|
.pill-items li img {
|
||||||
width: 1.5em;
|
width: 1.5em;
|
||||||
height: 1.5em;
|
height: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-provider-items li:hover {
|
.pill-items li:hover {
|
||||||
border-color: var(--color-primary);
|
border-color: var(--color-primary);
|
||||||
background: #f3f4f6;
|
background: #f3f4f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-provider-items li:hover a {
|
.pill-items li:hover a {
|
||||||
color: #111827;
|
color: #111827;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-provider-items li:focus-within {
|
.pill-items li:focus-within {
|
||||||
border-color: var(--color-primary);
|
border-color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -109,7 +109,7 @@
|
||||||
<th:block th:text="#{socialLogin.label}"></th:block>
|
<th:block th:text="#{socialLogin.label}"></th:block>
|
||||||
<hr />
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
<ul class="auth-provider-items">
|
<ul class="pill-items">
|
||||||
<li th:each="provider : ${socialAuthProviders}">
|
<li th:each="provider : ${socialAuthProviders}">
|
||||||
<a th:href="${provider.spec.authenticationUrl}">
|
<a th:href="${provider.spec.authenticationUrl}">
|
||||||
<img th:src="${provider.spec.logo}" />
|
<img th:src="${provider.spec.logo}" />
|
||||||
|
@ -120,6 +120,26 @@
|
||||||
</th:block>
|
</th:block>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div th:remove="tag" th:fragment="passwordResetMethods">
|
||||||
|
<th:block th:unless="${#lists.isEmpty(otherMethods)}">
|
||||||
|
<div class="divider-wrapper">
|
||||||
|
<hr />
|
||||||
|
<th:block th:text="#{passwordResetMethods.label}"></th:block>
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
<ul class="pill-items">
|
||||||
|
<li th:each="method : ${otherMethods}">
|
||||||
|
<a th:href="${method.href}">
|
||||||
|
<img th:src="${method.icon}" />
|
||||||
|
<span
|
||||||
|
th:text="${#messages.msgOrNull('passwordResetMethods.' + method.name + '.displayName') ?: method.name}"
|
||||||
|
></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div th:remove="tag" th:fragment="signupNoticeContent">
|
<div th:remove="tag" th:fragment="signupNoticeContent">
|
||||||
<style>
|
<style>
|
||||||
.signup-notice-content {
|
.signup-notice-content {
|
||||||
|
|
|
@ -7,3 +7,6 @@ signupNotice.link=立即注册
|
||||||
loginNotice.description=已有账号,
|
loginNotice.description=已有账号,
|
||||||
loginNotice.link=立即登录
|
loginNotice.link=立即登录
|
||||||
returnToSite=返回网站
|
returnToSite=返回网站
|
||||||
|
|
||||||
|
passwordResetMethods.label=其他重置方式
|
||||||
|
passwordResetMethods.email.displayName=通过邮件重置
|
|
@ -7,3 +7,6 @@ signupNotice.link=Sign up
|
||||||
loginNotice.description=Already have an account,
|
loginNotice.description=Already have an account,
|
||||||
loginNotice.link=Login now
|
loginNotice.link=Login now
|
||||||
returnToSite=Return to site
|
returnToSite=Return to site
|
||||||
|
|
||||||
|
passwordResetMethods.label=Other Reset Methods
|
||||||
|
passwordResetMethods.email.displayName=Reset via Email
|
|
@ -7,3 +7,6 @@ signupNotice.link=Regístrate ahora
|
||||||
loginNotice.description=Ya tienes una cuenta,
|
loginNotice.description=Ya tienes una cuenta,
|
||||||
loginNotice.link=Inicia sesión ahora
|
loginNotice.link=Inicia sesión ahora
|
||||||
returnToSite=Volver al sitio
|
returnToSite=Volver al sitio
|
||||||
|
|
||||||
|
passwordResetMethods.label=Otros Métodos de Restablecimiento
|
||||||
|
passwordResetMethods.email.displayName=Restablecer por Correo Electrónico
|
|
@ -7,3 +7,6 @@ signupNotice.link=立即註冊
|
||||||
loginNotice.description=已有帳號,
|
loginNotice.description=已有帳號,
|
||||||
loginNotice.link=立即登入
|
loginNotice.link=立即登入
|
||||||
returnToSite=返回網站
|
returnToSite=返回網站
|
||||||
|
|
||||||
|
passwordResetMethods.label=其他重置方式
|
||||||
|
passwordResetMethods.email.displayName=通過郵件重置
|
|
@ -245,7 +245,7 @@
|
||||||
<form
|
<form
|
||||||
th:fragment="passwordResetLink"
|
th:fragment="passwordResetLink"
|
||||||
class="halo-form"
|
class="halo-form"
|
||||||
th:action="@{/password-reset/{resetToken}(resetToken=${resetToken})}"
|
th:action="@{/password-reset/email/{resetToken}(resetToken=${resetToken})}"
|
||||||
method="post"
|
method="post"
|
||||||
>
|
>
|
||||||
<div class="alert alert-error" role="alert" th:if="${error}">
|
<div class="alert alert-error" role="alert" th:if="${error}">
|
||||||
|
@ -280,7 +280,7 @@
|
||||||
<button type="submit" th:text="#{form.passwordReset.sent.submit}"></button>
|
<button type="submit" th:text="#{form.passwordReset.sent.submit}"></button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<form th:unless="${sent}" class="halo-form" th:action="@{/password-reset}" method="post">
|
<form th:unless="${sent}" class="halo-form" th:action="@{/password-reset/email}" method="post">
|
||||||
<div class="alert alert-error" th:if="${error}">
|
<div class="alert alert-error" th:if="${error}">
|
||||||
<strong th:text="${error}"></strong>
|
<strong th:text="${error}"></strong>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<th:block th:text="#{otherLogin.label}"></th:block>
|
<th:block th:text="#{otherLogin.label}"></th:block>
|
||||||
<hr />
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
<ul class="auth-provider-items">
|
<ul class="pill-items">
|
||||||
<li th:each="provider : ${formAuthProviders}">
|
<li th:each="provider : ${formAuthProviders}">
|
||||||
<a th:href="'/login?method=' + ${provider.metadata.name}">
|
<a th:href="'/login?method=' + ${provider.metadata.name}">
|
||||||
<img th:src="${provider.spec.logo}" />
|
<img th:src="${provider.spec.logo}" />
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
<a
|
<a
|
||||||
class="form-item-extra-link"
|
class="form-item-extra-link"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
th:href="@{/password-reset}"
|
th:href="@{/password-reset/email}"
|
||||||
th:text="#{form.password.forgot}"
|
th:text="#{form.password.forgot}"
|
||||||
>
|
>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
<div class="halo-form-wrapper">
|
<div class="halo-form-wrapper">
|
||||||
<h1 class="form-title" th:text="${sent} ? #{sent.title} : #{title}"></h1>
|
<h1 class="form-title" th:text="${sent} ? #{sent.title} : #{title}"></h1>
|
||||||
<form th:replace="~{gateway_modules/form_fragments::passwordReset}"></form>
|
<form th:replace="~{gateway_modules/form_fragments::passwordReset}"></form>
|
||||||
|
|
||||||
|
<div th:replace="~{gateway_modules/common_fragments::passwordResetMethods}"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div th:replace="~{gateway_modules/common_fragments::languageSwitcher}"></div>
|
<div th:replace="~{gateway_modules/common_fragments::languageSwitcher}"></div>
|
Loading…
Reference in New Issue