added more style fixes for the login.v2 (#32708)

* added more style fixes for the login.v2

related: #32522
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed grant screen

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* test fixes

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fix for code.ftl

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* test fixes

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed tests

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

---------

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
Erik Jan de Wit 2024-09-11 20:52:49 +02:00 committed by GitHub
parent 125124c2d9
commit 9aad6f650d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 438 additions and 158 deletions

View file

@ -12,7 +12,7 @@ public class DeleteAccountActionConfirmPage extends RequiredActions {
@FindBy(css = "button[name='cancel-aia']") @FindBy(css = "button[name='cancel-aia']")
WebElement cancelActionButton; WebElement cancelActionButton;
@FindBy(css = "input[type='submit']") @FindBy(css = "button[type='submit']")
WebElement confirmActionButton; WebElement confirmActionButton;
@Override @Override

View file

@ -36,10 +36,10 @@ public class LoginActions extends LoginBase {
.path("login-actions"); .path("login-actions");
} }
@FindBy(css = "input[type='submit']") @FindBy(css = "button[type='submit']")
private WebElement submitButton; private WebElement submitButton;
@FindBy(css = "button[type='submit']") @FindBy(css = "button[name='cancel-aia']")
private WebElement cancelButton; private WebElement cancelButton;
public void submit() { public void submit() {

View file

@ -34,10 +34,10 @@ import static org.keycloak.testsuite.util.UIUtils.clickLink;
* @author Vaclav Muzikar <vmuzikar@redhat.com> * @author Vaclav Muzikar <vmuzikar@redhat.com>
*/ */
public class OAuthGrant extends RequiredActions { public class OAuthGrant extends RequiredActions {
@FindBy(css = "input[name=\"accept\"]") @FindBy(css = "button[name=\"accept\"]")
private WebElement acceptButton; private WebElement acceptButton;
@FindBy(css = "input[name=\"cancel\"]") @FindBy(css = "button[name=\"cancel\"]")
private WebElement cancelButton; private WebElement cancelButton;
@FindBy(xpath = "//div[@id='kc-oauth']/ul/li/span") @FindBy(xpath = "//div[@id='kc-oauth']/ul/li/span")

View file

@ -37,7 +37,7 @@ public class OneTimeCode extends Authenticate {
@FindBy(css = "div[class^='pf-v5-c-alert'], div[class^='alert-error']") @FindBy(css = "div[class^='pf-v5-c-alert'], div[class^='alert-error']")
private WebElement loginErrorMessage; private WebElement loginErrorMessage;
@FindBy(id = "input-error-otp-code") @FindBy(id = "input-error-otp")
private WebElement totpInputCodeError; private WebElement totpInputCodeError;
public String getOtpLabel() { public String getOtpLabel() {

View file

@ -40,7 +40,7 @@ public class LoginPasswordUpdatePage {
@FindBy(id = "password-confirm") @FindBy(id = "password-confirm")
private WebElement passwordConfirmInput; private WebElement passwordConfirmInput;
@FindBy(css = "input[type=\"submit\"]") @FindBy(css = "button[type=\"submit\"]")
private WebElement submitButton; private WebElement submitButton;
@FindBy(css = "div[class^='pf-v5-c-alert'], div[class^='alert-error']") @FindBy(css = "div[class^='pf-v5-c-alert'], div[class^='alert-error']")

View file

@ -32,7 +32,7 @@ public class LoginPasswordResetPage extends LanguageComboboxAwarePage {
@FindBy(id = "input-error-username") @FindBy(id = "input-error-username")
private WebElement usernameError; private WebElement usernameError;
@FindBy(css = "input[type=\"submit\"]") @FindBy(css = "button[type=\"submit\"]")
private WebElement submitButton; private WebElement submitButton;
@FindBy(className = "pf-v5-c-success") @FindBy(className = "pf-v5-c-success")

View file

@ -32,7 +32,7 @@ public class LoginPasswordUpdatePage extends LogoutSessionsPage {
@FindBy(id = "password-confirm") @FindBy(id = "password-confirm")
private WebElement passwordConfirmInput; private WebElement passwordConfirmInput;
@FindBy(css = "input[type=\"submit\"]") @FindBy(css = "button[type=\"submit\"]")
private WebElement submitButton; private WebElement submitButton;
@FindBy(css = "div[class^='pf-v5-c-alert'], div[class^='alert-error']") @FindBy(css = "div[class^='pf-v5-c-alert'], div[class^='alert-error']")

View file

@ -22,6 +22,7 @@ import java.util.stream.Collectors;
import org.junit.Assert; import org.junit.Assert;
import org.keycloak.common.util.Retry; import org.keycloak.common.util.Retry;
import org.keycloak.testsuite.util.UIUtils; import org.keycloak.testsuite.util.UIUtils;
import org.keycloak.testsuite.util.WaitUtils;
import org.openqa.selenium.By; import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement; import org.openqa.selenium.WebElement;
@ -38,13 +39,13 @@ public class LoginTotpPage extends LanguageComboboxAwarePage {
@FindBy(id = "password-token") @FindBy(id = "password-token")
private WebElement passwordToken; private WebElement passwordToken;
@FindBy(css = "input[type=\"submit\"]") @FindBy(css = "button[type=\"submit\"]")
private WebElement submitButton; private WebElement submitButton;
@FindBy(css = "div[class^='pf-v5-c-alert'], div[class^='alert-error']") @FindBy(css = "div[class^='pf-v5-c-alert'], div[class^='alert-error']")
private WebElement loginErrorMessage; private WebElement loginErrorMessage;
@FindBy(id = "input-error-otp-code") @FindBy(id = "input-error-otp")
private WebElement totpInputCodeError; private WebElement totpInputCodeError;
public void login(String totp) { public void login(String totp) {
@ -88,7 +89,7 @@ public class LoginTotpPage extends LanguageComboboxAwarePage {
// If false, we don't expect that credentials combobox is available. If true, we expect that it is available on the page // If false, we don't expect that credentials combobox is available. If true, we expect that it is available on the page
public void assertOtpCredentialSelectorAvailability(boolean expectedAvailability) { public void assertOtpCredentialSelectorAvailability(boolean expectedAvailability) {
try { try {
driver.findElement(By.className("pf-c-tile")); driver.findElement(By.className("pf-v5-c-tile"));
Assert.assertTrue(expectedAvailability); Assert.assertTrue(expectedAvailability);
} catch (NoSuchElementException nse) { } catch (NoSuchElementException nse) {
Assert.assertFalse(expectedAvailability); Assert.assertFalse(expectedAvailability);
@ -113,21 +114,19 @@ public class LoginTotpPage extends LanguageComboboxAwarePage {
} }
private By getXPathForLookupAllCards() { private By getXPathForLookupAllCards() {
return By.xpath("//span[contains(@class, 'pf-c-tile__title')]"); return By.xpath("//span[contains(@class, 'pf-v5-c-tile__title')]");
} }
private By getCssSelectorForLookupActiveCard() { private By getCssSelectorForLookupActiveCard() {
return By.cssSelector(".pf-c-tile__input:checked + .pf-c-tile .pf-c-tile__title"); return By.cssSelector(".pf-m-selected");
} }
private By getXPathForLookupCardWithName(String credentialName) { private By getXPathForLookupCardWithName(String credentialName) {
return By.xpath("//label[contains(@class, 'pf-c-tile')][normalize-space() = '"+ credentialName +"']"); return By.xpath("//div[contains(@class, 'pf-v5-c-tile')][normalize-space() = '"+ credentialName +"']");
} }
public void selectOtpCredential(String credentialName) { public void selectOtpCredential(String credentialName) {
waitForElement(getCssSelectorForLookupActiveCard());
WebElement webElement = driver.findElement( WebElement webElement = driver.findElement(
getXPathForLookupCardWithName(credentialName)); getXPathForLookupCardWithName(credentialName));
UIUtils.clickLink(webElement); UIUtils.clickLink(webElement);

View file

@ -41,9 +41,9 @@ public class OAuthGrantPage extends LanguageComboboxAwarePage {
public static final String OFFLINE_ACCESS_CONSENT_TEXT = "Offline Access"; public static final String OFFLINE_ACCESS_CONSENT_TEXT = "Offline Access";
public static final String ROLES_CONSENT_TEXT = "User roles"; public static final String ROLES_CONSENT_TEXT = "User roles";
@FindBy(css = "input[name=\"accept\"]") @FindBy(css = "button[name=\"accept\"]")
private WebElement acceptButton; private WebElement acceptButton;
@FindBy(css = "input[name=\"cancel\"]") @FindBy(css = "button[name=\"cancel\"]")
private WebElement cancelButton; private WebElement cancelButton;

View file

@ -18,11 +18,13 @@ package org.keycloak.testsuite.util.saml;
import org.keycloak.testsuite.util.SamlClient.Step; import org.keycloak.testsuite.util.SamlClient.Step;
import org.keycloak.testsuite.util.SamlClientBuilder; import org.keycloak.testsuite.util.SamlClientBuilder;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URI; import java.net.URI;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder; import jakarta.ws.rs.core.UriBuilder;
import org.apache.http.NameValuePair; import org.apache.http.NameValuePair;
@ -37,12 +39,12 @@ import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils; import org.apache.http.util.EntityUtils;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Element; import org.jsoup.nodes.Element;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC; import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
/** /**
*
* @author hmlnarik * @author hmlnarik
*/ */
public class RequiredConsentBuilder implements Step { public class RequiredConsentBuilder implements Step {
@ -88,18 +90,18 @@ public class RequiredConsentBuilder implements Step {
for (Element form : theLoginPage.getElementsByTag("form")) { for (Element form : theLoginPage.getElementsByTag("form")) {
String method = form.attr("method"); String method = form.attr("method");
String action = form.attr("action"); String action = form.attr("action");
boolean isPost = method != null && "post".equalsIgnoreCase(method); boolean isPost = "post".equalsIgnoreCase(method);
Element submitButton;
if (approveConsent) {
submitButton = form.getElementById("kc-login");
} else {
submitButton = form.getElementById("kc-cancel");
}
parameters.add(new BasicNameValuePair(submitButton.attr("name"), submitButton.attr("value")));
for (Element input : form.getElementsByTag("input")) { for (Element input : form.getElementsByTag("input")) {
if (Objects.equals(input.id(), "kc-login")) { parameters.add(new BasicNameValuePair(input.attr("name"), input.val()));
if (approveConsent)
parameters.add(new BasicNameValuePair(input.attr("name"), input.attr("value")));
} else if (Objects.equals(input.id(), "kc-cancel")) {
if (!approveConsent)
parameters.add(new BasicNameValuePair(input.attr("name"), input.attr("value")));
} else {
parameters.add(new BasicNameValuePair(input.attr("name"), input.val()));
}
} }
if (isPost) { if (isPost) {

View file

@ -181,6 +181,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
Assert.assertTrue(oneTimeCodePage.isOtpLabelPresent()); Assert.assertTrue(oneTimeCodePage.isOtpLabelPresent());
loginTotpPage.assertCurrent(); loginTotpPage.assertCurrent();
loginTotpPage.assertOtpCredentialSelectorAvailability(true); loginTotpPage.assertOtpCredentialSelectorAvailability(true);
loginTotpPage.selectOtpCredential("first");
// Check that selected credential is "first" // Check that selected credential is "first"
Assert.assertEquals("first", loginTotpPage.getSelectedOtpCredential()); Assert.assertEquals("first", loginTotpPage.getSelectedOtpCredential());
@ -207,6 +208,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
Assert.assertTrue(oneTimeCodePage.isOtpLabelPresent()); Assert.assertTrue(oneTimeCodePage.isOtpLabelPresent());
loginTotpPage.assertCurrent(); loginTotpPage.assertCurrent();
loginTotpPage.assertOtpCredentialSelectorAvailability(true); loginTotpPage.assertOtpCredentialSelectorAvailability(true);
loginTotpPage.selectOtpCredential(orderedCredentials.get(0));
// Check that preferred credential is selected // Check that preferred credential is selected
Assert.assertEquals(orderedCredentials.get(0), loginTotpPage.getSelectedOtpCredential()); Assert.assertEquals(orderedCredentials.get(0), loginTotpPage.getSelectedOtpCredential());

View file

@ -1370,7 +1370,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
newPassword.sendKeys("resetPassword"); newPassword.sendKeys("resetPassword");
final WebElement confirmPassword = driver.findElement(By.id("password-confirm")); final WebElement confirmPassword = driver.findElement(By.id("password-confirm"));
confirmPassword.sendKeys("resetPassword"); confirmPassword.sendKeys("resetPassword");
final WebElement submit = driver.findElement(By.cssSelector("input[type=\"submit\"]")); final WebElement submit = driver.findElement(By.cssSelector("button[type=\"submit\"]"));
submit.click(); submit.click();
} }

View file

@ -3,12 +3,17 @@
<div class="${properties.kcFormActionGroupClass}"> <div class="${properties.kcFormActionGroupClass}">
<#nested> <#nested>
</div> </div>
</div>
</#macro> </#macro>
<#macro button id name label class=["kcButtonPrimaryClass"]> <#macro button label id="" name="" class=["kcButtonPrimaryClass"]>
<button class="<#list class as c>${properties[c]} </#list>" name="${name}" id="${id}" type="submit">${msg(label)}</button> <button class="<#list class as c>${properties[c]} </#list>" name="${name}" id="${id}" type="submit">${msg(label)}</button>
</#macro> </#macro>
<#macro buttonLink href label id="" class=["kcButtonSecondaryClass"]>
<a id="${id}" href="${href}" class="<#list class as c>${properties[c]} </#list>">${kcSanitize(msg(label))?no_esc}</a>
</#macro>
<#macro loginButton> <#macro loginButton>
<@buttons.actionGroup> <@buttons.actionGroup>
<@buttons.button id="kc-login" name="login" label="doLogIn" class=["kcButtonPrimaryClass", "kcButtonBlockClass"] /> <@buttons.button id="kc-login" name="login" label="doLogIn" class=["kcButtonPrimaryClass", "kcButtonBlockClass"] />

View file

@ -0,0 +1,20 @@
<#import "template.ftl" as layout>
<#import "field.ftl" as field>
<@layout.registrationLayout; section>
<#if section = "header">
<#if code.success>
${msg("codeSuccessTitle")}
<#else>
${kcSanitize(msg("codeErrorTitle", code.error))}
</#if>
<#elseif section = "form">
<div id="kc-code">
<#if code.success>
<p>${msg("copyCodeInstruction")}</p>
<@field.input name="code" label="" value=code.code />
<#else>
<p id="error">${kcSanitize(code.error)}</p>
</#if>
</div>
</#if>
</@layout.registrationLayout>

View file

@ -0,0 +1,40 @@
<#import "template.ftl" as layout>
<#import "buttons.ftl" as buttons>
<@layout.registrationLayout; section>
<!-- template: delete-account-confirm.ftl -->
<#if section = "header">
${msg("deleteAccountConfirm")}
<#elseif section = "form">
<form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-deleteaccount-form" method="post">
<div class="${properties.kcAlertClass!} pf-m-warning">
<div class="${properties.kcAlertIconClass!}">
<i class="${properties.kcFeedbackWarningIcon!}" aria-hidden="true"></i>
</div>
<span class="${properties.kcAlertTitleClass!}">
${msg("irreversibleAction")}
</span>
</div>
<p>${msg("deletingImplies")}</p>
<ul class="pf-v5-c-list" role="list">
<li>${msg("loggingOutImmediately")}</li>
<li>${msg("errasingData")}</li>
</ul>
<p class="delete-account-text">${msg("finalDeletionConfirmation")}</p>
<@buttons.actionGroup>
<@buttons.button label="doConfirmDelete" class=["kcButtonPrimaryClass"]/>
<#if triggered_from_aia>
<@buttons.button name="cancel-aia" label="doCancel" class=["kcButtonSecondaryClass"]/>
</#if>
</@buttons.actionGroup>
</form>
</#if>
</@layout.registrationLayout>

View file

@ -0,0 +1,21 @@
<#import "template.ftl" as layout>
<#import "buttons.ftl" as buttons>
<@layout.registrationLayout displayMessage=false; section>
<!-- template: delete-credential.ftl -->
<#if section = "header">
${msg("deleteCredentialTitle", credentialLabel)}
<#elseif section = "form">
<div id="kc-delete-text" class="${properties.kcContentWrapperClass!}">
${msg("deleteCredentialMessage", credentialLabel)}
</div>
<form class="${properties.kcFormClass!}" action="${url.loginAction}" method="POST">
<@buttons.actionGroup>
<@buttons.button name="accept" id="kc-accept" label="doConfirmDelete" class=["kcButtonPrimaryClass"]/>
<@buttons.button name="cancel-aia" id="kc-decline" label="doDecline" class=["kcButtonSecondaryClass"]/>
</@buttons.actionGroup>
</form
</#if>
</@layout.registrationLayout>

View file

@ -41,19 +41,19 @@
</#if> </#if>
</#macro> </#macro>
<#macro input name label value="" required=false> <#macro input name label value="" required=false autocomplete="off" fieldName=name>
<#assign error=kcSanitize(messagesPerField.get(name))?no_esc> <#assign error=kcSanitize(messagesPerField.get(fieldName))?no_esc>
<@field.group name=name label=label error=error required=required> <@field.group name=name label=label error=error required=required>
<span class="${properties.kcInputClass} <#if error?has_content>${properties.kcError}</#if>"> <span class="${properties.kcInputClass} <#if error?has_content>${properties.kcError}</#if>">
<input id="${name}" name="${name}" value="${value}" type="text" autocomplete="off" <input id="${name}" name="${name}" value="${value}" type="text" autocomplete="${autocomplete}"
aria-invalid="<#if error?has_content>true</#if>"/> aria-invalid="<#if error?has_content>true</#if>"/>
<@errorIcon error=error/> <@errorIcon error=error/>
</span> </span>
</@field.group> </@field.group>
</#macro> </#macro>
<#macro password name label value="" required=false forgotPassword=false> <#macro password name label value="" required=false forgotPassword=false fieldName=name>
<#assign error=kcSanitize(messagesPerField.get(name))?no_esc> <#assign error=kcSanitize(messagesPerField.get(fieldName))?no_esc>
<@field.group name=name label=label error=error required=required> <@field.group name=name label=label error=error required=required>
<div class="${properties.kcInputGroup}"> <div class="${properties.kcInputGroup}">
<div class="${properties.kcInputGroupItemClass} ${properties.kcFill}"> <div class="${properties.kcInputGroupItemClass} ${properties.kcFill}">

View file

@ -0,0 +1,59 @@
<#import "template.ftl" as layout>
<#import "buttons.ftl" as buttons>
<@layout.registrationLayout bodyClass="oauth"; section>
<#if section = "header">
<#if client.attributes.logoUri??>
<img src="${client.attributes.logoUri}"/>
</#if>
<p>
<#if client.name?has_content>
${msg("oauthGrantTitle",advancedMsg(client.name))}
<#else>
${msg("oauthGrantTitle",client.clientId)}
</#if>
</p>
<#elseif section = "form">
<div id="kc-oauth" class="content-area">
<h3>${msg("oauthGrantRequest")}</h3>
<ul class="${properties.kcListClass!}">
<#if oauth.clientScopesRequested??>
<#list oauth.clientScopesRequested as clientScope>
<li>
<span><#if !clientScope.dynamicScopeParameter??>
${advancedMsg(clientScope.consentScreenText)}
<#else>
${advancedMsg(clientScope.consentScreenText)}: <b>${clientScope.dynamicScopeParameter}</b>
</#if>
</span>
</li>
</#list>
</#if>
</ul>
<#if client.attributes.policyUri?? || client.attributes.tosUri??>
<h3>
<#if client.name?has_content>
${msg("oauthGrantInformation",advancedMsg(client.name))}
<#else>
${msg("oauthGrantInformation",client.clientId)}
</#if>
<#if client.attributes.tosUri??>
${msg("oauthGrantReview")}
<a href="${client.attributes.tosUri}" target="_blank">${msg("oauthGrantTos")}</a>
</#if>
<#if client.attributes.policyUri??>
${msg("oauthGrantReview")}
<a href="${client.attributes.policyUri}" target="_blank">${msg("oauthGrantPolicy")}</a>
</#if>
</h3>
</#if>
<form class="${properties.kcFormClass} ${properties.kcMarginTopClass!}" action="${url.oauthAction}" method="POST">
<input type="hidden" name="code" value="${oauth.code}">
<@buttons.actionGroup>
<@buttons.button id="kc-login" name="accept" label="doYes"/>
<@buttons.button id="kc-cancel" name="cancel" label="doNo" class=["kcButtonSecondaryClass"]/>
</@buttons.actionGroup>
</form>
</div>
</#if>
</@layout.registrationLayout>

View file

@ -0,0 +1,45 @@
<#import "template.ftl" as layout>
<#import "field.ftl" as field>
<#import "buttons.ftl" as buttons>
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('totp'); section>
<!-- template: login-otp.ftl -->
<#if section="header">
${msg("doLogIn")}
<#elseif section="form">
<form id="kc-otp-login-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<input id="selectedCredentialId" type="hidden" name="selectedCredentialId" value="${otpLogin.selectedCredentialId!''}">
<#if otpLogin.userOtpCredentials?size gt 1>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcInputWrapperClass!}">
<#list otpLogin.userOtpCredentials as otpCredential>
<div id="kc-otp-credential-${otpCredential?index}" class="${properties.kcLoginOTPListClass!}"
onclick="toggleOTP(${otpCredential?index}, '${otpCredential.id}')">
<span class="${properties.kcLoginOTPListItemHeaderClass!}">
<span class="${properties.kcLoginOTPListItemIconBodyClass!}">
<i class="${properties.kcLoginOTPListItemIconClass!}" aria-hidden="true"></i>
</span>
<span class="${properties.kcLoginOTPListItemTitleClass!}">${otpCredential.userLabel}</span>
</span>
</div>
</#list>
</div>
</div>
</#if>
<@field.input name="otp" label=msg("loginOtpOneTime") autocomplete="one-time-code" fieldName="totp" />
<@buttons.loginButton />
</form>
<script>
function toggleOTP(index, value) {
// select the clicked OTP credential
document.getElementById("selectedCredentialId").value = value;
// remove selected class from all OTP credentials
Array.from(document.getElementsByClassName("${properties.kcLoginOTPListSelectedClass!}")).map(i => i.classList.remove("${properties.kcLoginOTPListSelectedClass!}"));
// add selected class to the clicked OTP credential
document.getElementById("kc-otp-credential-" + index).classList.add("${properties.kcLoginOTPListSelectedClass!}");
}
</script>
</#if>
</@layout.registrationLayout>

View file

@ -0,0 +1,27 @@
<#import "template.ftl" as layout>
<#import "field.ftl" as field>
<#import "buttons.ftl" as buttons>
<@layout.registrationLayout displayInfo=true displayMessage=!messagesPerField.existsError('username'); section>
<#if section = "header">
${msg("emailForgotTitle")}
<#elseif section = "form">
<form id="kc-reset-password-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<#assign label>
<#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if>
</#assign>
<@field.input name="username" label=label value=auth.attemptedUsername!''/>
<@buttons.actionGroup>
<@buttons.button id="kc-form-buttons" label="doSubmit" class=["kcButtonPrimaryClass", "kcButtonBlockClass"]/>
<@buttons.buttonLink href=url.loginUrl label="backToLogin" class=["kcButtonSecondaryClass", "kcButtonBlockClass"]/>
</@buttons.actionGroup>
</form>
<#elseif section = "info" >
<#if realm.duplicateEmailsAllowed>
${msg("emailInstructionUsername")}
<#else>
${msg("emailInstruction")}
</#if>
</#if>
</@layout.registrationLayout>

View file

@ -1,79 +1,28 @@
<#import "template.ftl" as layout> <#import "template.ftl" as layout>
<#import "password-commons.ftl" as passwordCommons> <#import "password-commons.ftl" as passwordCommons>
<#import "field.ftl" as field>
<#import "buttons.ftl" as buttons>
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('password','password-confirm'); section> <@layout.registrationLayout displayMessage=!messagesPerField.existsError('password','password-confirm'); section>
<!-- template: login-update-password.ftl -->
<#if section = "header"> <#if section = "header">
${msg("updatePasswordTitle")} ${msg("updatePasswordTitle")}
<#elseif section = "form"> <#elseif section = "form">
<form id="kc-passwd-update-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post" novalidate="novalidate"> <form id="kc-passwd-update-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post" novalidate="novalidate">
<div class="${properties.kcFormGroupClass!}"> <@field.password name="password-new" label=msg("passwordNew") fieldName="password" />
<label for="password-new" class="${properties.kcLabelClass!}"> <@field.password name="password-confirm" label=msg("passwordConfirm") />
<span class="pf-v5-c-form__label-text">
${msg("passwordNew")}
</span>
</label>
<div class="${properties.kcInputGroup!}">
<span class="${properties.kcInputClass!}" dir="ltr">
<input type="password" id="password-new" name="password-new" autofocus autocomplete="new-password"
aria-invalid="<#if messagesPerField.existsError('password','password-confirm')>true</#if>"
/>
</span>
<button class="${properties.kcFormPasswordVisibilityButtonClass!}" type="button" aria-label="${msg('showPassword')}"
aria-controls="password-new" data-password-toggle
data-icon-show="${properties.kcFormPasswordVisibilityIconShow!}" data-icon-hide="${properties.kcFormPasswordVisibilityIconHide!}"
data-label-show="${msg('showPassword')}" data-label-hide="${msg('hidePassword')}">
<i class="${properties.kcFormPasswordVisibilityIconShow!}" aria-hidden="true"></i>
</button>
</div>
<#if messagesPerField.existsError('password')>
<span id="input-error-password" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('password'))?no_esc}
</span>
</#if>
</div>
<div class="${properties.kcFormGroupClass!}">
<label for="password-confirm" class="${properties.kcLabelClass!}">
<span class="pf-v5-c-form__label-text">
${msg("passwordConfirm")}
</span>
</label>
<div class="${properties.kcInputGroup!}">
<span class="${properties.kcInputClass!}" dir="ltr">
<input type="password" id="password-confirm" name="password-confirm"
autocomplete="new-password"
aria-invalid="<#if messagesPerField.existsError('password-confirm')>true</#if>"
/>
</span>
<button class="${properties.kcFormPasswordVisibilityButtonClass!}" type="button" aria-label="${msg('showPassword')}"
aria-controls="password-confirm" data-password-toggle
data-icon-show="${properties.kcFormPasswordVisibilityIconShow!}" data-icon-hide="${properties.kcFormPasswordVisibilityIconHide!}"
data-label-show="${msg('showPassword')}" data-label-hide="${msg('hidePassword')}">
<i class="${properties.kcFormPasswordVisibilityIconShow!}" aria-hidden="true"></i>
</button>
</div>
<#if messagesPerField.existsError('password-confirm')>
<span id="input-error-password-confirm" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('password-confirm'))?no_esc}
</span>
</#if>
</div>
<div class="${properties.kcFormGroupClass!}"> <div class="${properties.kcFormGroupClass!}">
<@passwordCommons.logoutOtherSessions/> <@passwordCommons.logoutOtherSessions/>
</div> </div>
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!} pf-m-action"> <@buttons.actionGroup>
<div class="pf-v5-c-form__actions"> <#if isAppInitiatedAction??>
<#if isAppInitiatedAction??> <@buttons.button label="doSubmit" class=["kcButtonPrimaryClass"]/>
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" /> <@buttons.button label="doCancel" name="cancel-aia" class=["kcButtonSecondaryClass"]/>
<button class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" type="submit" name="cancel-aia" value="true" />${msg("doCancel")}</button> <#else>
<#else> <@buttons.button label="doSubmit" class=["kcButtonPrimaryClass", "kcButtonBlockClass"]/>
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" /> </#if>
</#if> </@buttons.actionGroup>
</div>
</div>
</form> </form>
</#if> </#if>
</@layout.registrationLayout> </@layout.registrationLayout>

View file

@ -17,7 +17,7 @@
<#assign label> <#assign label>
<#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if> <#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if>
</#assign> </#assign>
<@field.input name="username" label=label value="${(login.username!'')}" /> <@field.input name="username" label=label value=login.username!'' />
<#if messagesPerField.existsError('username')> <#if messagesPerField.existsError('username')>
<span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite"> <span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">

View file

@ -15,7 +15,7 @@
<#assign label> <#assign label>
<#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if> <#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if>
</#assign> </#assign>
<@field.input name="username" label=label value="${(login.username!'')}" /> <@field.input name="username" label=label value=login.username!'' />
</#if> </#if>
<@field.password name="password" label=msg("password") forgotPassword=realm.resetPasswordAllowed/> <@field.password name="password" label=msg("password") forgotPassword=realm.resetPasswordAllowed/>
@ -42,7 +42,7 @@
</div> </div>
</#if> </#if>
<#elseif section = "socialProviders" > <#elseif section = "socialProviders" >
<#if realm.password && social?? && social.providers?has_content> <#if realm.password && social.providers?? && social.providers?has_content>
<div class="pf-v5-c-login__main-footer-band"> <div class="pf-v5-c-login__main-footer-band">
<p class="pf-v5-c-login__main-footer-band-item"> <p class="pf-v5-c-login__main-footer-band-item">
${msg("identity-provider-login-label")} ${msg("identity-provider-login-label")}

View file

@ -173,7 +173,7 @@
<#if message.type = 'error'><span class="${properties.kcFeedbackErrorIcon!}"></span></#if> <#if message.type = 'error'><span class="${properties.kcFeedbackErrorIcon!}"></span></#if>
<#if message.type = 'info'><span class="${properties.kcFeedbackInfoIcon!}"></span></#if> <#if message.type = 'info'><span class="${properties.kcFeedbackInfoIcon!}"></span></#if>
</div> </div>
<span class="${properties.kcAlertTitleClass!} kc-feedback-text">${kcSanitize(message.summary)?no_esc}</span> <span class="${properties.kcAlertTitleClass!} kc-feedback-text">${kcSanitize(message.summary)?no_esc}</span>
</div> </div>
</#if> </#if>
@ -181,11 +181,11 @@
<#if auth?has_content && auth.showTryAnotherWayLink()> <#if auth?has_content && auth.showTryAnotherWayLink()>
<form id="kc-select-try-another-way-form" action="${url.loginAction}" method="post" novalidate="novalidate"> <form id="kc-select-try-another-way-form" action="${url.loginAction}" method="post" novalidate="novalidate">
<div class="${properties.kcFormGroupClass!}"> <input type="hidden" name="tryAnotherWay" value="on"/>
<input type="hidden" name="tryAnotherWay" value="on"/> <a id="try-another-way" href="javascript:document.forms['kc-select-try-another-way-form'].submit()"
<a href="#" id="try-another-way" class="${properties.kcButtonSecondaryClass} ${properties.kcButtonBlockClass} ${properties.kcMarginTopClass}">
onclick="document.forms['kc-select-try-another-way-form'].submit();return false;">${msg("doTryAnotherWay")}</a> ${kcSanitize(msg("doTryAnotherWay"))?no_esc}
</div> </a>
</form> </form>
</#if> </#if>

View file

@ -0,0 +1,21 @@
<#import "template.ftl" as layout>
<#import "buttons.ftl" as buttons>
<@layout.registrationLayout displayMessage=false; section>
<!-- template: terms.ftl -->
<#if section = "header">
${msg("termsTitle")}
<#elseif section = "form">
<div class="${properties.kcContentWrapperClass}">
${kcSanitize(msg("termsText"))?no_esc}
</div>
<form class="${properties.kcFormClass!}" action="${url.loginAction}" method="POST">
<@buttons.actionGroup>
<@buttons.button name="accept" id="kc-accept" label="doAccept" class=["kcButtonPrimaryClass"]/>
<@buttons.button name="cancel" id="kc-decline" label="doDecline" class=["kcButtonSecondaryClass"]/>
</@buttons.actionGroup>
</form>
<div class="clearfix"></div>
</#if>
</@layout.registrationLayout>

View file

@ -44,6 +44,7 @@ kcListClass=pf-v5-c-list
kcButtonClass=pf-v5-c-button kcButtonClass=pf-v5-c-button
kcButtonPrimaryClass=pf-v5-c-button pf-m-primary kcButtonPrimaryClass=pf-v5-c-button pf-m-primary
kcButtonSecondaryClass=pf-v5-c-button pf-m-secondary
kcButtonBlockClass=pf-m-block kcButtonBlockClass=pf-m-block
kcButtonLinkClass=pf-v5-c-button pf-m-link kcButtonLinkClass=pf-v5-c-button pf-m-link
kcCommonLogoIdP=pf-v5-c-login__main-footer-links-item kcCommonLogoIdP=pf-v5-c-login__main-footer-links-item
@ -79,3 +80,11 @@ kcLoginMainBody=pf-v5-c-login__main-body
kcContentWrapperClass=pf-v5-u-mb-md-on-md kcContentWrapperClass=pf-v5-u-mb-md-on-md
kcWebAuthnDefaultIcon=pf-v5-c-icon pf-m-lg kcWebAuthnDefaultIcon=pf-v5-c-icon pf-m-lg
kcMarginTopClass=pf-v5-u-mt-md-on-md
kcLoginOTPListClass=pf-v5-c-tile
kcLoginOTPListItemHeaderClass=pf-v5-c-tile__header pf-m-stacked
kcLoginOTPListItemIconBodyClass=pf-v5-c-tile__icon
kcLoginOTPListItemIconClass=fa fa-mobile
kcLoginOTPListItemTitleClass=pf-v5-c-tile__title
kcLoginOTPListSelectedClass=pf-m-selected

View file

@ -127,28 +127,48 @@
</#macro> </#macro>
<#macro selectTag attribute> <#macro selectTag attribute>
<select id="${attribute.name}" name="${attribute.name}" class="${properties.kcInputClass!}" <div class="${properties.kcInputClass!}">
aria-invalid="<#if messagesPerField.existsError('${attribute.name}')>true</#if>" <select id="${attribute.name}" name="${attribute.name}"
<#if attribute.readOnly>disabled</#if> aria-invalid="<#if messagesPerField.existsError('${attribute.name}')>true</#if>"
<#if attribute.annotations.inputType=='multiselect'>multiple</#if> <#if attribute.readOnly>disabled</#if>
<#if attribute.annotations.inputTypeSize??>size="${attribute.annotations.inputTypeSize}"</#if> <#if attribute.annotations.inputType=='multiselect'>multiple</#if>
> <#if attribute.annotations.inputTypeSize??>size="${attribute.annotations.inputTypeSize}"</#if>
<#if attribute.annotations.inputType=='select'> >
<option value=""></option> <#if attribute.annotations.inputType=='select'>
</#if> <option value=""></option>
</#if>
<#if attribute.annotations.inputOptionsFromValidation?? && attribute.validators[attribute.annotations.inputOptionsFromValidation]?? && attribute.validators[attribute.annotations.inputOptionsFromValidation].options??> <#if attribute.annotations.inputOptionsFromValidation?? && attribute.validators[attribute.annotations.inputOptionsFromValidation]?? && attribute.validators[attribute.annotations.inputOptionsFromValidation].options??>
<#assign options=attribute.validators[attribute.annotations.inputOptionsFromValidation].options> <#assign options=attribute.validators[attribute.annotations.inputOptionsFromValidation].options>
<#elseif attribute.validators.options?? && attribute.validators.options.options??> <#elseif attribute.validators.options?? && attribute.validators.options.options??>
<#assign options=attribute.validators.options.options> <#assign options=attribute.validators.options.options>
<#else> <#else>
<#assign options=[]> <#assign options=[]>
</#if> </#if>
<#list options as option> <#list options as option>
<option value="${option}" <#if attribute.values?seq_contains(option)>selected</#if>><@selectOptionLabelText attribute=attribute option=option/></option> <option value="${option}" <#if attribute.values?seq_contains(option)>selected</#if>><@selectOptionLabelText attribute=attribute option=option/></option>
</#list> </#list>
</select> </select>
<span class="${properties.kcFormControlUtilClass}">
<span class="${properties.kcFormControlToggleIcon!}">
<svg
class="pf-v5-svg"
viewBox="0 0 320 512"
fill="currentColor"
aria-hidden="true"
role="img"
width="1em"
height="1em"
>
<path
d="M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z"
>
</path>
</svg>
</span>
</span>
</div>
</#macro> </#macro>
<#macro inputTagSelects attribute> <#macro inputTagSelects attribute>

View file

@ -0,0 +1,61 @@
<#import "template.ftl" as layout>
<#import "password-commons.ftl" as passwordCommons>
<#import "buttons.ftl" as buttons>
<@layout.registrationLayout; section>
<#if section = "title">
title
<#elseif section = "header">
<span class="${properties.kcWebAuthnKeyIcon!}"></span>
${kcSanitize(msg("webauthn-registration-title"))?no_esc}
<#elseif section = "form">
<form id="register" action="${url.loginAction}" method="post">
<div class="${properties.kcFormGroupClass!}">
<input type="hidden" id="clientDataJSON" name="clientDataJSON"/>
<input type="hidden" id="attestationObject" name="attestationObject"/>
<input type="hidden" id="publicKeyCredentialId" name="publicKeyCredentialId"/>
<input type="hidden" id="authenticatorLabel" name="authenticatorLabel"/>
<input type="hidden" id="transports" name="transports"/>
<input type="hidden" id="error" name="error"/>
<@passwordCommons.logoutOtherSessions/>
</div>
</form>
<script type="module">
import { registerByWebAuthn } from "${url.resourcesPath}/js/webauthnRegister.js";
const registerButton = document.getElementById('registerWebAuthn');
registerButton.addEventListener("click", function() {
const input = {
challenge : '${challenge}',
userid : '${userid}',
username : '${username}',
signatureAlgorithms : [<#list signatureAlgorithms as sigAlg>${sigAlg?c},</#list>],
rpEntityName : '${rpEntityName}',
rpId : '${rpId}',
attestationConveyancePreference : '${attestationConveyancePreference}',
authenticatorAttachment : '${authenticatorAttachment}',
requireResidentKey : '${requireResidentKey}',
userVerificationRequirement : '${userVerificationRequirement}',
createTimeout : ${createTimeout},
excludeCredentialIds : '${excludeCredentialIds}',
initLabel : "${msg("webauthn-registration-init-label")?no_esc}",
initLabelPrompt : "${msg("webauthn-registration-init-label-prompt")?no_esc}",
errmsg : "${msg("webauthn-unsupported-browser-text")?no_esc}"
};
registerByWebAuthn(input);
});
</script>
<div class="pf-v5-u-py-lg ${properties.kcFormClass!}">
<@buttons.actionGroup>
<@buttons.button id="registerWebAuthn" label="doRegisterSecurityKey" />
<#if !isSetRetry?has_content && isAppInitiatedAction?has_content>
<form action="${url.loginAction}" id="kc-webauthn-settings-form" method="post">
<@buttons.button id="cancelWebAuthnAIA" name="cancel-aia" label="doCancel" class=["kcButtonSecondaryClass"]/>
</form>
</#if>
</@buttons.actionGroup>
</div>
</#if>
</@layout.registrationLayout>