Merge remote-tracking branch 'origin/v13/dev' into release/13.0
This commit is contained in:
34
src/Umbraco.Web.UI.Login/src/auth-styles.css
Normal file
34
src/Umbraco.Web.UI.Login/src/auth-styles.css
Normal file
@@ -0,0 +1,34 @@
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
#umb-login-form umb-login-input {
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
border: 1px solid var(--uui-color-border);
|
||||
border-radius: var(--uui-border-radius);
|
||||
outline: none;
|
||||
background-color: var(--uui-color-surface);
|
||||
}
|
||||
#umb-login-form umb-login-input input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
background: none;
|
||||
outline: none;
|
||||
padding: var(--uui-size-1, 3px) var(--uui-size-space-3, 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 umb-login-input:focus-within {
|
||||
border-color: var(--uui-input-border-color-focus, var(--uui-color-border-emphasis, #a1a1a1));
|
||||
}
|
||||
#umb-login-form umb-login-input:hover:not(:focus-within) {
|
||||
border-color: var(--uui-input-border-color-hover, var(--uui-color-border-standalone, #c2c2c2));
|
||||
}
|
||||
@@ -5,121 +5,247 @@ import { until } from 'lit/directives/until.js';
|
||||
|
||||
import { umbAuthContext } from './context/auth.context.js';
|
||||
import { umbLocalizationContext } from './external/localization/localization-context.js';
|
||||
import { UmbLocalizeElement } from './external/localization/localize.element.js';
|
||||
import type { UmbLoginInputElement } from './components/login-input.element.js';
|
||||
import { InputType, UUIFormLayoutItemElement, UUILabelElement } from '@umbraco-ui/uui';
|
||||
|
||||
import authStyles from './auth-styles.css?inline';
|
||||
|
||||
const createInput = (opts: {id: string, type: InputType, name: string, autocomplete: AutoFill, requiredMessage: string, label: string, inputmode: string}) => {
|
||||
const input = document.createElement('umb-login-input');
|
||||
input.type = opts.type;
|
||||
input.name = opts.name;
|
||||
input.autocomplete = opts.autocomplete;
|
||||
input.id = opts.id;
|
||||
input.required = true;
|
||||
input.requiredMessage = opts.requiredMessage;
|
||||
input.label = opts.label;
|
||||
input.spellcheck = false;
|
||||
input.inputMode = opts.inputmode;
|
||||
|
||||
return input;
|
||||
};
|
||||
|
||||
const createLabel = (opts: {forId: string, localizeAlias: string}) => {
|
||||
const label = document.createElement('uui-label');
|
||||
const umbLocalize = document.createElement('umb-localize') as UmbLocalizeElement;
|
||||
umbLocalize.key = opts.localizeAlias;
|
||||
label.for = opts.forId;
|
||||
label.appendChild(umbLocalize);
|
||||
|
||||
return label;
|
||||
};
|
||||
|
||||
const createFormLayoutItem = (label: UUILabelElement, input: UmbLoginInputElement) => {
|
||||
const formLayoutItem = document.createElement('uui-form-layout-item') as UUIFormLayoutItemElement;
|
||||
formLayoutItem.appendChild(label);
|
||||
formLayoutItem.appendChild(input);
|
||||
|
||||
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';
|
||||
|
||||
elements.push(styles);
|
||||
elements.forEach((element) => form.appendChild(element));
|
||||
|
||||
return form;
|
||||
};
|
||||
|
||||
@customElement('umb-auth')
|
||||
export default class UmbAuthElement extends LitElement {
|
||||
#returnPath = '';
|
||||
#returnPath = '';
|
||||
|
||||
/**
|
||||
* Disables the local login form and only allows external login providers.
|
||||
*
|
||||
* @attr disable-local-login
|
||||
*/
|
||||
@property({ type: Boolean, attribute: 'disable-local-login' })
|
||||
set disableLocalLogin(value: boolean) {
|
||||
umbAuthContext.disableLocalLogin = value;
|
||||
}
|
||||
/**
|
||||
* Disables the local login form and only allows external login providers.
|
||||
*
|
||||
* @attr disable-local-login
|
||||
*/
|
||||
@property({ type: Boolean, attribute: 'disable-local-login' })
|
||||
set disableLocalLogin(value: boolean) {
|
||||
umbAuthContext.disableLocalLogin = value;
|
||||
}
|
||||
|
||||
@property({ type: String, attribute: 'background-image' })
|
||||
backgroundImage?: string;
|
||||
@property({ attribute: 'background-image' })
|
||||
backgroundImage?: string;
|
||||
|
||||
@property({ type: String, attribute: 'logo-image' })
|
||||
logoImage?: string;
|
||||
@property({ attribute: 'logo-image' })
|
||||
logoImage?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: 'username-is-email' })
|
||||
usernameIsEmail = false;
|
||||
@property({ type: Boolean, attribute: 'username-is-email' })
|
||||
usernameIsEmail = false;
|
||||
|
||||
@property({ type: Boolean, attribute: 'allow-password-reset' })
|
||||
allowPasswordReset = false;
|
||||
@property({ type: Boolean, attribute: 'allow-password-reset' })
|
||||
allowPasswordReset = false;
|
||||
|
||||
@property({ type: Boolean, attribute: 'allow-user-invite' })
|
||||
allowUserInvite = false;
|
||||
@property({ type: Boolean, attribute: 'allow-user-invite' })
|
||||
allowUserInvite = false;
|
||||
|
||||
@property({ type: String, attribute: 'return-url' })
|
||||
set returnPath(value: string) {
|
||||
this.#returnPath = value;
|
||||
umbAuthContext.returnPath = this.returnPath;
|
||||
}
|
||||
get returnPath() {
|
||||
// Check if there is a ?redir querystring or else return the returnUrl attribute
|
||||
return new URLSearchParams(window.location.search).get('returnPath') || this.#returnPath;
|
||||
}
|
||||
@property({ type: String, attribute: 'return-url' })
|
||||
set returnPath(value: string) {
|
||||
this.#returnPath = value;
|
||||
umbAuthContext.returnPath = this.returnPath;
|
||||
}
|
||||
get returnPath() {
|
||||
// Check if there is a ?redir querystring or else return the returnUrl attribute
|
||||
return new URLSearchParams(window.location.search).get('returnPath') || this.#returnPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the default flow.
|
||||
*/
|
||||
protected flow?: 'mfa' | 'reset-password' | 'invite-user';
|
||||
/**
|
||||
* Override the default flow.
|
||||
*/
|
||||
protected flow?: 'mfa' | 'reset-password' | 'invite-user';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.classList.add('uui-text');
|
||||
this.classList.add('uui-font');
|
||||
_form?: HTMLFormElement;
|
||||
_usernameLayoutItem?: UUIFormLayoutItemElement;
|
||||
_passwordLayoutItem?: UUIFormLayoutItemElement;
|
||||
_usernameInput?: UmbLoginInputElement;
|
||||
_passwordInput?: UmbLoginInputElement;
|
||||
_usernameLabel?: UUILabelElement;
|
||||
_passwordLabel?: UUILabelElement;
|
||||
|
||||
(this as unknown as EventTarget).addEventListener('umb-login-flow', (e) => {
|
||||
if (e instanceof CustomEvent) {
|
||||
this.flow = e.detail.flow || undefined;
|
||||
}
|
||||
this.requestUpdate();
|
||||
});
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
this.classList.add('uui-text');
|
||||
this.classList.add('uui-font');
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<umb-auth-layout background-image=${ifDefined(this.backgroundImage)} logo-image=${ifDefined(this.logoImage)}>
|
||||
(this as unknown as EventTarget).addEventListener('umb-login-flow', (e) => {
|
||||
if (e instanceof CustomEvent) {
|
||||
this.flow = e.detail.flow || undefined;
|
||||
}
|
||||
this.requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.#initializeForm();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._usernameLayoutItem?.remove();
|
||||
this._passwordLayoutItem?.remove();
|
||||
this._usernameLabel?.remove();
|
||||
this._usernameInput?.remove();
|
||||
this._passwordLabel?.remove();
|
||||
this._passwordInput?.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.
|
||||
*
|
||||
* @see Track this intent-to-ship for Chrome https://groups.google.com/a/chromium.org/g/blink-dev/c/RY9leYMu5hI?pli=1
|
||||
* @private
|
||||
*/
|
||||
async #initializeForm() {
|
||||
const labelUsername =
|
||||
this.usernameIsEmail
|
||||
? await umbLocalizationContext.localize('general_username', undefined, 'Username')
|
||||
: await umbLocalizationContext.localize('general_email', undefined, 'Email');
|
||||
const labelPassword = await umbLocalizationContext.localize('general_password', undefined, 'Password');
|
||||
const requiredMessage = await umbLocalizationContext.localize('general_required', undefined, 'Required');
|
||||
|
||||
this._usernameInput = createInput({
|
||||
id: 'username-input',
|
||||
type: 'text',
|
||||
name: 'username',
|
||||
autocomplete: 'username',
|
||||
requiredMessage,
|
||||
label: labelUsername,
|
||||
inputmode: this.usernameIsEmail ? 'email' : ''
|
||||
});
|
||||
this._passwordInput = createInput({
|
||||
id: 'password-input',
|
||||
type: 'password',
|
||||
name: 'password',
|
||||
autocomplete: 'current-password',
|
||||
requiredMessage,
|
||||
label: labelPassword,
|
||||
inputmode: ''
|
||||
});
|
||||
this._usernameLabel = createLabel({ forId: 'username-input', localizeAlias: this.usernameIsEmail ? 'general_email' : 'user_username' });
|
||||
this._passwordLabel = createLabel({ forId: 'password-input', localizeAlias: 'user_password' });
|
||||
|
||||
this._usernameLayoutItem = createFormLayoutItem(this._usernameLabel, this._usernameInput);
|
||||
this._passwordLayoutItem = createFormLayoutItem(this._passwordLabel, this._passwordInput);
|
||||
|
||||
this._form = createForm([this._usernameLayoutItem, this._passwordLayoutItem]);
|
||||
|
||||
this.insertAdjacentElement('beforeend', this._form);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<umb-auth-layout
|
||||
background-image=${ifDefined(this.backgroundImage)}
|
||||
logo-image=${ifDefined(this.logoImage)}>
|
||||
${this._renderFlowAndStatus()}
|
||||
</umb-auth-layout>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
private _renderFlowAndStatus() {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
let flow = this.flow || searchParams.get('flow')?.toLowerCase();
|
||||
const status = searchParams.get('status');
|
||||
private _renderFlowAndStatus() {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
let flow = this.flow || searchParams.get('flow')?.toLowerCase();
|
||||
const status = searchParams.get('status');
|
||||
|
||||
if (status === 'resetCodeExpired') {
|
||||
return html` <umb-error-layout
|
||||
header="Hi there"
|
||||
message=${until(umbLocalizationContext.localize('login_resetCodeExpired'), 'The link you have clicked on is invalid or has expired')}>
|
||||
</umb-error-layout>`;
|
||||
}
|
||||
if (status === 'resetCodeExpired') {
|
||||
return html` <umb-error-layout
|
||||
header="Hi there"
|
||||
message=${until(
|
||||
umbLocalizationContext.localize('login_resetCodeExpired', undefined, 'The link you have clicked on is invalid or has expired')
|
||||
)}>
|
||||
</umb-error-layout>`;
|
||||
}
|
||||
|
||||
if (flow === 'invite-user' && status === 'false') {
|
||||
return html` <umb-error-layout
|
||||
header="Hi there"
|
||||
message=${until(umbLocalizationContext.localize('user_userinviteExpiredMessage'), 'Welcome to Umbraco! Unfortunately your invite has expired. Please contact your administrator and ask them to resend it.')}>
|
||||
</umb-error-layout>`;
|
||||
}
|
||||
if (flow === 'invite-user' && status === 'false') {
|
||||
return html` <umb-error-layout
|
||||
header="Hi there"
|
||||
message=${until(
|
||||
umbLocalizationContext.localize('user_userinviteExpiredMessage', undefined, 'Welcome to Umbraco! Unfortunately your invite has expired. Please contact your administrator and ask them to resend it.'),
|
||||
)}>
|
||||
</umb-error-layout>`;
|
||||
}
|
||||
|
||||
// validate
|
||||
if (flow) {
|
||||
if (flow === 'mfa' && !umbAuthContext.isMfaEnabled) {
|
||||
flow = undefined;
|
||||
}
|
||||
}
|
||||
// validate
|
||||
if (flow) {
|
||||
if (flow === 'mfa' && !umbAuthContext.isMfaEnabled) {
|
||||
flow = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
switch (flow) {
|
||||
case 'mfa':
|
||||
return html`<umb-mfa-page></umb-mfa-page>`;
|
||||
case 'reset':
|
||||
return html`<umb-reset-password-page></umb-reset-password-page>`;
|
||||
case 'reset-password':
|
||||
return html`<umb-new-password-page></umb-new-password-page>`;
|
||||
case 'invite-user':
|
||||
return html`<umb-invite-page></umb-invite-page>`;
|
||||
switch (flow) {
|
||||
case 'mfa':
|
||||
return html`<umb-mfa-page></umb-mfa-page>`;
|
||||
case 'reset':
|
||||
return html`<umb-reset-password-page></umb-reset-password-page>`;
|
||||
case 'reset-password':
|
||||
return html`<umb-new-password-page></umb-new-password-page>`;
|
||||
case 'invite-user':
|
||||
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 name="subheadline" slot="subheadline"></slot>
|
||||
<slot name="external" slot="external"></slot>
|
||||
</umb-login-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>
|
||||
<slot name="external" slot="external"></slot>
|
||||
</umb-login-page>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-auth': UmbAuthElement;
|
||||
}
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-auth': UmbAuthElement;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// make new lit element that extends UUIInputElement
|
||||
|
||||
import { UUIInputElement } from '@umbraco-ui/uui';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
|
||||
@customElement('umb-login-input')
|
||||
export class UmbLoginInputElement extends UUIInputElement {
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
static styles = [...UUIInputElement.styles];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-login-input': UmbLoginInputElement;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { UUIButtonState } from '@umbraco-ui/uui';
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { customElement, property, queryAssignedElements, state } from 'lit/decorators.js';
|
||||
import { when } from 'lit/directives/when.js';
|
||||
import { until } from 'lit/directives/until.js';
|
||||
|
||||
@@ -9,225 +9,194 @@ import { umbLocalizationContext } from '../../external/localization/localization
|
||||
|
||||
@customElement('umb-login-page')
|
||||
export default class UmbLoginPageElement extends LitElement {
|
||||
@property({ type: Boolean, attribute: 'username-is-email' })
|
||||
usernameIsEmail = false;
|
||||
|
||||
@property({ type: Boolean, attribute: 'username-is-email' })
|
||||
usernameIsEmail = false;
|
||||
@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 = undefined;
|
||||
@state()
|
||||
private _loginState: UUIButtonState = undefined;
|
||||
|
||||
@state()
|
||||
private _loginError = '';
|
||||
@state()
|
||||
private _loginError = '';
|
||||
|
||||
@state()
|
||||
private get disableLocalLogin() {
|
||||
return umbAuthContext.disableLocalLogin;
|
||||
}
|
||||
@state()
|
||||
private get disableLocalLogin() {
|
||||
return umbAuthContext.disableLocalLogin;
|
||||
}
|
||||
|
||||
#handleSubmit = async (e: SubmitEvent) => {
|
||||
e.preventDefault();
|
||||
#formElement?: HTMLFormElement;
|
||||
|
||||
const form = e.target as HTMLFormElement;
|
||||
if (!form) return;
|
||||
async #onSlotChanged() {
|
||||
this.#formElement = this.slottedElements?.[0];
|
||||
|
||||
if (!form.checkValidity()) return;
|
||||
if (!this.#formElement) return;
|
||||
|
||||
const formData = new FormData(form);
|
||||
this.#formElement.onsubmit = this.#handleSubmit;
|
||||
}
|
||||
|
||||
const username = formData.get('email') as string;
|
||||
const password = formData.get('password') as string;
|
||||
const persist = formData.has('persist');
|
||||
#handleSubmit = async (e: SubmitEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
this._loginState = 'waiting';
|
||||
const form = e.target as HTMLFormElement;
|
||||
if (!form) return;
|
||||
|
||||
const response = await umbAuthContext.login({
|
||||
username,
|
||||
password,
|
||||
persist,
|
||||
});
|
||||
if (!form.checkValidity()) return;
|
||||
|
||||
this._loginError = response.error || '';
|
||||
this._loginState = response.error ? 'failed' : 'success';
|
||||
const formData = new FormData(form);
|
||||
|
||||
// Check for 402 status code indicating that MFA is required
|
||||
if (response.status === 402) {
|
||||
umbAuthContext.isMfaEnabled = true;
|
||||
if (response.twoFactorView) {
|
||||
umbAuthContext.twoFactorView = response.twoFactorView;
|
||||
}
|
||||
const username = formData.get('username') as string;
|
||||
const password = formData.get('password') as string;
|
||||
const persist = formData.has('persist');
|
||||
|
||||
this.dispatchEvent(new CustomEvent('umb-login-flow', { composed: true, detail: { flow: 'mfa' }}));
|
||||
return;
|
||||
}
|
||||
this._loginState = 'waiting';
|
||||
|
||||
if (response.error) {
|
||||
this.dispatchEvent(new CustomEvent('umb-login-failed', { bubbles: true, composed: true, detail: response }));
|
||||
return;
|
||||
}
|
||||
const response = await umbAuthContext.login({
|
||||
username,
|
||||
password,
|
||||
persist,
|
||||
});
|
||||
|
||||
const returnPath = umbAuthContext.returnPath;
|
||||
this._loginError = response.error || '';
|
||||
this._loginState = response.error ? 'failed' : 'success';
|
||||
|
||||
if (returnPath) {
|
||||
location.href = returnPath;
|
||||
}
|
||||
// Check for 402 status code indicating that MFA is required
|
||||
if (response.status === 402) {
|
||||
umbAuthContext.isMfaEnabled = true;
|
||||
if (response.twoFactorView) {
|
||||
umbAuthContext.twoFactorView = response.twoFactorView;
|
||||
}
|
||||
|
||||
this.dispatchEvent(new CustomEvent('umb-login-success', { bubbles: true, composed: true, detail: response.data }));
|
||||
};
|
||||
this.dispatchEvent(new CustomEvent('umb-login-flow', { composed: true, detail: { flow: 'mfa' } }));
|
||||
return;
|
||||
}
|
||||
|
||||
get #greetingLocalizationKey() {
|
||||
return [
|
||||
'login_greeting0',
|
||||
'login_greeting1',
|
||||
'login_greeting2',
|
||||
'login_greeting3',
|
||||
'login_greeting4',
|
||||
'login_greeting5',
|
||||
'login_greeting6',
|
||||
][new Date().getDay()];
|
||||
}
|
||||
if (response.error) {
|
||||
this.dispatchEvent(new CustomEvent('umb-login-failed', { bubbles: true, composed: true, detail: response }));
|
||||
return;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
const returnPath = umbAuthContext.returnPath;
|
||||
|
||||
if (returnPath) {
|
||||
location.href = returnPath;
|
||||
}
|
||||
|
||||
this.dispatchEvent(new CustomEvent('umb-login-success', { bubbles: true, composed: true, detail: response.data }));
|
||||
};
|
||||
|
||||
get #greetingLocalizationKey() {
|
||||
return [
|
||||
'login_greeting0',
|
||||
'login_greeting1',
|
||||
'login_greeting2',
|
||||
'login_greeting3',
|
||||
'login_greeting4',
|
||||
'login_greeting5',
|
||||
'login_greeting6',
|
||||
][new Date().getDay()];
|
||||
}
|
||||
|
||||
#onSubmitClick = () => {
|
||||
this.#formElement?.requestSubmit();
|
||||
};
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<h1 id="greeting" class="uui-h3">
|
||||
<umb-localize .key=${this.#greetingLocalizationKey}></umb-localize>
|
||||
</h1>
|
||||
<slot name="subheadline"></slot>
|
||||
${this.disableLocalLogin
|
||||
? nothing
|
||||
: html`
|
||||
<uui-form>
|
||||
<form id="LoginForm" name="login" @submit="${this.#handleSubmit}">
|
||||
<uui-form-layout-item>
|
||||
<uui-label id="emailLabel" for="umb-username" slot="label" required>
|
||||
${this.usernameIsEmail
|
||||
? html`<umb-localize key="general_email">Email</umb-localize>`
|
||||
: html`<umb-localize key="user_username">Name</umb-localize>`}
|
||||
</uui-label>
|
||||
<uui-input
|
||||
type=${this.usernameIsEmail ? 'email' : 'text'}
|
||||
id="umb-username"
|
||||
name="email"
|
||||
autocomplete=${this.usernameIsEmail
|
||||
? 'username'
|
||||
: 'email'}
|
||||
.label=${this.usernameIsEmail
|
||||
? until(umbLocalizationContext.localize('general_email', undefined, 'Email'))
|
||||
: until(umbLocalizationContext.localize('user_username', undefined, 'Username'))}
|
||||
required
|
||||
required-message=${until(umbLocalizationContext.localize('general_required', undefined, 'Required'))}></uui-input>
|
||||
</uui-form-layout-item>
|
||||
? nothing
|
||||
: html`
|
||||
<slot @slotchange=${this.#onSlotChanged}></slot>
|
||||
<div id="secondary-actions">
|
||||
${when(
|
||||
umbAuthContext.supportsPersistLogin,
|
||||
() => html`<uui-form-layout-item>
|
||||
<uui-checkbox
|
||||
name="persist"
|
||||
.label=${until(umbLocalizationContext.localize('user_rememberMe', undefined, 'Remember me'))}>
|
||||
<umb-localize key="user_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="login_forgottenPassword">Forgotten password?</umb-localize>
|
||||
</button>`
|
||||
)}
|
||||
</div>
|
||||
<uui-button
|
||||
type="submit"
|
||||
id="umb-login-button"
|
||||
look="primary"
|
||||
@click=${this.#onSubmitClick}
|
||||
.label=${until(umbLocalizationContext.localize('general_login', undefined, 'Login'), 'Login')}
|
||||
color="default"
|
||||
.state=${this._loginState}></uui-button>
|
||||
|
||||
<uui-form-layout-item>
|
||||
<uui-label id="passwordLabel" for="umb-password" slot="label" required>
|
||||
<umb-localize key="user_password">Password</umb-localize>
|
||||
</uui-label>
|
||||
<uui-input-password
|
||||
id="umb-password"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
.label=${until(umbLocalizationContext.localize('user_password', undefined, 'Password'))}
|
||||
required
|
||||
required-message=${until(umbLocalizationContext.localize('general_required', undefined, 'Required'))}></uui-input-password>
|
||||
</uui-form-layout-item>
|
||||
|
||||
<div id="secondary-actions">
|
||||
${when(
|
||||
umbAuthContext.supportsPersistLogin,
|
||||
() => html`<uui-form-layout-item>
|
||||
<uui-checkbox name="persist" .label=${until(umbLocalizationContext.localize('user_rememberMe', undefined, 'Remember me'))}>
|
||||
<umb-localize key="user_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="login_forgottenPassword">Forgotten password?</umb-localize>
|
||||
</button>`
|
||||
)}
|
||||
</div>
|
||||
|
||||
${this.#renderErrorMessage()}
|
||||
|
||||
<uui-button
|
||||
type="submit"
|
||||
id="umb-login-button"
|
||||
look="primary"
|
||||
.label=${until(umbLocalizationContext.localize('general_login', undefined, 'Login'))}
|
||||
color="default"
|
||||
.state=${this._loginState}></uui-button>
|
||||
</form>
|
||||
</uui-form>
|
||||
${this.#renderErrorMessage()}
|
||||
`}
|
||||
<umb-external-login-providers-layout .showDivider=${!this.disableLocalLogin}>
|
||||
<slot name="external"></slot>
|
||||
</umb-external-login-providers-layout>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
#renderErrorMessage() {
|
||||
if (!this._loginError || this._loginState !== 'failed') return nothing;
|
||||
#renderErrorMessage() {
|
||||
if (!this._loginError || this._loginState !== 'failed') return nothing;
|
||||
|
||||
return html`<span class="text-error text-danger">${this._loginError}</span>`;
|
||||
}
|
||||
return html`<span class="text-error text-danger">${this._loginError}</span>`;
|
||||
}
|
||||
|
||||
#handleForgottenPassword() {
|
||||
this.dispatchEvent(new CustomEvent('umb-login-flow', { composed: true, detail: { flow: 'reset' }}));
|
||||
}
|
||||
#handleForgottenPassword() {
|
||||
this.dispatchEvent(new CustomEvent('umb-login-flow', { composed: true, detail: { flow: 'reset' } }));
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = [
|
||||
css`
|
||||
static styles: CSSResultGroup = [
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#greeting {
|
||||
color: var(--uui-color-interactive);
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 1.4rem;
|
||||
margin: 0 0 var(--uui-size-space-6);
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--uui-size-space-5);
|
||||
}
|
||||
|
||||
uui-form-layout-item {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
uui-input,
|
||||
uui-input-password {
|
||||
width: 100%;
|
||||
border-radius: var(--uui-border-radius);
|
||||
font-weight: 400;
|
||||
font-size: 1.5rem;
|
||||
margin: 0 0 var(--uui-size-layout-1);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
#umb-login-button {
|
||||
margin-top: var(--uui-size-space-4);
|
||||
width: 100%;
|
||||
--uui-button-padding-top-factor: 1.5;
|
||||
--uui-button-padding-bottom-factor: 1.5;
|
||||
}
|
||||
|
||||
#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);
|
||||
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);
|
||||
}
|
||||
|
||||
#forgot-password:hover {
|
||||
@@ -244,11 +213,11 @@ export default class UmbLoginPageElement extends LitElement {
|
||||
justify-content: space-between;
|
||||
}
|
||||
`,
|
||||
];
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-login-page': UmbLoginPageElement;
|
||||
}
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-login-page': UmbLoginPageElement;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,3 +15,5 @@ import './components/external-login-provider.element.js';
|
||||
import './components/layouts/new-password-layout.element.js';
|
||||
import './components/layouts/confirmation-layout.element.js';
|
||||
import './components/layouts/error-layout.element.js';
|
||||
|
||||
import './components/login-input.element.js';
|
||||
|
||||
Reference in New Issue
Block a user