Login: Added custom validation for missing password and user/email on the login form (#20233)

* Added custom validation for missing password and user/email

* Changed some of the logic behind custom validation, so it now uses aria-errormessage

* fix: imports from src folder instead

* build(deps-dev): bump vite to 7.2.0

* formatting

* fix: moves the form into the login.page.element.ts component to better control submission

* fix: creates elements globally

* fix: adds id back to form

* fix: no need to store references to all form elements

* fix: errormessage should show with password field in a span as well

* fix: checks validity of form

* fix: constructs form in auth.element.ts anyway and append localization to validation and add oninput and onblur

* chore: fixes import paths

* fix: fixes special case where ?status was not reset

* fix: changes wording in english

* fix: removes duplicate en-us keys

* feat: adds ariaLive and role attributes

* fix: always clears the text

* fix: username required validation should switch between username and email

* package-lock.json updated on (re)install

* Renamed SVG eye icon filenames

to be conventional and kebab-cased.

---------

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Co-authored-by: leekelleher <leekelleher@gmail.com>
This commit is contained in:
Mathias Helsengren
2025-11-10 11:21:33 +01:00
committed by GitHub
parent bce85e1e88
commit 73fd52aeea
11 changed files with 580 additions and 423 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +1,28 @@
{
"name": "login",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"watch": "tsc && vite build --watch",
"preview": "vite preview",
"generate:server-api": "openapi-ts"
},
"engines": {
"node": ">=22",
"npm": ">=10.9"
},
"devDependencies": {
"@hey-api/openapi-ts": "^0.85.0",
"@umbraco-cms/backoffice": "^16.2.0",
"msw": "^2.11.3",
"typescript": "^5.9.3",
"vite": "^7.1.11"
},
"msw": {
"workerDirectory": [
"public"
]
}
"name": "login",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"watch": "tsc && vite build --watch",
"preview": "vite preview",
"generate:server-api": "openapi-ts"
},
"engines": {
"node": ">=22",
"npm": ">=10.9"
},
"devDependencies": {
"@hey-api/openapi-ts": "^0.85.0",
"@umbraco-cms/backoffice": "^16.2.0",
"msw": "^2.11.3",
"typescript": "^5.9.3",
"vite": "^7.2.0"
},
"msw": {
"workerDirectory": [
"public"
]
}
}

View File

Before

Width:  |  Height:  |  Size: 452 B

After

Width:  |  Height:  |  Size: 452 B

View File

Before

Width:  |  Height:  |  Size: 273 B

After

Width:  |  Height:  |  Size: 273 B

View File

