Made the login more modular

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
Erik Jan de Wit 2024-07-31 10:46:50 +02:00 committed by Stan Silvert
parent d82ca2ed41
commit e410a83c3c
11 changed files with 182 additions and 187 deletions

View file

@ -47,8 +47,11 @@ public class LoginPage extends LanguageComboboxAwarePage {
@FindBy(id = "password")
private WebElement passwordInput;
@FindBy(id = "input-error")
private WebElement inputError;
@FindBy(id = "input-error-username")
private WebElement userNameInputError;
@FindBy(id = "input-error-password")
private WebElement passwordInputError;
@FindBy(id = "rememberMe")
private WebElement rememberMe;
@ -161,11 +164,15 @@ public class LoginPage extends LanguageComboboxAwarePage {
public String getInputError() {
try {
return getTextFromElement(inputError);
return getTextFromElement(userNameInputError);
} catch (NoSuchElementException ex) {
try {
return getTextFromElement(passwordInputError);
} catch (NoSuchElementException e) {
return null;
}
}
}
public String getError() {
try {
@ -246,7 +253,7 @@ public class LoginPage extends LanguageComboboxAwarePage {
assertCurrent();
}
public void open(String realm){
public void open(String realm) {
oauth.realm(realm);
oauth.openLoginForm();
assertCurrent(realm);

View file

@ -148,11 +148,11 @@ public class RequiredActionUpdateProfileWithUserProfileTest extends AbstractTest
//assert field names
// i18n replaced
Assert.assertEquals("First name", updateProfilePage.getLabelForField("firstName"));
Assert.assertEquals("First name *", updateProfilePage.getLabelForField("firstName"));
// attribute name used if no display name set
Assert.assertEquals("lastName", updateProfilePage.getLabelForField("lastName"));
// direct value in display name
Assert.assertEquals("Department", updateProfilePage.getLabelForField("department"));
Assert.assertEquals("Department *", updateProfilePage.getLabelForField("department"));
}

View file

@ -465,11 +465,11 @@ public class KcOidcFirstBrokerLoginTest extends AbstractFirstBrokerLoginTest {
//assert field names
// i18n replaced
org.junit.Assert.assertEquals("First name", updateAccountInformationPage.getLabelForField("firstName"));
org.junit.Assert.assertEquals("First name *", updateAccountInformationPage.getLabelForField("firstName"));
// attribute name used if no display name set
org.junit.Assert.assertEquals("lastName", updateAccountInformationPage.getLabelForField("lastName"));
// direct value in display name
org.junit.Assert.assertEquals("Department", updateAccountInformationPage.getLabelForField("department"));
org.junit.Assert.assertEquals("Department *", updateAccountInformationPage.getLabelForField("department"));
}
@Test

View file

@ -265,11 +265,11 @@ public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest {
//assert field names
// i18n replaced
Assert.assertEquals("First name",registerPage.getLabelForField("firstName"));
Assert.assertEquals("First name *",registerPage.getLabelForField("firstName"));
// attribute name used if no display name set
Assert.assertEquals("lastName",registerPage.getLabelForField("lastName"));
// direct value in display name
Assert.assertEquals("Department",registerPage.getLabelForField("department"));
Assert.assertEquals("Department *",registerPage.getLabelForField("department"));
}
@Test

View file

@ -191,11 +191,11 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest {
//assert field names
// i18n replaced
Assert.assertEquals("First name",verifyProfilePage.getLabelForField("firstName"));
Assert.assertEquals("First name *",verifyProfilePage.getLabelForField("firstName"));
// attribute name used if no display name set
Assert.assertEquals("lastName",verifyProfilePage.getLabelForField("lastName"));
// direct value in display name
Assert.assertEquals("Department",verifyProfilePage.getLabelForField("department"));
Assert.assertEquals("Department *",verifyProfilePage.getLabelForField("department"));
}
@Test

View file

@ -0,0 +1,105 @@
<#macro group name label error="" required=false>
<div class="${properties.kcFormGroupClass}">
<div class="${properties.kcFormGroupLabelClass}">
<label for="${name}" class="pf-v5-c-form__label">
<span class="pf-v5-c-form__label-text">
${label}
</span>
<#if required>
<span class="pf-v5-c-form__label-required" aria-hidden="true">&#42;</span>
</#if>
</label>
</div>
<#nested>
<#if error?has_content>
<div class="${properties.kcFormHelperTextClass}" aria-live="polite">
<div class="${properties.kcInputHelperTextClass}">
<div
class="${properties.kcInputHelperTextItemClass} ${properties.kcError}"
id="input-error-${name}">
<span class="${properties.kcInputErrorMessageClass}">
${error}
</span>
</div>
</div>
</div>
</#if>
</div>
</#macro>
<#macro errorIcon error="">
<#if error?has_content>
<span class="pf-v5-c-form-control__utilities">
<span class="pf-v5-c-form-control__icon pf-m-status">
<i class="fas fa-exclamation-circle" aria-hidden="true"></i>
</span>
</span>
</#if>
</#macro>
<#macro input name label value="" required=false>
<#assign error=kcSanitize(messagesPerField.get(name))?no_esc>
<@field.group name=name label=label error=error required=required>
<span class="${properties.kcInputClass} <#if error?has_content>${properties.kcError}</#if>">
<input id="${name}" name="${name}" value="${value}" type="text" autocomplete="off"
aria-invalid="<#if error?has_content>true</#if>"/>
<@errorIcon error=error/>
</span>
</@field.group>
</#macro>
<#macro password name label value="" required=false forgotPassword=false>
<#assign error=kcSanitize(messagesPerField.get(name))?no_esc>
<@field.group name=name label=label error=error required=required>
<div class="${properties.kcInputGroup}">
<div class="${properties.kcInputGroupItemClass} ${properties.kcFill}">
<span class="${properties.kcInputClass} <#if error?has_content>${properties.kcError}</#if>">
<input id="${name}" name="${name}" value="${value}" type="password" autocomplete="off"
aria-invalid="<#if error?has_content>true</#if>"/>
<@errorIcon error=error/>
</span>
</div>
<div class="${properties.kcInputGroupItemClass}">
<button class="pf-v5-c-button pf-m-control" type="button" aria-label="${msg('showPassword')}"
aria-controls="${name}" data-password-toggle
data-icon-show="fa-eye fas" data-icon-hide="fa-eye-slash fas"
data-label-show="${msg('showPassword')}" data-label-hide="${msg('hidePassword')}">
<i class="fa-eye fas" aria-hidden="true"></i>
</button>
</div>
</div>
<#if forgotPassword>
<div class="${properties.kcFormHelperTextClass}" aria-live="polite">
<div class="${properties.kcInputHelperTextClass}">
<div class="${properties.kcInputHelperTextItemClass}">
<span class="${properties.kcInputHelperTextItemTextClass}">
<a tabindex="3" href="${url.loginResetCredentialsUrl}">${msg("doForgotPassword")}</a>
</span>
</div>
</div>
</div>
</#if>
</@field.group>
</#macro>
<#macro checkbox name label value=false required=false>
<div class="${properties.kcCheckboxClass}">
<label for="${name}" class="${properties.kcCheckboxClass}">
<input
class="${properties.kcCheckboxInputClass}"
type="checkbox"
id="${name}"
name="${name}"
<#if value>checked</#if>
/>
<span class="${properties.kcCheckboxLabelClass}">${label}</span>
<#if required>
<span class="${properties.kcCheckboxLabelRequiredClass}" aria-hidden="true">&#42;</span>
</#if>
</label>
</div>
</#macro>

View file

@ -1,4 +1,5 @@
<#import "template.ftl" as layout>
<#import "field.ftl" as field>
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username','password') displayInfo=realm.password && realm.registrationAllowed && !registrationDisabled??; section>
<#if section = "header">
${msg("loginAccountTitle")}
@ -6,92 +7,25 @@
<div id="kc-form">
<div id="kc-form-wrapper">
<#if realm.password>
<form id="kc-form-login" class="${properties.kcFormClass!} onsubmit="login.disabled = true; return true;" action="${url.loginAction}" method="post" novalidate="novalidate">
<form id="kc-form-login" class="pf-v5-c-form" onsubmit="login.disabled = true; return true;" action="${url.loginAction}" method="post" novalidate="novalidate">
<#if !usernameHidden??>
<div class="${properties.kcFormGroupClass!}">
<label for="username" class="${properties.kcLabelClass!}">
<span class="pf-v5-c-form__label-text">
<#assign label>
<#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if>
</span>
</label>
<span class="${properties.kcInputClass!} ${messagesPerField.existsError('username','password')?then('pf-m-error', '')}">
<input tabindex="1" id="username" name="username" value="${(login.username!'')}" type="text" autofocus autocomplete="off"
aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>"
dir="ltr"
/>
<#if messagesPerField.existsError('username','password')>
<span class="pf-v5-c-form-control__utilities">
<span class="pf-v5-c-form-control__icon pf-m-status">
<i class="fas fa-exclamation-circle" aria-hidden="true"></i>
</span>
</span>
</#if>
</span>
<#if messagesPerField.existsError('username','password')>
<span id="input-error" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc}
</span>
</#assign>
<@field.input name="username" label=label value="${(login.username!'')}" />
</#if>
</div>
</#if>
<@field.password name="password" label=msg("password") forgotPassword=realm.resetPasswordAllowed/>
<div class="${properties.kcFormGroupClass!}">
<label for="password" class="${properties.kcLabelClass!}">
<span class="pf-v5-c-form__label-text">${msg("password")}</span>
</label>
<div class="${properties.kcInputGroup!}" dir="ltr">
<span class="${properties.kcInputClass!}">
<input tabindex="2" id="password" name="password" type="password" autocomplete="off"
aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>"
/>
</span>
<button class="${properties.kcFormPasswordVisibilityButtonClass!}" type="button" aria-label="${msg('showPassword')}"
aria-controls="password" 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 usernameHidden?? && messagesPerField.existsError('username','password')>
<span id="input-error" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc}
</span>
</#if>
</div>
<div class="${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}">
<div id="kc-form-options">
<div class="pf-v5-c-form__group">
<#if realm.rememberMe && !usernameHidden??>
<div class="checkbox">
<label>
<span class="pf-v5-c-form__label-text">
<#if login.rememberMe??>
<input tabindex="3" id="rememberMe" name="rememberMe" type="checkbox" checked> ${msg("rememberMe")}
<#else>
<input tabindex="3" id="rememberMe" name="rememberMe" type="checkbox"> ${msg("rememberMe")}
</#if>
</span>
</label>
</div>
</#if>
</div>
<div class="${properties.kcFormOptionsWrapperClass!}">
<#if realm.resetPasswordAllowed>
<span><a tabindex="5" href="${url.loginResetCredentialsUrl}">${msg("doForgotPassword")}</a></span>
<@field.checkbox name="rememberMe" label=msg("rememberMe") value=login.rememberMe?? />
</#if>
</div>
</div>
<div id="kc-form-buttons" class="${properties.kcFormGroupClass!}">
<div id="kc-form-buttons" class="pf-v5-c-form__group">
<input type="hidden" id="id-hidden-input" name="credentialId" <#if auth.selectedCredential?has_content>value="${auth.selectedCredential}"</#if>/>
<input tabindex="4" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" name="login" id="kc-login" type="submit" value="${msg("doLogIn")}"/>
<input tabindex="4" class="pf-v5-c-button pf-m-primary pf-m-block" name="login" id="kc-login" type="submit" value="${msg("doLogIn")}"/>
</div>
</form>
</#if>

View file

@ -1,4 +1,5 @@
<#import "template.ftl" as layout>
<#import "field.ftl" as field>
<#import "user-profile-commons.ftl" as userProfileCommons>
<#import "register-commons.ftl" as registerCommons>
<@layout.registrationLayout displayMessage=messagesPerField.exists('global') displayRequiredFields=true; section>
@ -15,65 +16,8 @@
<#if callback = "afterField">
<#-- render password fields just under the username or email (if used as username) -->
<#if passwordRequired?? && (attribute.name == 'username' || (attribute.name == 'email' && realm.registrationEmailAsUsername))>
<div class="${properties.kcFormGroupClass!}">
<label for="password" class="${properties.kcLabelClass!}">
<span class="pf-v5-c-form__label-text">
${msg("password")}
<span class="pf-v5-c-form__label-required" aria-hidden="true">&#42;</span>
</span>
</label>
<span class="${properties.kcInputGroup!}">
<span class="${properties.kcInputClass!}" dir="ltr">
<input type="password" id="password" name="password"
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" 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>
</span>
<#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!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="password-confirm" class="${properties.kcLabelClass!}">
<span class="pf-v5-c-form__label-text">
${msg("passwordConfirm")}
<span class="pf-v5-c-form__label-required" aria-hidden="true">&#42;</span>
</span>
</label>
</div>
<div class="${properties.kcInputGroup!}" dir="ltr">
<span class="${properties.kcInputClass!}">
<input type="password" id="password-confirm"
name="password-confirm"
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>
<@field.password name="password" label=msg("password") />
<@field.password name="password-confirm" label=msg("passwordConfirm") />
</#if>
</#if>
</@userProfileCommons.userProfileFormFields>

View file

@ -1,7 +1,6 @@
<#import "footer.ftl" as loginFooter>
<#macro registrationLayout bodyClass="" displayInfo=false displayMessage=true displayRequiredFields=false>
<!DOCTYPE html>
<html class="${properties.kcHtmlClass!}"<#if realm.internationalizationEnabled> lang="${locale.currentLanguageTag}" dir="${(locale.rtl)?then('rtl','ltr')}"</#if>>
<html class="${properties.kcHtmlClass!}"<#if realm.internationalizationEnabled> lang="${locale.currentLanguageTag}"</#if>>
<head>
<meta charset="utf-8">
@ -60,7 +59,7 @@
class="pf-v5-c-brand">${kcSanitize(msg("loginTitleHtml",(realm.displayNameHtml!'')))?no_esc}</div>
</header>
<main class="pf-v5-c-login__main">
<header class="pf-v5-c-login__main-header">
<div class="pf-v5-c-login__main-header">
<h1 class="pf-v5-c-title pf-m-3xl" id="kc-page-title"><#nested "header"></h1>
<#if realm.internationalizationEnabled && locale.supported?size gt 1>
<div class="pf-v5-c-login__main-header-utilities">
@ -100,7 +99,7 @@
</div>
</div>
</#if>
</header>
</div>
<div class="pf-v5-c-login__main-body">
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
<#if displayRequiredFields>
@ -180,8 +179,6 @@
<footer class="pf-v5-c-login__main-footer">
<#nested "socialProviders">
</footer>
<@loginFooter.content/>
</main>
</div>
</div>

View file

@ -5,9 +5,22 @@ styles=css/styles.css
stylesCommon=vendor/patternfly-v5/patternfly.min.css vendor/patternfly-v5/patternfly-addons.css
kcFormGroupClass=pf-v5-c-form__group
kcFormGroupLabelClass=pf-v5-c-form__group-label
kcLabelClass=pf-v5-c-form__label
kcInputClass=pf-v5-c-form-control
kcInputGroup=pf-v5-c-input-group
kcFormHelperTextClass=pf-v5-c-form__helper-text
kcInputHelperTextClass=pf-v5-c-helper-text
kcInputHelperTextItemClass=pf-v5-c-helper-text__item
kcInputHelperTextItemTextClass=pf-v5-c-helper-text__item-text
kcInputGroupItemClass=pf-v5-c-input-group__item
kcFill=pf-m-fill
kcError=pf-m-error
kcCheckboxClass=pf-v5-c-check
kcCheckboxInputClass=pf-v5-c-check__input
kcCheckboxLabelClass=pf-v5-c-check__label
kcCheckboxLabelRequiredClass=pf-v5-c-check__label-required
kcInputErrorMessageClass=pf-v5-c-helper-text__item pf-m-error pf-v5-c-form__label-required kc-feedback-text
kcAlertClass=pf-v5-c-alert pf-m-inline

View file

@ -1,3 +1,4 @@
<#import "field.ftl" as field>
<#macro userProfileFormFields>
<#assign currentGroup="">
@ -35,26 +36,18 @@
</#if>
<#nested "beforeField" attribute>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="${attribute.name}" class="${properties.kcLabelClass!}">${advancedMsg(attribute.displayName!'')}</label>
<#if attribute.required>*</#if>
</div>
<@field.group name=attribute.name label=advancedMsg(attribute.displayName!'') error=kcSanitize(messagesPerField.get('${attribute.name}'))?no_esc required=attribute.required>
<div class="${properties.kcInputWrapperClass!}">
<#if attribute.annotations.inputHelperTextBefore??>
<div class="${properties.kcInputHelperTextBeforeClass!}" id="form-help-text-before-${attribute.name}" aria-live="polite">${kcSanitize(advancedMsg(attribute.annotations.inputHelperTextBefore))?no_esc}</div>
</#if>
<@inputFieldByType attribute=attribute/>
<#if messagesPerField.existsError('${attribute.name}')>
<span id="input-error-${attribute.name}" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('${attribute.name}'))?no_esc}
</span>
</#if>
<#if attribute.annotations.inputHelperTextAfter??>
<div class="${properties.kcInputHelperTextAfterClass!}" id="form-help-text-after-${attribute.name}" aria-live="polite">${kcSanitize(advancedMsg(attribute.annotations.inputHelperTextAfter))?no_esc}</div>
</#if>
</div>
</div>
</@field.group>
<#nested "afterField" attribute>
</#list>
@ -88,6 +81,7 @@
</#macro>
<#macro inputTag attribute value>
<span class="${properties.kcInputClass} <#if error?has_content>${properties.kcError}</#if>">
<input type="<@inputTagType attribute=attribute/>" id="${attribute.name}" name="${attribute.name}" value="${(value!'')}" class="${properties.kcInputClass!}"
aria-invalid="<#if messagesPerField.existsError('${attribute.name}')>true</#if>"
<#if attribute.readOnly>disabled</#if>
@ -105,6 +99,7 @@
data-${key}="${value}"
</#list>
/>
</span>
</#macro>
<#macro inputTagType attribute>