KEYCLOAK-74 Adapting the Forget password forms to original design

This commit is contained in:
vrockai 2013-09-30 16:51:41 +02:00
parent 625061002f
commit 144f5f9cfd
15 changed files with 145 additions and 27 deletions

View file

@ -21,6 +21,8 @@
*/
package org.keycloak.forms;
import org.keycloak.services.resources.flows.FormFlows;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@ -28,12 +30,32 @@ public class ErrorBean {
private String summary;
private FormFlows.ErrorType type;
// Message is considered ERROR by default
public ErrorBean(String summary) {
this(summary, FormFlows.ErrorType.ERROR);
}
public ErrorBean(String summary, FormFlows.ErrorType type) {
this.summary = summary;
this.type = type;
}
public String getSummary() {
return summary;
}
public boolean isSuccess(){
return FormFlows.ErrorType.SUCCESS.equals(this.type);
}
public boolean isWarning(){
return FormFlows.ErrorType.WARNING.equals(this.type);
}
public boolean isError(){
return FormFlows.ErrorType.ERROR.equals(this.type);
}
}

View file

@ -42,6 +42,7 @@ import org.keycloak.forms.TotpBean;
import org.keycloak.forms.UrlBean;
import org.keycloak.forms.UserBean;
import org.keycloak.services.FormService;
import org.keycloak.services.resources.flows.FormFlows;
import org.keycloak.services.resources.flows.Pages;
/**
@ -149,6 +150,10 @@ public class FormServiceImpl implements FormService {
private class CommandPassword implements Command {
public void exec(Map<String, Object> attributes, FormServiceDataBean dataBean) {
if (dataBean.getError() != null){
attributes.put("message", new ErrorBean(dataBean.getError(), dataBean.getErrorType()));
}
RealmBean realm = new RealmBean(dataBean.getRealm());
attributes.put("realm", realm);

View file

@ -250,19 +250,19 @@ button.primary:enabled:active {
background-position: 1.27272727272727em 1.63636363636364em;
}
.feedback.error {
background-image: url(img/feedback-error-arrow-down.svg);
background-image: url(img/feedback-error-arrow-down.png);
}
.feedback.error p {
border-color: #b91415;
background-image: url(img/feedback-error-sign.svg);
background-image: url(img/feedback-error-sign.png);
background-color: #f8e7e7;
}
.feedback.success {
background-image: url(img/feedback-success-arrow-down.svg);
background-image: url(img/feedback-success-arrow-down.png);
}
.feedback.success p {
border-color: #4b9e39;
background-image: url(img/feedback-success-sign.svg);
background-image: url(img/feedback-success-sign.png);
background-color: #e4f1e1;
}
.feedback.warning p {

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

View file

@ -324,6 +324,12 @@ a.zocial:before {
line-height: 1.3em;
}
.rcue-login-register.reset p.subtitle {
margin-bottom: 10px;
position: inherit;
text-align: right;
}
.rcue-login-register .background-area p.instruction.instruction.second {
color: #999999;
}

View file

@ -1,30 +1,36 @@
<#import "template-login-action.ftl" as layout>
<@layout.registrationLayout bodyClass=""; section>
<@layout.registrationLayout bodyClass="reset"; section>
<#if section = "title">
Reset password
${rb.getString('emailForgotHeader')}
<#elseif section = "header">
Reset password
${rb.getString('emailForgotHeader')}
<#elseif section = "form">
<div id="form">
<#if message?has_content>
<#if message.success>
<div class="feedback success bottom-left show"><p><strong>${rb.getString('successHeader')}</strong> ${rb.getString(message.summary)}</p></div>
</#if>
<#if message.error>
<div class="feedback error bottom-left show"><p><strong>${rb.getString('errorHeader')}</strong><br/>${rb.getString(message.summary)}</p></div>
</#if>
</#if>
<p class="instruction">${rb.getString('emailInstruction')}</p>
<form action="${url.passwordResetUrl}" method="post">
<div>
<label for="username">${rb.getString('username')}</label>
<input id="username" name="username" type="text" />
<label for="username">${rb.getString('username')}</label><input id="username" name="username" type="text" />
</div>
<div>
<label for="email">${rb.getString('email')}</label>
<input type="text" id="email" name="email" />
<label for="email">${rb.getString('email')}</label><input type="text" id="email" name="email" />
</div>
<input class="btn-primary" type="submit" value="Submit" />
</form>
</div>
<#elseif section = "info" >
<div id="info">

View file

@ -1,24 +1,22 @@
<#import "template-login-action.ftl" as layout>
<@layout.registrationLayout bodyClass=""; section>
<@layout.registrationLayout bodyClass="reset"; section>
<#if section = "title">
Update password
${rb.getString('emailUpdateHeader')}
<#elseif section = "header">
Update password
${rb.getString('emailUpdateHeader')}
<#elseif section = "form">
<div id="form">
<form action="${url.passwordUrl}" method="post">
<div>
<label for="password-new">${rb.getString('passwordNew')}</label>
<input type="password" id="password-new" name="password-new" />
<label for="password-new">${rb.getString('passwordNew')}</label><input type="password" id="password-new" name="password-new" />
</div>
<div>
<label for="password-confirm">${rb.getString('passwordConfirm')}</label>
<input type="password" id="password-confirm" name="password-confirm" />
<label for="password-confirm">${rb.getString('passwordConfirm')}</label><input type="password" id="password-confirm" name="password-confirm" />
</div>
<input class="btn-primary" type="submit" value="Submit" />

View file

@ -17,6 +17,7 @@ email=Email
password=Password
passwordConfirm=Confirm Password
passwordNew=New Password
passwordNewConfirm=New Password confirmation
authenticatorCode=One-time-password
clientCertificate=Client Certificate
@ -39,3 +40,14 @@ invalidTotp=Invalid authenticator code
usernameExists=Username already exists
error=A system error has occured, contact admin
successHeader=Success!
errorHeader=Error!
# Forgot password part
emailForgotHeader=Forgot Your Password?
emailUpdateHeader=Update password
emailSent=You should receive an email shortly with further instructions.
emailError=Invalid username or email.
emailInstruction=Enter your username and email address and we will send you instructions on how to create a new password.

View file

@ -27,6 +27,7 @@ import javax.ws.rs.core.MultivaluedMap;
import org.keycloak.services.models.RealmModel;
import org.keycloak.services.models.UserModel;
import org.keycloak.services.resources.flows.FormFlows;
/**
* @author <a href="mailto:vrockai@redhat.com">Viliam Rockai</a>
@ -42,6 +43,9 @@ public interface FormService {
private RealmModel realm;
private UserModel userModel;
private String error;
private FormFlows.ErrorType errorType;
private MultivaluedMap<String, String> formData;
private URI baseURI;
@ -121,5 +125,13 @@ public interface FormService {
public void setUserModel(UserModel userModel) {
this.userModel = userModel;
}
public FormFlows.ErrorType getErrorType() {
return errorType;
}
public void setErrorType(FormFlows.ErrorType errorType) {
this.errorType = errorType;
}
}
}

View file

@ -103,9 +103,16 @@ public class EmailSender {
URI uri = builder.build(realm.getId());
StringBuilder sb = new StringBuilder();
sb.append("Hi ").append(user.getFirstName()).append(",\n\n");
sb.append("Someone just requested to change your Keycloak account's password. ");
sb.append("If this was you, click on the link below to set a new password:\n");
sb.append(uri.toString());
sb.append("\n");
sb.append("Expires in " + TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction()));
sb.append("\n\nThis link will expire within ").append(TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction()));
sb.append(" minutes.\n\n");
sb.append("If you don't want to reset your password, just ignore this message and nothing will be changed.\n\n");
sb.append("Thanks,\n");
sb.append("The Keycloak Team");
try {
send(user.getEmail(), "Reset password link", sb.toString());

View file

@ -240,8 +240,7 @@ public class AccountService {
UserModel user = realm.getUser(username);
if (user == null || !email.equals(user.getEmail())) {
return Flows.forms(realm, request, uriInfo).setError("Invalid username or email")
.forwardToAction(RequiredAction.UPDATE_PASSWORD);
return Flows.forms(realm, request, uriInfo).setError("emailError").forwardToPasswordReset();
}
Set<RequiredAction> requiredActions = new HashSet<RequiredAction>(user.getRequiredActions());
@ -253,7 +252,8 @@ public class AccountService {
new EmailSender().sendPasswordReset(user, realm, accessCode, uriInfo);
return Flows.forms(realm, request, uriInfo).forwardToPasswordReset();
return Flows.forms(realm, request, uriInfo).setError("emailSent").setErrorType(FormFlows.ErrorType.SUCCESS)
.forwardToPasswordReset();
}
@Path("email-verification")

View file

@ -53,7 +53,12 @@ public class FormFlows {
public static final String SOCIAL_REGISTRATION = "socialRegistration";
public static final String CODE = "code";
// TODO refactor/rename "error" to "message" everywhere where it makes sense
private String error;
public static enum ErrorType {SUCCESS, WARNING, ERROR};
private ErrorType errorType;
private MultivaluedMap<String, String> formData;
private RealmModel realm;
@ -98,6 +103,7 @@ public class FormFlows {
private Response forwardToForm(String template) {
FormService.FormServiceDataBean formDataBean = new FormService.FormServiceDataBean(realm, userModel, formData, error);
formDataBean.setErrorType(errorType == null ? ErrorType.ERROR : errorType);
// Getting URI needed by form processing service
ResteasyUriInfo uriInfo = request.getUri();
@ -172,6 +178,11 @@ public class FormFlows {
return this;
}
public FormFlows setErrorType(ErrorType errorType) {
this.errorType = errorType;
return this;
}
public FormFlows setUser(UserModel userModel) {
this.userModel = userModel;
return this;

View file

@ -85,12 +85,14 @@ public class ResetPasswordTest {
resetPasswordPage.assertCurrent();
Assert.assertEquals("Success!", resetPasswordPage.getMessage());
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
MimeMessage message = greenMail.getReceivedMessages()[0];
String body = (String) message.getContent();
String changePasswordUrl = body.split("\n")[0];
String changePasswordUrl = body.split("\n")[3];
driver.navigate().to(changePasswordUrl.trim());
@ -109,4 +111,34 @@ public class ResetPasswordTest {
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
}
@Test
public void resetPasswordWrongUsername() throws IOException, MessagingException {
loginPage.open();
loginPage.resetPassword();
resetPasswordPage.assertCurrent();
resetPasswordPage.changePassword("invalid", "test-user@localhost");
resetPasswordPage.assertCurrent();
Assert.assertNotEquals("Success!", resetPasswordPage.getMessage());
Assert.assertEquals("Error!", resetPasswordPage.getMessage());
}
@Test
public void resetPasswordWrongEmail() throws IOException, MessagingException {
loginPage.open();
loginPage.resetPassword();
resetPasswordPage.assertCurrent();
resetPasswordPage.changePassword("test-user@localhost", "invalid");
resetPasswordPage.assertCurrent();
Assert.assertNotEquals("Success!", resetPasswordPage.getMessage());
Assert.assertEquals("Error!", resetPasswordPage.getMessage());
}
}

View file

@ -38,6 +38,9 @@ public class LoginPasswordResetPage extends Page {
@FindBy(css = "input[type=\"submit\"]")
private WebElement submitButton;
@FindBy(css = ".feedback > p > strong")
private WebElement emailErrorMessage;
public void changePassword(String username, String email) {
usernameInput.sendKeys(username);
emailInput.sendKeys(email);
@ -46,11 +49,15 @@ public class LoginPasswordResetPage extends Page {
}
public boolean isCurrent() {
return driver.getTitle().equals("Reset password");
return driver.getTitle().equals("Forgot Your Password?");
}
public void open() {
throw new UnsupportedOperationException();
}
public String getMessage() {
return emailErrorMessage != null ? emailErrorMessage.getText() : null;
}
}