@@ -1,4 +1,19 @@
#umb-login-form #username-input {
.errormessage {
color: var(--uui-color-invalid-standalone);
display: none;
margin-top: var(--uui-size-1);
}
.errormessage.active {
display: block;
}
uui-form-layout-item {
margin-top: var(--uui-size-space-4);
margin-bottom: var(--uui-size-space-4);
}
#username-input {
width: 100%;
height: var(--input-height);
box-sizing: border-box;
@@ -9,21 +24,16 @@
padding: var(--uui-size-1, 3px) var(--uui-size-space-4, 9px);
}
#umb-login-form uui-form-layout-item {
margin-top: var(--uui-size-space-4);
margin-bottom: var(--uui-size-space-4);
}
#umb-login-form #username-input:focus-within {
#username-input:focus-within {
border-color: var(--uui-input-border-color-focus, var(--uui-color-border-emphasis, #a1a1a1));
outline: calc(2px * var(--uui-show-focus-outline, 1)) solid var(--uui-color-focus);
}
#umb-login-form #username-input:hover:not(:focus-within) {
#username-input:hover:not(:focus-within) {
border-color: var(--uui-input-border-color-hover, var(--uui-color-border-standalone, #c2c2c2));
}
#umb-login-form #password-input-span button {
#password-show-toggle {
color: var(--uui-color-default-standalone);
display: inline-flex;
justify-content: center;
@@ -39,12 +49,12 @@
transition-timing-function: linear;
}
#umb-login-form #password-input-span button:hover {
#password-show-toggle:hover {
color: var(--uui-color-default-emphasis);
cursor: pointer;
}
#umb-login-form #password-input-span {
#password-input-span {
display: inline-flex;
width: 100%;
align-items: center;
@@ -60,7 +70,7 @@
padding: var(--uui-size-1, 3px) var(--uui-size-space-4, 9px);
}
#umb-login-form #password-input-span input {
#password-input {
flex-grow: 1;
align-self: stretch;
min-width: 0;
@@ -70,15 +80,15 @@
outline-style: none;
}
#umb-login-form #password-input-span:focus-within {
#password-input-span:focus-within {
border-color: var(--uui-input-border-color-focus, var(--uui-color-border-emphasis, #a1a1a1));
outline: calc(2px * var(--uui-show-focus-outline, 1)) solid var(--uui-color-focus);
}
#umb-login-form #password-input-span:hover:not(:focus-within) {
#password-input-span:hover:not(:focus-within) {
border-color: var(--uui-input-border-color-hover, var(--uui-color-border-standalone, #c2c2c2));
}
#umb-login-form input::-ms-reveal {
#password-input::-ms-reveal {
display: none;
}

View File

@@ -3,15 +3,15 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { InputType, UUIFormLayoutItemElement } from '@umbraco-cms/backoffice/external/uui';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UMB_AUTH_CONTEXT, UmbAuthContext } from './contexts';
import { UmbSlimBackofficeController } from './controllers';
import { UMB_AUTH_CONTEXT, UmbAuthContext } from './contexts/index.js';
import { UmbSlimBackofficeController } from './controllers/index.js';
// We import the authStyles here so that we can inline it in the shadow DOM that is created outside of the UmbAuthElement.
import authStyles from './auth-styles.css?inline';
// Import the SVG files
import openEyeSVG from '../public/openEye.svg?raw';
import closedEyeSVG from '../public/closedEye.svg?raw';
import svgEyeOpen from './assets/eye-open.svg?raw';
import svgEyeClosed from './assets/eye-closed.svg?raw';
// Import the main bundle
import { extensions } from './umbraco-package.js';
@@ -21,6 +21,7 @@ const createInput = (opts: {
type: InputType;
name: string;
autocomplete: AutoFill;
errorId: string;
inputmode: string;
autofocus?: boolean;
}) => {
@@ -31,7 +32,10 @@ const createInput = (opts: {
input.id = opts.id;
input.required = true;
input.inputMode = opts.inputmode;
input.setAttribute('aria-errormessage', opts.errorId);
input.autofocus = opts.autofocus || false;
input.className = 'input';
return input;
};
@@ -46,6 +50,14 @@ const createLabel = (opts: { forId: string; localizeAlias: string; localizeFallb
return label;
};
const createValidationMessage = (errorId: string) => {
const validationElement = document.createElement('div');
validationElement.className = 'errormessage';
validationElement.id = errorId;
validationElement.role = 'alert';
return validationElement;
};
const createShowPasswordToggleButton = (opts: {
id: string;
name: string;
@@ -58,7 +70,7 @@ const createShowPasswordToggleButton = (opts: {
button.name = opts.name;
button.type = 'button';
button.innerHTML = openEyeSVG;
button.innerHTML = svgEyeOpen;
button.onclick = () => {
const passwordInput = document.getElementById('password-input') as HTMLInputElement;
@@ -66,11 +78,11 @@ const createShowPasswordToggleButton = (opts: {
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
button.ariaLabel = opts.ariaLabelHidePassword;
button.innerHTML = closedEyeSVG;
button.innerHTML = svgEyeClosed;
} else {
passwordInput.type = 'password';
button.ariaLabel = opts.ariaLabelShowPassword;
button.innerHTML = openEyeSVG;
button.innerHTML = svgEyeOpen;
}
passwordInput.focus();
@@ -87,44 +99,67 @@ const createShowPasswordToggleItem = (button: HTMLButtonElement) => {
return span;
};
const createFormLayoutItem = (label: HTMLLabelElement, input: HTMLInputElement) => {
const createFormLayoutItem = (label: HTMLLabelElement, input: HTMLInputElement, localizationKey: string) => {
const formLayoutItem = document.createElement('uui-form-layout-item') as UUIFormLayoutItemElement;
const errorId = input.getAttribute('aria-errormessage') || input.id + '-error';
formLayoutItem.appendChild(label);
formLayoutItem.appendChild(input);
const validationMessage = createValidationMessage(errorId);
formLayoutItem.appendChild(validationMessage);
// Bind validation
input.oninput = () => validateInput(input, validationMessage, localizationKey);
input.onblur = () => validateInput(input, validationMessage, localizationKey);
return formLayoutItem;
};
const createFormLayoutPasswordItem = (
label: HTMLLabelElement,
input: HTMLInputElement,
showPasswordToggle: HTMLSpanElement
showPasswordToggle: HTMLSpanElement,
requiredMessageKey: string
) => {
const formLayoutItem = document.createElement('uui-form-layout-item') as UUIFormLayoutItemElement;
const errorId = input.getAttribute('aria-errormessage') || input.id + '-error';
formLayoutItem.appendChild(label);
const span = document.createElement('span');
span.id = 'password-input-span';
span.appendChild(input);
span.appendChild(showPasswordToggle);
formLayoutItem.appendChild(span);
const validationMessage = createValidationMessage(errorId);
formLayoutItem.appendChild(validationMessage);
// Bind validation
input.oninput = () => validateInput(input, validationMessage, requiredMessageKey);
input.onblur = () => validateInput(input, validationMessage, requiredMessageKey);
return formLayoutItem;
};
const createForm = (elements: HTMLElement[]) => {
const styles = document.createElement('style');
styles.innerHTML = authStyles;
const form = document.createElement('form');
form.id = 'umb-login-form';
form.name = 'login-form';
form.spellcheck = false;
const validateInput = (input: HTMLInputElement, validationElement: HTMLElement, requiredMessage = '') => {
validationElement.innerHTML = '';
if (input.validity.valid) {
input.removeAttribute('aria-invalid');
validationElement.classList.remove('active');
validationElement.ariaLive = 'off';
} else {
input.setAttribute('aria-invalid', 'true');
elements.push(styles);
elements.forEach((element) => form.appendChild(element));
const localizeElement = document.createElement('umb-localize');
localizeElement.innerHTML = input.validationMessage;
localizeElement.key = requiredMessage;
validationElement.appendChild(localizeElement);
return form;
validationElement.classList.add('active');
validationElement.ariaLive = 'assertive';
}
};
@customElement('umb-auth')
@@ -168,16 +203,6 @@ export default class UmbAuthElement extends UmbLitElement {
*/
protected flow?: 'mfa' | 'reset-password' | 'invite-user';
_form?: HTMLFormElement;
_usernameLayoutItem?: UUIFormLayoutItemElement;
_passwordLayoutItem?: UUIFormLayoutItemElement;
_usernameInput?: HTMLInputElement;
_passwordInput?: HTMLInputElement;
_usernameLabel?: HTMLLabelElement;
_passwordLabel?: HTMLLabelElement;
_passwordShowPasswordToggleItem?: HTMLSpanElement;
_passwordShowPasswordToggleButton?: HTMLButtonElement;
#authContext = new UmbAuthContext(this, UMB_AUTH_CONTEXT);
constructor() {
@@ -186,6 +211,16 @@ export default class UmbAuthElement extends UmbLitElement {
(this as unknown as EventTarget).addEventListener('umb-login-flow', (e) => {
if (e instanceof CustomEvent) {
this.flow = e.detail.flow || undefined;
if (typeof e.detail.status !== 'undefined') {
const searchParams = new URLSearchParams(window.location.search);
if (e.detail.status === null) {
searchParams.delete('status');
} else {
searchParams.set('status', e.detail.status);
}
const newRelativePathQuery = window.location.pathname + '?' + searchParams.toString();
window.history.pushState(null, '', newRelativePathQuery);
}
}
this.requestUpdate();
});
@@ -229,18 +264,6 @@ export default class UmbAuthElement extends UmbLitElement {
});
}
disconnectedCallback() {
super.disconnectedCallback();
this._usernameLayoutItem?.remove();
this._passwordLayoutItem?.remove();
this._usernameLabel?.remove();
this._usernameInput?.remove();
this._passwordLabel?.remove();
this._passwordInput?.remove();
this._passwordShowPasswordToggleItem?.remove();
this._passwordShowPasswordToggleButton?.remove();
}
/**
* Creates the login form and adds it to the DOM in the default slot.
* This is done to avoid having to deal with the shadow DOM, which is not supported in Google Chrome for autocomplete/autofill.
@@ -249,48 +272,65 @@ export default class UmbAuthElement extends UmbLitElement {
* @private
*/
#initializeForm() {
this._usernameInput = createInput({
const usernameInput = createInput({
id: 'username-input',
type: 'text',
name: 'username',
autocomplete: 'username',
errorId: 'username-input-error',
inputmode: this.usernameIsEmail ? 'email' : '',
autofocus: true,
});
this._passwordInput = createInput({
const passwordInput = createInput({
id: 'password-input',
type: 'password',
name: 'password',
autocomplete: 'current-password',
errorId: 'password-input-error',
inputmode: '',
});
this._passwordShowPasswordToggleButton = createShowPasswordToggleButton({
const passwordShowPasswordToggleButton = createShowPasswordToggleButton({
id: 'password-show-toggle',
name: 'password-show-toggle',
ariaLabelShowPassword: this.localize.term('auth_showPassword'),
ariaLabelHidePassword: this.localize.term('auth_hidePassword'),
});
this._passwordShowPasswordToggleItem = createShowPasswordToggleItem(this._passwordShowPasswordToggleButton);
this._usernameLabel = createLabel({
const passwordShowPasswordToggleItem = createShowPasswordToggleItem(passwordShowPasswordToggleButton);
const usernameLabel = createLabel({
forId: 'username-input',
localizeAlias: this.usernameIsEmail ? 'auth_email' : 'auth_username',
localizeFallback: this.usernameIsEmail ? 'Email' : 'Username',
});
this._passwordLabel = createLabel({
const passwordLabel = createLabel({
forId: 'password-input',
localizeAlias: 'auth_password',
localizeFallback: 'Password',
});
this._usernameLayoutItem = createFormLayoutItem(this._usernameLabel, this._usernameInput);
this._passwordLayoutItem = createFormLayoutPasswordItem(
this._passwordLabel,
this._passwordInput,
this._passwordShowPasswordToggleItem
const usernameLayoutItem = createFormLayoutItem(
usernameLabel,
usernameInput,
this.usernameIsEmail ? 'auth_requiredEmailValidationMessage' : 'auth_requiredUsernameValidationMessage'
);
const passwordLayoutItem = createFormLayoutPasswordItem(
passwordLabel,
passwordInput,
passwordShowPasswordToggleItem,
'auth_requiredPasswordValidationMessage'
);
const style = document.createElement('style');
style.innerHTML = authStyles;
document.head.appendChild(style);
this._form = createForm([this._usernameLayoutItem, this._passwordLayoutItem]);
const form = document.createElement('form');
form.id = 'umb-login-form';
form.name = 'login-form';
form.spellcheck = false;
form.setAttribute('novalidate', '');
this.insertAdjacentElement('beforeend', this._form);
form.appendChild(usernameLayoutItem);
form.appendChild(passwordLayoutItem);
this.insertAdjacentElement('beforeend', form);
}
render() {
@@ -347,12 +387,11 @@ export default class UmbAuthElement extends UmbLitElement {
return html` <umb-invite-page></umb-invite-page>`;
default:
return html` <umb-login-page
?allow-password-reset=${this.allowPasswordReset}
?username-is-email=${this.usernameIsEmail}>
<slot></slot>
<slot name="subheadline" slot="subheadline"></slot>
</umb-login-page>`;
return html`
<umb-login-page ?allow-password-reset=${this.allowPasswordReset} ?username-is-email=${this.usernameIsEmail}>
<slot></slot>
</umb-login-page>
`;
}
}
}

View File

@@ -17,7 +17,7 @@ export default class UmbBackToLoginButtonElement extends UmbLitElement {
}
#handleClick() {
this.dispatchEvent(new CustomEvent('umb-login-flow', { composed: true, detail: { flow: 'login' } }));
this.dispatchEvent(new CustomEvent('umb-login-flow', { composed: true, detail: { flow: 'login', status: null } }));
}
static styles: CSSResultGroup = [
@@ -39,7 +39,7 @@ export default class UmbBackToLoginButtonElement extends UmbLitElement {
display: inline-flex;
line-height: 1;
font-size: 14px;
font-family: var(--uui-font-family),sans-serif;
font-family: var(--uui-font-family), sans-serif;
}
button svg {
width: 1rem;

View File

@@ -1,254 +1,263 @@
import type { UUIButtonState } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { css, type CSSResultGroup, html, nothing, when, customElement, property, queryAssignedElements, state } from '@umbraco-cms/backoffice/external/lit';
import {
css,
html,
nothing,
when,
customElement,
property,
queryAssignedElements,
state,
} from '@umbraco-cms/backoffice/external/lit';
import { UMB_AUTH_CONTEXT } from '../../contexts';
import { UMB_AUTH_CONTEXT } from '../../contexts/index.js';
@customElement('umb-login-page')
export default class UmbLoginPageElement extends UmbLitElement {
@property({type: Boolean, attribute: 'username-is-email'})
usernameIsEmail = false;
@property({ type: Boolean, attribute: 'username-is-email' })
usernameIsEmail = false;
@queryAssignedElements({flatten: true})
protected slottedElements?: HTMLFormElement[];
@queryAssignedElements({ flatten: true })
protected slottedElements?: HTMLFormElement[];
@property({type: Boolean, attribute: 'allow-password-reset'})
allowPasswordReset = false;
@property({ type: Boolean, attribute: 'allow-password-reset' })
allowPasswordReset = false;
@state()
private _loginState?: UUIButtonState;
@state()
private _loginState?: UUIButtonState;
@state()
private _loginError = '';
@state()
private _loginError = '';
@state()
supportPersistLogin = false;
@state()
supportPersistLogin = false;
#formElement?: HTMLFormElement;
#formElement?: HTMLFormElement;
#authContext?: typeof UMB_AUTH_CONTEXT.TYPE;
#authContext?: typeof UMB_AUTH_CONTEXT.TYPE;
constructor() {
super();
constructor() {
super();
this.consumeContext(UMB_AUTH_CONTEXT, (authContext) => {
this.#authContext = authContext;
this.supportPersistLogin = authContext?.supportsPersistLogin ?? false;
});
}
this.consumeContext(UMB_AUTH_CONTEXT, (authContext) => {
this.#authContext = authContext;
this.supportPersistLogin = authContext?.supportsPersistLogin ?? false;
});
}
async #onSlotChanged() {
this.#formElement = this.slottedElements?.find((el) => el.id === 'umb-login-form');
async #onSlotChanged() {
this.#formElement = this.slottedElements?.find((el) => el.id === 'umb-login-form');
if (!this.#formElement) return;
if (!this.#formElement) return;
// We need to listen for the enter key to submit the form, because the uui-button does not support the native input fields submit event
this.#formElement.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.#onSubmitClick();
}
});
this.#formElement.onsubmit = this.#handleSubmit;
}
this.#formElement.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.#onSubmitClick();
}
});
#handleSubmit = async (e: SubmitEvent) => {
e.preventDefault();
this.#formElement.onsubmit = this.#handleSubmit;
}
if (!this.#authContext) return;
#handleSubmit = async (e: SubmitEvent) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
if (!form) return;
this._loginError = '';
this._loginState = undefined;
if (!this.#authContext) return;
const formData = new FormData(form);
const form = e.target as HTMLFormElement;
if (!form) return;
const username = formData.get('username') as string;
const password = formData.get('password') as string;
const persist = formData.has('persist');
if (!form?.checkValidity()) {
return;
}
if (!username || !password) {
this._loginError = this.localize.term('auth_userFailedLogin');
this._loginState = 'failed';
return;
}
const formData = new FormData(form);
this._loginState = 'waiting';
const username = formData.get('username') as string;
const password = formData.get('password') as string;
const persist = formData.has('persist');
const response = await this.#authContext.login({
username,
password,
persist,
});
if (!username || !password) {
return;
}
this._loginError = response.error || '';
this._loginState = response.error ? 'failed' : 'success';
this._loginState = 'waiting';
// Check for 402 status code indicating that MFA is required
if (response.status === 402) {
this.#authContext.isMfaEnabled = true;
if (response.twoFactorView) {
this.#authContext.twoFactorView = response.twoFactorView;
}
if (response.twoFactorProviders) {
this.#authContext.mfaProviders = response.twoFactorProviders;
}
const response = await this.#authContext.login({
username,
password,
persist,
});
this.dispatchEvent(new CustomEvent('umb-login-flow', {composed: true, detail: {flow: 'mfa'}}));
return;
}
this._loginError = response.error || '';
this._loginState = response.error ? 'failed' : 'success';
if (response.error) {
return;
}
// Check for 402 status code indicating that MFA is required
if (response.status === 402) {
this.#authContext.isMfaEnabled = true;
if (response.twoFactorView) {
this.#authContext.twoFactorView = response.twoFactorView;
}
if (response.twoFactorProviders) {
this.#authContext.mfaProviders = response.twoFactorProviders;
}
const returnPath = this.#authContext.returnPath;
this.dispatchEvent(new CustomEvent('umb-login-flow', { composed: true, detail: { flow: 'mfa' } }));
return;
}
if (returnPath) {
location.href = returnPath;
}
};
if (response.error) {
return;
}
get #greetingLocalizationKey() {
return [
'auth_greeting0',
'auth_greeting1',
'auth_greeting2',
'auth_greeting3',
'auth_greeting4',
'auth_greeting5',
'auth_greeting6',
][new Date().getDay()];
}
const returnPath = this.#authContext.returnPath;
#onSubmitClick = () => {
this.#formElement?.requestSubmit();
};
if (returnPath) {
location.href = returnPath;
}
};
render() {
return html`
<header id="header">
<h1 id="greeting">
<umb-localize .key=${this.#greetingLocalizationKey}></umb-localize>
</h1>
<slot name="subheadline"></slot>
</header>
<slot @slotchange=${this.#onSlotChanged}></slot>
<div id="secondary-actions">
${when(
this.supportPersistLogin,
() => html`
<uui-form-layout-item>
<uui-checkbox
name="persist"
.label=${this.localize.term('auth_rememberMe')}>
<umb-localize key="auth_rememberMe">Remember me</umb-localize>
</uui-checkbox>
</uui-form-layout-item>`
)}
${when(
this.allowPasswordReset,
() =>
html`
<button type="button" id="forgot-password" @click=${this.#handleForgottenPassword}>
<umb-localize key="auth_forgottenPassword">Forgotten password?</umb-localize>
</button>`
)}
</div>
<uui-button
type="submit"
id="umb-login-button"
look="primary"
@click=${this.#onSubmitClick}
.label=${this.localize.term('auth_login')}
color="default"
.state=${this._loginState}></uui-button>
get #greetingLocalizationKey() {
return [
'auth_greeting0',
'auth_greeting1',
'auth_greeting2',
'auth_greeting3',
'auth_greeting4',
'auth_greeting5',
'auth_greeting6',
][new Date().getDay()];
}
${this.#renderErrorMessage()}
`;
}
#onSubmitClick = () => {
this.#formElement?.requestSubmit();
};
#renderErrorMessage() {
if (!this._loginError || this._loginState !== 'failed') return nothing;
render() {
return html`
<header id="header">
<h1 id="greeting">
<umb-localize .key=${this.#greetingLocalizationKey}></umb-localize>
</h1>
<slot name="subheadline"></slot>
</header>
<slot @slotchange=${this.#onSlotChanged}></slot>
<div id="secondary-actions">
${when(
this.supportPersistLogin,
() => html` <uui-form-layout-item>
<uui-checkbox name="persist" .label=${this.localize.term('auth_rememberMe')}>
<umb-localize key="auth_rememberMe">Remember me</umb-localize>
</uui-checkbox>
</uui-form-layout-item>`
)}
${when(
this.allowPasswordReset,
() =>
html` <button type="button" id="forgot-password" @click=${this.#handleForgottenPassword}>
<umb-localize key="auth_forgottenPassword">Forgotten password?</umb-localize>
</button>`
)}
</div>
<uui-button
@click=${this.#onSubmitClick}
type="submit"
id="umb-login-button"
look="primary"
.label=${this.localize.term('auth_login')}
color="default"
.state=${this._loginState}></uui-button>
return html`<span class="text-error text-danger">${this._loginError}</span>`;
}
${this.#renderErrorMessage()}
`;
}
#handleForgottenPassword() {
this.dispatchEvent(new CustomEvent('umb-login-flow', {composed: true, detail: {flow: 'reset'}}));
}
#renderErrorMessage() {
if (!this._loginError || this._loginState !== 'failed') return nothing;
static styles: CSSResultGroup = [
css`
:host {
display: flex;
flex-direction: column;
}
return html`<span class="text-error text-danger">${this._loginError}</span>`;
}
#header {
text-align: center;
display: flex;
flex-direction: column;
gap: var(--uui-size-space-5);
}
#handleForgottenPassword() {
this.dispatchEvent(new CustomEvent('umb-login-flow', { composed: true, detail: { flow: 'reset' } }));
}
#header span {
color: var(--uui-color-text-alt); /* TODO Change to uui color when uui gets a muted text variable */
font-size: 14px;
}
static readonly styles = [
css`
:host {
display: flex;
flex-direction: column;
}
#greeting {
color: var(--uui-color-interactive);
text-align: center;
font-weight: 400;
font-size: var(--header-font-size);
margin: 0 0 var(--uui-size-layout-1);
line-height: 1.2;
}
#header {
text-align: center;
display: flex;
flex-direction: column;
gap: var(--uui-size-space-5);
}
#umb-login-button {
margin-top: var(--uui-size-space-4);
width: 100%;
}
#header span {
color: var(--uui-color-text-alt); /* TODO Change to uui color when uui gets a muted text variable */
font-size: 14px;
}
#forgot-password {
cursor: pointer;
background: none;
border: 0;
height: 1rem;
color: var(--uui-color-text-alt); /* TODO Change to uui color when uui gets a muted text variable */
gap: var(--uui-size-space-1);
align-self: center;
text-decoration: none;
display: inline-flex;
line-height: 1;
font-size: 14px;
font-family: var(--uui-font-family),sans-serif;
margin-left: auto;
margin-bottom: var(--uui-size-space-3);
}
#greeting {
color: var(--uui-color-interactive);
text-align: center;
font-weight: 400;
font-size: var(--header-font-size);
margin: 0 0 var(--uui-size-layout-1);
line-height: 1.2;
}
#forgot-password:hover {
color: var(--uui-color-interactive-emphasis);
}
#umb-login-button {
margin-top: var(--uui-size-space-4);
width: 100%;
}
.text-error {
margin-top: var(--uui-size-space-4);
}
#forgot-password {
cursor: pointer;
background: none;
border: 0;
height: 1rem;
color: var(--uui-color-text-alt); /* TODO Change to uui color when uui gets a muted text variable */
gap: var(--uui-size-space-1);
align-self: center;
text-decoration: none;
display: inline-flex;
line-height: 1;
font-size: 14px;
font-family: var(--uui-font-family), sans-serif;
margin-left: auto;
margin-bottom: var(--uui-size-space-3);
}
.text-danger {
color: var(--uui-color-danger-standalone);
}
#forgot-password:hover {
color: var(--uui-color-interactive-emphasis);
}
#secondary-actions {
display: flex;
align-items: center;
justify-content: space-between;
}
`,
];
.text-error {
margin-top: var(--uui-size-space-4);
}
.text-danger {
color: var(--uui-color-danger-standalone);
}
#secondary-actions {
display: flex;
align-items: center;
justify-content: space-between;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
'umb-login-page': UmbLoginPageElement;
}
interface HTMLElementTagNameMap {
'umb-login-page': UmbLoginPageElement;
}
}

View File

@@ -1,53 +1,61 @@
import type { UmbLocalizationDictionary } from '@umbraco-cms/backoffice/localization-api';
export default {
auth: {
continue: 'Fortsæt',
validate: 'Indsend',
login: 'Log ind',
email: 'E-mail',
username: 'Brugernavn',
password: 'Adgangskode',
submit: 'Indsend',
required: 'Påkrævet',
success: 'Succes',
forgottenPassword: 'Glemt adgangskode?',
forgottenPasswordInstruction: 'En e-mail vil blive sendt til den angivne adresse med et link til at nulstille din adgangskode',
requestPasswordResetConfirmation: 'En e-mail med instruktioner for nulstilling af adgangskoden vil blive sendt til den angivne adresse, hvis det matcher vores optegnelser',
setPasswordConfirmation: 'Din adgangskode er blevet opdateret',
rememberMe: 'Husk mig',
error: 'Fejl',
defaultError: 'Der er opstået en ukendt fejl.',
errorInPasswordFormat: 'Kodeordet skal være på minimum %0% tegn og indeholde mindst %1% alfanumeriske tegn.',
passwordMismatch: 'Adgangskoderne er ikke ens.',
passwordMinLength: 'Adgangskoden skal være mindst {0} tegn lang.',
passwordIsBlank: 'Din nye adgangskode kan ikke være tom.',
userFailedLogin: 'Ups! Vi kunne ikke logge dig ind. Tjek at dit brugernavn og adgangskode er korrekt og prøv igen.',
userLockedOut: 'Din konto er blevet låst. Prøv igen senere.',
receivedErrorFromServer: 'Der skete en fejl på serveren',
resetCodeExpired: 'Det link, du har klikket på, er ugyldigt eller udløbet',
userInviteWelcomeMessage: 'Hej og velkommen til Umbraco! På bare 1 minut vil du være klar til at komme i gang, vi skal bare have dig til at oprette en adgangskode.',
userInviteExpiredMessage: 'Velkommen til Umbraco! Desværre er din invitation udløbet. Kontakt din administrator og bed om at gensende invitationen.',
newPassword: 'Ny adgangskode',
confirmNewPassword: 'Bekræft adgangskode',
greeting0: 'Velkommen',
greeting1: 'Velkommen',
greeting2: 'Velkommen',
greeting3: 'Velkommen',
greeting4: 'Velkommen',
greeting5: 'Velkommen',
greeting6: 'Velkommen',
mfaTitle: 'Sidste skridt!',
mfaCodeInputHelp: 'Indtast venligst bekræftelseskoden',
mfaText: 'Du har aktiveret multi-faktor godkendelse. Du skal nu bekræfte din identitet.',
mfaMultipleText: 'Vælg venligst en godkendelsesmetode',
mfaCodeInput: 'Kode',
mfaInvalidCode: 'Forkert kode indtastet',
signInWith: 'Log ind med {0}',
returnToLogin: 'Tilbage til log ind',
localLoginDisabled: 'Desværre er det ikke muligt at logge ind direkte. Det er blevet deaktiveret af en login-udbyder.',
friendlyGreeting: 'Hej!',
showPassword: 'Vis adgangskode',
hidePassword: 'Skjul adgangskode',
},
auth: {
continue: 'Fortsæt',
validate: 'Indsend',
login: 'Log ind',
email: 'E-mail',
username: 'Brugernavn',
password: 'Adgangskode',
submit: 'Indsend',
required: 'Påkrævet',
success: 'Succes',
forgottenPassword: 'Glemt adgangskode?',
forgottenPasswordInstruction:
'En e-mail vil blive sendt til den angivne adresse med et link til at nulstille din adgangskode',
requestPasswordResetConfirmation:
'En e-mail med instruktioner for nulstilling af adgangskoden vil blive sendt til den angivne adresse, hvis det matcher vores optegnelser',
setPasswordConfirmation: 'Din adgangskode er blevet opdateret',
rememberMe: 'Husk mig',
error: 'Fejl',
defaultError: 'Der er opstået en ukendt fejl.',
errorInPasswordFormat: 'Kodeordet skal være på minimum %0% tegn og indeholde mindst %1% alfanumeriske tegn.',
passwordMismatch: 'Adgangskoderne er ikke ens.',
passwordMinLength: 'Adgangskoden skal være mindst {0} tegn lang.',
passwordIsBlank: 'Din nye adgangskode kan ikke være tom.',
userFailedLogin: 'Ups! Vi kunne ikke logge dig ind. Tjek at dit brugernavn og adgangskode er korrekt og prøv igen.',
userLockedOut: 'Din konto er blevet låst. Prøv igen senere.',
receivedErrorFromServer: 'Der skete en fejl på serveren',
resetCodeExpired: 'Det link, du har klikket på, er ugyldigt eller udløbet',
userInviteWelcomeMessage:
'Hej og velkommen til Umbraco! På bare 1 minut vil du være klar til at komme i gang, vi skal bare have dig til at oprette en adgangskode.',
userInviteExpiredMessage:
'Velkommen til Umbraco! Desværre er din invitation udløbet. Kontakt din administrator og bed om at gensende invitationen.',
newPassword: 'Ny adgangskode',
confirmNewPassword: 'Bekræft adgangskode',
greeting0: 'Velkommen',
greeting1: 'Velkommen',
greeting2: 'Velkommen',
greeting3: 'Velkommen',
greeting4: 'Velkommen',
greeting5: 'Velkommen',
greeting6: 'Velkommen',
mfaTitle: 'Sidste skridt!',
mfaCodeInputHelp: 'Indtast venligst bekræftelseskoden',
mfaText: 'Du har aktiveret multi-faktor godkendelse. Du skal nu bekræfte din identitet.',
mfaMultipleText: 'Vælg venligst en godkendelsesmetode',
mfaCodeInput: 'Kode',
mfaInvalidCode: 'Forkert kode indtastet',
signInWith: 'Log ind med {0}',
returnToLogin: 'Tilbage til log ind',
localLoginDisabled:
'Desværre er det ikke muligt at logge ind direkte. Det er blevet deaktiveret af en login-udbyder.',
friendlyGreeting: 'Hej!',
requiredEmailValidationMessage: 'Udfyld venligst en e-mail',
requiredUsernameValidationMessage: 'Udfyld venligst et brugernavn',
requiredPasswordValidationMessage: 'Udfyld venligst en adgangskode',
showPassword: 'Vis adgangskode',
hidePassword: 'Skjul adgangskode',
},
} satisfies UmbLocalizationDictionary;

View File

@@ -2,54 +2,6 @@ import type { UmbLocalizationDictionary } from '@umbraco-cms/backoffice/localiza
export default {
auth: {
continue: 'Continue',
validate: 'Validate',
login: 'Login',
email: 'E-mail',
username: 'Username',
password: 'Password',
submit: 'Submit',
required: 'Required',
success: 'Success',
forgottenPassword: 'Forgotten password?',
forgottenPasswordInstruction: 'An email will be sent to the address specified with a link to reset your password',
requestPasswordResetConfirmation:
'An email with password reset instructions will be sent to the specified address if it matched our records',
setPasswordConfirmation: 'Your password has been updated',
rememberMe: 'Remember me',
error: 'Error',
defaultError: 'An error occurred while processing your request.',
errorInPasswordFormat:
'The password must be at least {0} characters long and contain at least {1} special characters.',
passwordMismatch: 'The confirmed password does not match the new password!',
passwordMinLength: 'The password must be at least {0} characters long.',
passwordIsBlank: 'The password cannot be blank.',
userFailedLogin: "Oops! We couldn't log you in. Please check your credentials and try again.",
userLockedOut: 'Your account has been locked out. Please try again later.',
receivedErrorFromServer: 'Received an error from the server',
resetCodeExpired: 'The link you have clicked on is invalid or has expired',
userInviteWelcomeMessage:
'Hello there and welcome to Umbraco! In just 1 minute youll be good to go, we just need you to setup a password.',
userInviteExpiredMessage:
'Welcome to Umbraco! Unfortunately your invite has expired. Please contact your administrator and ask them to resend it.',
newPassword: 'New password',
confirmNewPassword: 'Confirm password',
greeting0: 'Welcome',
greeting1: 'Welcome',
greeting2: 'Welcome',
greeting3: 'Welcome',
greeting4: 'Welcome',
greeting5: 'Welcome',
greeting6: 'Welcome',
mfaTitle: 'One last step',
mfaCodeInputHelp: 'Enter the code from your authenticator app',
mfaText: 'You have enabled 2-factor authentication and must verify your identity.',
mfaMultipleText: 'Please choose a 2-factor provider',
mfaCodeInput: 'Verification code',
mfaInvalidCode: 'Invalid code entered',
signInWith: 'Sign in with {0}',
returnToLogin: 'Return to login',
localLoginDisabled: 'Unfortunately, direct login is not possible. It has been disabled by a provider.',
friendlyGreeting: 'Hi there',
},
} satisfies UmbLocalizationDictionary;

View File

@@ -29,7 +29,7 @@ export default {
receivedErrorFromServer: 'Received an error from the server',
resetCodeExpired: 'The link you have clicked on is invalid or has expired',
userInviteWelcomeMessage:
'Hello there and welcome to Umbraco! In just 1 minute youll be good to go, we just need you to setup a password.',
"Hello there and welcome to Umbraco! In just 1 minute you'll be good to go, we just need you to setup a password.",
userInviteExpiredMessage:
'Welcome to Umbraco! Unfortunately your invite has expired. Please contact your administrator and ask them to resend it.',
newPassword: 'New password',
@@ -51,6 +51,9 @@ export default {
returnToLogin: 'Return to login',
localLoginDisabled: 'Unfortunately, direct login is not possible. It has been disabled by a provider.',
friendlyGreeting: 'Hello',
requiredEmailValidationMessage: 'Please fill in an email',
requiredUsernameValidationMessage: 'Please fill in a username',
requiredPasswordValidationMessage: 'Please fill in a password',
showPassword: 'Show password',
hidePassword: 'Hide password',
},