V16: Upgrade Login dependencies to Umbraco 16 (#19433)

* build: updates login page to match Umbraco 16

* build(deps): bump heyapi to latest

* chore: simplify logic
This commit is contained in:
Jacob Overgaard
2025-05-28 14:09:09 +02:00
committed by GitHub
parent b4d5c8fd51
commit 434189ee47
8 changed files with 1115 additions and 1062 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -14,12 +14,12 @@
"npm": ">=10.9"
},
"devDependencies": {
"@hey-api/client-fetch": "^0.10.0",
"@hey-api/openapi-ts": "^0.66.3",
"@umbraco-cms/backoffice": "15.3.0",
"@hey-api/client-fetch": "^0.10.2",
"@hey-api/openapi-ts": "^0.67.6",
"@umbraco-cms/backoffice": "16.0.0-rc3",
"msw": "^2.7.0",
"typescript": "^5.7.3",
"vite": "^6.2.6",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vite-tsconfig-paths": "^5.1.4"
},
"msw": {

View File

@@ -34,9 +34,9 @@ export default class UmbNewPasswordLayoutElement extends UmbLitElement {
super();
this.consumeContext(UMB_AUTH_CONTEXT, (authContext) => {
this._passwordConfiguration = authContext.passwordConfiguration;
// Build a pattern
let pattern = '';
this._passwordConfiguration = authContext?.passwordConfiguration;
if (this._passwordConfiguration?.requireDigit) {
pattern += '(?=.*\\d)';
}

View File

@@ -33,7 +33,7 @@ export default class UmbLoginPageElement extends UmbLitElement {
this.consumeContext(UMB_AUTH_CONTEXT, (authContext) => {
this.#authContext = authContext;
this.supportPersistLogin = authContext.supportsPersistLogin;
this.supportPersistLogin = authContext?.supportsPersistLogin ?? false;
});
}

View File

@@ -1,168 +1,173 @@
import type {UUIButtonState} from '@umbraco-cms/backoffice/external/uui';
import {type CSSResultGroup, css, html, nothing, customElement, state} from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from "@umbraco-cms/backoffice/lit-element";
import type { UUIButtonState } from '@umbraco-cms/backoffice/external/uui';
import { type CSSResultGroup, css, html, nothing, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UMB_AUTH_CONTEXT } from '../../contexts';
@customElement('umb-reset-password-page')
export default class UmbResetPasswordPageElement extends UmbLitElement {
@state()
resetCallState: UUIButtonState = undefined;
@state()
resetCallState: UUIButtonState = undefined;
@state()
error = '';
@state()
error = '';
#handleResetSubmit = async (e: SubmitEvent) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
#handleResetSubmit = async (e: SubmitEvent) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
if (!form) return;
if (!form.checkValidity()) return;
if (!form) return;
if (!form.checkValidity()) return;
const formData = new FormData(form);
const username = formData.get('email') as string;
const formData = new FormData(form);
const username = formData.get('email') as string;
this.resetCallState = 'waiting';
const authContext = await this.getContext(UMB_AUTH_CONTEXT);
const response = await authContext.resetPassword(username);
this.resetCallState = response.error ? 'failed' : 'success';
this.error = response.error || '';
};
this.resetCallState = 'waiting';
const authContext = await this.getContext(UMB_AUTH_CONTEXT);
if (!authContext) {
this.resetCallState = 'failed';
this.error = 'Authentication context not available.';
return;
}
const response = await authContext.resetPassword(username);
this.resetCallState = response.error ? 'failed' : 'success';
this.error = response.error || '';
};
#renderResetPage() {
return html`
<uui-form>
<form id="LoginForm" name="login" @submit="${this.#handleResetSubmit}">
<header id="header">
<h1>
<umb-localize key="auth_forgottenPassword">Forgotten password?</umb-localize>
</h1>
<span>
#renderResetPage() {
return html`
<uui-form>
<form id="LoginForm" name="login" @submit="${this.#handleResetSubmit}">
<header id="header">
<h1>
<umb-localize key="auth_forgottenPassword">Forgotten password?</umb-localize>
</h1>
<span>
<umb-localize key="auth_forgottenPasswordInstruction">
An email will be sent to the address specified with a link to reset your password
</umb-localize>
An email will be sent to the address specified with a link to reset your password
</umb-localize>
</span>
</header>
</header>
<uui-form-layout-item>
<uui-label for="email" slot="label" required>
<umb-localize key="auth_email">Email</umb-localize>
</uui-label>
<uui-input
type="email"
id="email"
name="email"
.label=${this.localize.term('auth_email')}
required
required-message=${this.localize.term('auth_required')}>
</uui-input>
</uui-form-layout-item>
<uui-form-layout-item>
<uui-label for="email" slot="label" required>
<umb-localize key="auth_email">Email</umb-localize>
</uui-label>
<uui-input
type="email"
id="email"
name="email"
.label=${this.localize.term('auth_email')}
required
required-message=${this.localize.term('auth_required')}>
</uui-input>
</uui-form-layout-item>
${this.#renderErrorMessage()}
${this.#renderErrorMessage()}
<uui-button
type="submit"
.label=${this.localize.term('auth_submit')}
look="primary"
color="default"
.state=${this.resetCallState}></uui-button>
</form>
</uui-form>
<uui-button
type="submit"
.label=${this.localize.term('auth_submit')}
look="primary"
color="default"
.state=${this.resetCallState}></uui-button>
</form>
</uui-form>
<umb-back-to-login-button style="margin-top: var(--uui-size-space-6)"></umb-back-to-login-button>
`;
}
<umb-back-to-login-button style="margin-top: var(--uui-size-space-6)"></umb-back-to-login-button>
`;
}
#renderErrorMessage() {
if (!this.error || this.resetCallState !== 'failed') return nothing;
#renderErrorMessage() {
if (!this.error || this.resetCallState !== 'failed') return nothing;
return html`<span class="text-danger">${this.error}</span>`;
}
return html`<span class="text-danger">${this.error}</span>`;
}
#renderConfirmationPage() {
return html`
<umb-confirmation-layout
header=${this.localize.term('auth_forgottenPassword')}
message=${this.localize.term('auth_requestPasswordResetConfirmation')}>
</umb-confirmation-layout>
`;
}
#renderConfirmationPage() {
return html`
<umb-confirmation-layout
header=${this.localize.term('auth_forgottenPassword')}
message=${this.localize.term('auth_requestPasswordResetConfirmation')}>
</umb-confirmation-layout>
`;
}
render() {
return this.resetCallState === 'success' ? this.#renderConfirmationPage() : this.#renderResetPage();
}
render() {
return this.resetCallState === 'success' ? this.#renderConfirmationPage() : this.#renderResetPage();
}
static styles: CSSResultGroup = [
css`
#header {
text-align: center;
display: flex;
flex-direction: column;
gap: var(--uui-size-space-5);
}
static styles: CSSResultGroup = [
css`
#header {
text-align: center;
display: flex;
flex-direction: column;
gap: var(--uui-size-space-5);
}
#header span {
color: var(--uui-color-text-alt); /* TODO Change to uui color when uui gets a muted text variable */
font-size: 14px;
}
#header span {
color: var(--uui-color-text-alt); /* TODO Change to uui color when uui gets a muted text variable */
font-size: 14px;
}
#header h1 {
margin: 0;
font-weight: 400;
font-size: var(--header-secondary-font-size);
color: var(--uui-color-interactive);
line-height: 1.2;
}
#header h1 {
margin: 0;
font-weight: 400;
font-size: var(--header-secondary-font-size);
color: var(--uui-color-interactive);
line-height: 1.2;
}
form {
display: flex;
flex-direction: column;
gap: var(--uui-size-layout-2);
}
form {
display: flex;
flex-direction: column;
gap: var(--uui-size-layout-2);
}
uui-form-layout-item {
margin: 0;
}
uui-form-layout-item {
margin: 0;
}
uui-input,
uui-input-password {
width: 100%;
height: var(--input-height);
border-radius: var(--uui-border-radius);
}
uui-input,
uui-input-password {
width: 100%;
height: var(--input-height);
border-radius: var(--uui-border-radius);
}
uui-input {
width: 100%;
}
uui-input {
width: 100%;
}
uui-button {
width: 100%;
--uui-button-padding-top-factor: 1.5;
--uui-button-padding-bottom-factor: 1.5;
}
uui-button {
width: 100%;
--uui-button-padding-top-factor: 1.5;
--uui-button-padding-bottom-factor: 1.5;
}
#resend {
display: inline-flex;
font-size: 14px;
align-self: center;
gap: var(--uui-size-space-1);
}
#resend {
display: inline-flex;
font-size: 14px;
align-self: center;
gap: var(--uui-size-space-1);
}
#resend a {
color: var(--uui-color-selected);
font-weight: 600;
text-decoration: none;
}
#resend a {
color: var(--uui-color-selected);
font-weight: 600;
text-decoration: none;
}
#resend a:hover {
color: var(--uui-color-interactive-emphasis);
}
`,
];
#resend a:hover {
color: var(--uui-color-interactive-emphasis);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
'umb-reset-password-page': UmbResetPasswordPageElement;
}
interface HTMLElementTagNameMap {
'umb-reset-password-page': UmbResetPasswordPageElement;
}
}

View File

@@ -1,83 +1,85 @@
import {
LoginRequestModel,
LoginResponse,
ResetPasswordResponse,
ValidatePasswordResetCodeResponse,
NewPasswordResponse,
PasswordConfigurationModel, ValidateInviteCodeResponse, MfaCodeResponse
} from "../types.js";
LoginRequestModel,
LoginResponse,
ResetPasswordResponse,
ValidatePasswordResetCodeResponse,
NewPasswordResponse,
PasswordConfigurationModel,
ValidateInviteCodeResponse,
MfaCodeResponse,
} from '../types.js';
import { UmbAuthRepository } from './auth.repository.js';
import { UmbContextBase } from "@umbraco-cms/backoffice/class-api";
import { UmbContextToken } from "@umbraco-cms/backoffice/context-api";
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
export class UmbAuthContext extends UmbContextBase<UmbAuthContext> {
readonly supportsPersistLogin = false;
twoFactorView = '';
isMfaEnabled = false;
mfaProviders: string[] = [];
passwordConfiguration?: PasswordConfigurationModel;
export class UmbAuthContext extends UmbContextBase {
readonly supportsPersistLogin = false;
twoFactorView = '';
isMfaEnabled = false;
mfaProviders: string[] = [];
passwordConfiguration?: PasswordConfigurationModel;
#authRepository = new UmbAuthRepository(this);
#authRepository = new UmbAuthRepository(this);
#returnPath = '';
#returnPath = '';
set returnPath(value: string) {
this.#returnPath = value;
}
set returnPath(value: string) {
this.#returnPath = value;
}
/**
* Gets the return path from the query string.
*
* It will first look for a `ReturnUrl` parameter, then a `returnPath` parameter, and finally the `returnPath` property.
*
* @returns The return path from the query string.
*/
get returnPath(): string {
const params = new URLSearchParams(window.location.search);
let returnPath = params.get('ReturnUrl') ?? params.get('returnPath') ?? this.#returnPath;
/**
* Gets the return path from the query string.
*
* It will first look for a `ReturnUrl` parameter, then a `returnPath` parameter, and finally the `returnPath` property.
*
* @returns The return path from the query string.
*/
get returnPath(): string {
const params = new URLSearchParams(window.location.search);
let returnPath = params.get('ReturnUrl') ?? params.get('returnPath') ?? this.#returnPath;
// If return path is empty, return an empty string.
if (!returnPath) {
return '';
}
// If return path is empty, return an empty string.
if (!returnPath) {
return '';
}
// Safely check that the return path is valid and doesn't link to an external site.
const url = new URL(returnPath, window.location.origin);
// Safely check that the return path is valid and doesn't link to an external site.
const url = new URL(returnPath, window.location.origin);
if (url.origin !== window.location.origin) {
return '';
}
if (url.origin !== window.location.origin) {
return '';
}
return url.toString();
}
return url.toString();
}
login(data: LoginRequestModel): Promise<LoginResponse> {
return this.#authRepository.login(data);
}
login(data: LoginRequestModel): Promise<LoginResponse> {
return this.#authRepository.login(data);
}
resetPassword(username: string): Promise<ResetPasswordResponse> {
return this.#authRepository.resetPassword(username);
}
resetPassword(username: string): Promise<ResetPasswordResponse> {
return this.#authRepository.resetPassword(username);
}
validatePasswordResetCode(userId: string, resetCode: string): Promise<ValidatePasswordResetCodeResponse> {
return this.#authRepository.validatePasswordResetCode(userId, resetCode);
}
validatePasswordResetCode(userId: string, resetCode: string): Promise<ValidatePasswordResetCodeResponse> {
return this.#authRepository.validatePasswordResetCode(userId, resetCode);
}
newPassword(password: string, resetCode: string, userId: string): Promise<NewPasswordResponse> {
return this.#authRepository.newPassword(password, resetCode, userId);
}
newPassword(password: string, resetCode: string, userId: string): Promise<NewPasswordResponse> {
return this.#authRepository.newPassword(password, resetCode, userId);
}
newInvitedUserPassword(password: string, token: string, userId: string): Promise<NewPasswordResponse> {
return this.#authRepository.newInvitedUserPassword(password, token, userId);
}
newInvitedUserPassword(password: string, token: string, userId: string): Promise<NewPasswordResponse> {
return this.#authRepository.newInvitedUserPassword(password, token, userId);
}
validateInviteCode(token: string, userId: string): Promise<ValidateInviteCodeResponse> {
return this.#authRepository.validateInviteCode(token, userId);
}
validateInviteCode(token: string, userId: string): Promise<ValidateInviteCodeResponse> {
return this.#authRepository.validateInviteCode(token, userId);
}
validateMfaCode(code: string, provider: string): Promise<MfaCodeResponse> {
return this.#authRepository.validateMfaCode(code, provider);
}
validateMfaCode(code: string, provider: string): Promise<MfaCodeResponse> {
return this.#authRepository.validateMfaCode(code, provider);
}
}
export const UMB_AUTH_CONTEXT = new UmbContextToken<UmbAuthContext>('UmbAuthContext');

View File

@@ -7,8 +7,6 @@ import {
ValidateInviteCodeResponse,
ValidatePasswordResetCodeResponse,
} from '../types.js';
import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository';
import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
import {
postSecurityForgotPassword,
postSecurityForgotPasswordReset,
@@ -17,6 +15,8 @@ import {
postUserInviteVerify,
} from '../api/index.js';
import { isProblemDetails } from '../utils/is-problem-details.function.js';
import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository';
import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
export class UmbAuthRepository extends UmbRepositoryBase {
#localize = new UmbLocalizationController(this);

View File

@@ -1,14 +1,11 @@
import { UmbBundleExtensionInitializer, UmbServerExtensionRegistrator } from '@umbraco-cms/backoffice/extension-api';
import {
UmbBundleExtensionInitializer,
UmbServerExtensionRegistrator
} from "@umbraco-cms/backoffice/extension-api";
import {
UmbAppEntryPointExtensionInitializer,
umbExtensionsRegistry
} from "@umbraco-cms/backoffice/extension-registry";
import type { UmbElement } from "@umbraco-cms/backoffice/element-api";
import { UmbControllerBase } from "@umbraco-cms/backoffice/class-api";
import { UUIIconRegistryEssential } from "@umbraco-cms/backoffice/external/uui";
UmbAppEntryPointExtensionInitializer,
umbExtensionsRegistry,
} from '@umbraco-cms/backoffice/extension-registry';
import type { UmbElement } from '@umbraco-cms/backoffice/element-api';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import { UUIIconRegistryEssential } from '@umbraco-cms/backoffice/external/uui';
// We import what we need from the Backoffice app.
// In the future the login screen app will be a part of the Backoffice app, and we will not need to import these.
@@ -19,17 +16,20 @@ import '@umbraco-cms/backoffice/localization';
* It is responsible for initializing the backoffice and only the extensions that is needed to run the login screen.
*/
export class UmbSlimBackofficeController extends UmbControllerBase {
#uuiIconRegistry = new UUIIconRegistryEssential();
#uuiIconRegistry = new UUIIconRegistryEssential();
constructor(host: UmbElement) {
super(host);
new UmbBundleExtensionInitializer(host, umbExtensionsRegistry);
new UmbAppEntryPointExtensionInitializer(host, umbExtensionsRegistry);
new UmbServerExtensionRegistrator(host, umbExtensionsRegistry).registerPublicExtensions();
constructor(host: UmbElement) {
super(host);
new UmbBundleExtensionInitializer(host, umbExtensionsRegistry);
new UmbAppEntryPointExtensionInitializer(host, umbExtensionsRegistry);
new UmbServerExtensionRegistrator(host, umbExtensionsRegistry).registerPublicExtensions().catch(() => {
// We don't care about errors here, as this is just a fallback for the login screen.
// If the extensions are not registered, the login screen will still work, but some features may not be available.
});
this.#uuiIconRegistry.attach(host);
this.#uuiIconRegistry.attach(host);
host.classList.add('uui-text');
host.classList.add('uui-font');
}
host.classList.add('uui-text');
host.classList.add('uui-font');
}
}