From a5c84743564c317e6076e561fe8f42e444afd2b4 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 19 Apr 2024 11:45:10 +0200 Subject: [PATCH 01/45] add condition to check if the user is allowed to configure external login --- .../user/user/conditions/manifests.ts | 2 ++ ...r-allow-external-login-action.condition.ts | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/user-allow-external-login-action.condition.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/manifests.ts index e085874d5e..4670fdad35 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/manifests.ts @@ -1,6 +1,7 @@ import { manifest as userAllowDisableActionManifest } from './user-allow-disable-action.condition.js'; import { manifest as userAllowEnableActionManifest } from './user-allow-enable-action.condition.js'; import { manifest as userAllowUnlockActionManifest } from './user-allow-unlock-action.condition.js'; +import { manifest as userAllowExternalLoginActionManifest } from './user-allow-external-login-action.condition.js'; import { manifest as userAllowMfaActionManifest } from './user-allow-mfa-action.condition.js'; import { manifest as userAllowDeleteActionManifest } from './user-allow-delete-action.condition.js'; @@ -8,6 +9,7 @@ export const manifests = [ userAllowDisableActionManifest, userAllowEnableActionManifest, userAllowUnlockActionManifest, + userAllowExternalLoginActionManifest, userAllowMfaActionManifest, userAllowDeleteActionManifest, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/user-allow-external-login-action.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/user-allow-external-login-action.condition.ts new file mode 100644 index 0000000000..89b6aa140f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/user-allow-external-login-action.condition.ts @@ -0,0 +1,24 @@ +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { ManifestCondition } from '@umbraco-cms/backoffice/extension-api'; +import { UmbConditionBase, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; + +export class UmbUserAllowExternalLoginActionCondition extends UmbConditionBase { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(host: UmbControllerHost, args: any) { + super(host, args); + + // Check if there are any MFA providers available + this.observe( + umbExtensionsRegistry.byType('authProvider'), + (exts) => (this.permitted = exts.length > 0), + '_userAllowExternalLoginActionConditionProviders', + ); + } +} + +export const manifest: ManifestCondition = { + type: 'condition', + name: 'User Allow ExternalLogin Action Condition', + alias: 'Umb.Condition.User.AllowExternalLoginAction', + api: UmbUserAllowExternalLoginActionCondition, +}; From 00ffd036c9d6b74a293c71a54393e555b098b711 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 19 Apr 2024 11:46:31 +0200 Subject: [PATCH 02/45] remove unused file --- .../mfa-providers-current-user-app.element.ts | 54 ------------------- 1 file changed, 54 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/current-user/mfa-login/mfa-providers-current-user-app.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/mfa-login/mfa-providers-current-user-app.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/mfa-login/mfa-providers-current-user-app.element.ts deleted file mode 100644 index f48ab2e4ed..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/mfa-login/mfa-providers-current-user-app.element.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { UMB_CURRENT_USER_MFA_MODAL } from '../modals/current-user-mfa/current-user-mfa-modal.token.js'; -import { html, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; -import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; -import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs'; - -@customElement('umb-mfa-providers-current-user-app') -export class UmbMfaProvidersCurrentUserAppElement extends UmbLitElement { - @state() - _hasProviders = false; - - constructor() { - super(); - this.#init(); - } - - async #init() { - this._hasProviders = (await firstValueFrom(umbExtensionsRegistry.byType('mfaLoginProvider'))).length > 0; - } - - render() { - if (!this._hasProviders) { - return nothing; - } - - return html` - - - Configure Two Factor - - `; - } - - async #onClick() { - const modalManagerContext = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); - await modalManagerContext.open(this, UMB_CURRENT_USER_MFA_MODAL).onSubmit(); - } - - static styles = [UmbTextStyles]; -} - -export default UmbMfaProvidersCurrentUserAppElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-mfa-providers-current-user-app': UmbMfaProvidersCurrentUserAppElement; - } -} From 600beb2bbd7541958b6e15604b749aea9443da17 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 19 Apr 2024 11:47:01 +0200 Subject: [PATCH 03/45] add source for external login --- .../repository/current-user.repository.ts | 15 +++++++++++++++ .../current-user.server.data-source.ts | 14 ++++++++++++++ .../repository/current-user.store.ts | 17 ++++++++++++++++- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.repository.ts index 40026ffe1a..8361439260 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.repository.ts @@ -40,6 +40,21 @@ export class UmbCurrentUserRepository extends UmbRepositoryBase { return { data, error, asObservable: () => this.#currentUserStore!.data }; } + /** + * Request the current user's external login providers + * @memberof UmbCurrentUserRepository + */ + async requestExternalLoginProviders() { + await this.#init; + const { data, error } = await this.#currentUserSource.getExternalLoginProviders(); + + if (data) { + this.#currentUserStore?.setExternalLoginProviders(data.linkedLogins); + } + + return { data: data?.linkedLogins, error, asObservable: () => this.#currentUserStore!.externalLoginProviders }; + } + /** * Request the current user's available MFA login providers * @memberof UmbCurrentUserRepository diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.server.data-source.ts index 46bfc12a3c..2def478a38 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.server.data-source.ts @@ -51,6 +51,20 @@ export class UmbCurrentUserServerDataSource { return { error }; } + /** + * Get the current user's external login providers + * @memberof UmbCurrentUserServerDataSource + */ + async getExternalLoginProviders() { + const { data, error } = await tryExecuteAndNotify(this.#host, UserService.getUserCurrentLogins()); + + if (data) { + return { data }; + } + + return { error }; + } + /** * Get the current user's available MFA login providers * @memberof UmbCurrentUserServerDataSource diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.store.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.store.ts index 1e6a9ffa1f..de5e13a73d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.store.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.store.ts @@ -1,4 +1,8 @@ -import type { UmbCurrentUserMfaProviderModel, UmbCurrentUserModel } from '../types.js'; +import type { + UmbCurrentUserExternalLoginProviderModel, + UmbCurrentUserMfaProviderModel, + UmbCurrentUserModel, +} from '../types.js'; import type { UmbUserDetailModel } from '@umbraco-cms/backoffice/user'; import { UMB_USER_DETAIL_STORE_CONTEXT } from '@umbraco-cms/backoffice/user'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; @@ -13,6 +17,9 @@ export class UmbCurrentUserStore extends UmbContextBase { #mfaProviders = new UmbArrayState([], (e) => e.providerName); readonly mfaProviders = this.#mfaProviders.asObservable(); + #externalLoginProviders = new UmbArrayState([], (e) => e.providerName); + readonly externalLoginProviders = this.#externalLoginProviders.asObservable(); + constructor(host: UmbControllerHost) { super(host, UMB_CURRENT_USER_STORE_CONTEXT); @@ -85,6 +92,14 @@ export class UmbCurrentUserStore extends UmbContextBase { updateMfaProvider(data: Partial) { this.#mfaProviders.updateOne(data.providerName, data); } + + setExternalLoginProviders(data: Array) { + this.#externalLoginProviders.setValue(data); + } + + updateExternalLoginProvider(data: Partial) { + this.#externalLoginProviders.updateOne(data.providerName, data); + } } export const UMB_CURRENT_USER_STORE_CONTEXT = new UmbContextToken('UmbCurrentUserStore'); From d91c385453f11166de3724ce359bfff2280e4d03 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 19 Apr 2024 11:47:17 +0200 Subject: [PATCH 04/45] add modal + button to configure external login on the current user --- ...nfigure-external-login-providers-action.ts | 18 ++ .../current-user/external-login/manifests.ts | 33 ++++ .../modals/external-login-modal.element.ts | 160 ++++++++++++++++++ .../modals/external-login-modal.stories.ts | 46 +++++ .../modals/external-login-modal.token.ts | 8 + .../packages/user/current-user/manifests.ts | 2 + .../src/packages/user/current-user/types.ts | 3 + 7 files changed, 270 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/configure-external-login-providers-action.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.stories.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.token.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/configure-external-login-providers-action.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/configure-external-login-providers-action.ts new file mode 100644 index 0000000000..a3706c2988 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/configure-external-login-providers-action.ts @@ -0,0 +1,18 @@ +import { UMB_CURRENT_USER_EXTERNAL_LOGIN_MODAL } from './modals/external-login-modal.token.js'; +import { UmbActionBase } from '@umbraco-cms/backoffice/action'; +import type { UmbCurrentUserAction, UmbCurrentUserActionArgs } from '@umbraco-cms/backoffice/extension-registry'; +import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; + +export class UmbConfigureExternalLoginProvidersApi + extends UmbActionBase> + implements UmbCurrentUserAction +{ + async getHref() { + return undefined; + } + + async execute() { + const modalManagerContext = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + await modalManagerContext.open(this, UMB_CURRENT_USER_EXTERNAL_LOGIN_MODAL).onSubmit(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/manifests.ts new file mode 100644 index 0000000000..25607618bc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/manifests.ts @@ -0,0 +1,33 @@ +import { UmbConfigureExternalLoginProvidersApi } from './configure-external-login-providers-action.js'; +import type { ManifestCurrentUserActionDefaultKind, ManifestModal } from '@umbraco-cms/backoffice/extension-registry'; + +export const modals: Array = [ + { + type: 'modal', + alias: 'Umb.Modal.CurrentUserExternalLogin', + name: 'External Login Modal', + js: () => import('./modals/external-login-modal.element.js'), + }, +]; + +export const userProfileApps: Array = [ + { + type: 'currentUserAction', + kind: 'default', + alias: 'Umb.CurrentUser.App.ExternalLoginProviders', + name: 'External Login Providers Current User App', + weight: 800, + api: UmbConfigureExternalLoginProvidersApi, + meta: { + label: '#defaultdialogs_externalLoginProviders', + icon: 'icon-lock', + look: 'secondary', + }, + conditions: [ + { + alias: 'Umb.Condition.User.AllowExternalLoginAction', + }, + ], + }, +]; +export const manifests = [...modals, ...userProfileApps]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts new file mode 100644 index 0000000000..de2ad8523f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts @@ -0,0 +1,160 @@ +import { UmbCurrentUserRepository } from '../../repository/index.js'; +import type { UmbCurrentUserExternalLoginProviderModel } from '../../types.js'; +import { css, customElement, html, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbModalContext } from '@umbraco-cms/backoffice/modal'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { mergeObservables } from '@umbraco-cms/backoffice/observable-api'; + +type UmbExternalLoginProviderOption = UmbCurrentUserExternalLoginProviderModel & { + displayName: string; + isEnabledOnUser: boolean; +}; + +@customElement('umb-current-user-external-login-modal') +export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { + @property({ attribute: false }) + modalContext?: UmbModalContext; + + @state() + _items: Array = []; + + #currentUserRepository = new UmbCurrentUserRepository(this); + + constructor() { + super(); + this.#loadProviders(); + } + + async #loadProviders() { + const serverLoginProviders$ = (await this.#currentUserRepository.requestExternalLoginProviders()).asObservable(); + const manifestLoginProviders$ = umbExtensionsRegistry.byType('authProvider'); + + // Merge the server and manifest providers to get the final list of providers + const externalLoginProviders$ = mergeObservables( + [serverLoginProviders$, manifestLoginProviders$], + ([serverLoginProviders, manifestLoginProviders]) => { + const providers: UmbExternalLoginProviderOption[] = manifestLoginProviders.map((manifestLoginProvider) => { + const serverLoginProvider = serverLoginProviders.find( + (serverLoginProvider) => serverLoginProvider.providerName === manifestLoginProvider.forProviderName, + ); + return { + isEnabledOnUser: false, // TODO: Get this from the server + providerKey: manifestLoginProvider.forProviderName, + providerName: manifestLoginProvider.forProviderName, + displayName: + manifestLoginProvider.meta?.label ?? serverLoginProvider?.providerName ?? manifestLoginProvider.name, + }; + }); + + // Add any server providers that are not in the manifest + serverLoginProviders.forEach((serverLoginProvider) => { + if (!providers.some((p) => p.providerName === serverLoginProvider.providerName)) { + providers.push({ + isEnabledOnUser: false, // TODO: Get this from the server + providerKey: serverLoginProvider.providerName, + providerName: serverLoginProvider.providerName, + displayName: serverLoginProvider.providerName, + }); + } + }); + + return providers; + }, + ); + + this.observe( + externalLoginProviders$, + (providers) => { + this._items = providers; + }, + '_externalLoginProviders', + ); + } + + #close() { + this.modalContext?.submit(); + } + + render() { + return html` + +
+ ${when( + this._items.length > 0, + () => html` + ${repeat( + this._items, + (item) => item.providerName, + (item) => this.#renderProvider(item), + )} + `, + )} +
+
+ + ${this.localize.term('general_close')} + +
+
+ `; + } + + /** + * Render a provider with a toggle to enable/disable it + */ + #renderProvider(item: UmbExternalLoginProviderOption) { + return html` + + ${when( + item.isEnabledOnUser, + () => html` +

+ This provider is enabled + +

+ this.#onProviderDisable(item)}> + `, + () => html` + this.#onProviderEnable(item)}> + `, + )} +
+ `; + } + + async #onProviderEnable(item: UmbExternalLoginProviderOption) { + alert('Enable provider ' + item.providerName); + } + + async #onProviderDisable(item: UmbExternalLoginProviderOption) { + alert('Disable provider ' + item.providerName); + } + + static styles = [ + UmbTextStyles, + css` + uui-box { + margin-bottom: var(--uui-size-space-3); + } + `, + ]; +} + +export default UmbCurrentUserExternalLoginModalElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-current-user-external-login-modal': UmbCurrentUserExternalLoginModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.stories.ts new file mode 100644 index 0000000000..0a887bba9d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.stories.ts @@ -0,0 +1,46 @@ +import type { Meta, StoryObj } from '@storybook/web-components'; +import type { UmbCurrentUserExternalLoginModalElement } from './external-login-modal.element.js'; +import { html } from '@umbraco-cms/backoffice/external/lit'; +import { UmbServerExtensionRegistrator } from '@umbraco-cms/backoffice/extension-api'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; + +import './external-login-modal.element.js'; + +class UmbServerExtensionsHostElement extends UmbLitElement { + constructor() { + super(); + new UmbServerExtensionRegistrator(this, umbExtensionsRegistry).registerAllExtensions(); + } + + render() { + return html``; + } +} + +if (window.customElements.get('umb-server-extensions-host') === undefined) { + customElements.define('umb-server-extensions-host', UmbServerExtensionsHostElement); +} + +const meta: Meta = { + title: 'Current User/External Login/Configure External Login Providers', + component: 'umb-current-user-external-login-modal', + decorators: [ + (Story) => + html` + ${Story()} + `, + ], + parameters: { + layout: 'centered', + actions: { + disabled: true, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Overview: Story = {}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.token.ts new file mode 100644 index 0000000000..52f0adae32 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.token.ts @@ -0,0 +1,8 @@ +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export const UMB_CURRENT_USER_EXTERNAL_LOGIN_MODAL = new UmbModalToken('Umb.Modal.CurrentUserExternalLogin', { + modal: { + type: 'sidebar', + size: 'small', + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/manifests.ts index e3212adbe1..dce911901e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/manifests.ts @@ -1,6 +1,7 @@ import { manifest as actionDefaultKindManifest } from './action/default.kind.js'; import { manifests as modalManifests } from './modals/manifests.js'; import { manifests as historyManifests } from './history/manifests.js'; +import { manifests as externalLoginProviderManifests } from './external-login/manifests.js'; import { manifests as mfaLoginProviderManifests } from './mfa-login/manifests.js'; import { manifests as profileManifests } from './profile/manifests.js'; import { manifests as themeManifests } from './theme/manifests.js'; @@ -32,6 +33,7 @@ export const manifests = [ actionDefaultKindManifest, ...headerApps, ...historyManifests, + ...externalLoginProviderManifests, ...mfaLoginProviderManifests, ...modalManifests, ...profileManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/types.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/types.ts index ba8898ade5..2ffe906fed 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/types.ts @@ -2,6 +2,7 @@ import type { ApiError, CancelError, DocumentPermissionPresentationModel, + LinkedLoginModel, UnknownTypePermissionPresentationModel, UserTwoFactorProviderModel, } from '@umbraco-cms/backoffice/external/backend-api'; @@ -23,6 +24,8 @@ export interface UmbCurrentUserModel { isAdmin: boolean; } +export type UmbCurrentUserExternalLoginProviderModel = LinkedLoginModel; + export type UmbCurrentUserMfaProviderModel = UserTwoFactorProviderModel; export type UmbMfaProviderConfigurationCallback = Promise<{ error?: ApiError | CancelError }>; From e441c0c3a9722d93e83859b47e1b866558ee9766 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 19 Apr 2024 14:28:44 +0200 Subject: [PATCH 05/45] use current method as it is --- .../modals/external-login-modal.element.ts | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts index de2ad8523f..1c2bd7e203 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts @@ -29,7 +29,10 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { async #loadProviders() { const serverLoginProviders$ = (await this.#currentUserRepository.requestExternalLoginProviders()).asObservable(); - const manifestLoginProviders$ = umbExtensionsRegistry.byType('authProvider'); + const manifestLoginProviders$ = umbExtensionsRegistry.byTypeAndFilter( + 'authProvider', + (ext) => !!ext.meta?.linking?.allowManualLinking, + ); // Merge the server and manifest providers to get the final list of providers const externalLoginProviders$ = mergeObservables( @@ -40,26 +43,14 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { (serverLoginProvider) => serverLoginProvider.providerName === manifestLoginProvider.forProviderName, ); return { - isEnabledOnUser: false, // TODO: Get this from the server + isEnabledOnUser: !!serverLoginProvider, providerKey: manifestLoginProvider.forProviderName, providerName: manifestLoginProvider.forProviderName, displayName: - manifestLoginProvider.meta?.label ?? serverLoginProvider?.providerName ?? manifestLoginProvider.name, + manifestLoginProvider.meta?.label ?? manifestLoginProvider.forProviderName ?? manifestLoginProvider.name, }; }); - // Add any server providers that are not in the manifest - serverLoginProviders.forEach((serverLoginProvider) => { - if (!providers.some((p) => p.providerName === serverLoginProvider.providerName)) { - providers.push({ - isEnabledOnUser: false, // TODO: Get this from the server - providerKey: serverLoginProvider.providerName, - providerName: serverLoginProvider.providerName, - displayName: serverLoginProvider.providerName, - }); - } - }); - return providers; }, ); From a65fe6ee281a92f5b8435e19ccb56a5a59bcf367 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 19 Apr 2024 14:32:52 +0200 Subject: [PATCH 06/45] remove hardcoded text from label in manifest --- .../src/mocks/handlers/manifests.handlers.ts | 5 ++++- .../core/auth/components/auth-provider-default.element.ts | 8 ++++++-- .../src/packages/core/auth/providers/manifests.ts | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts index 20da4fec15..bd7ddaacdb 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts @@ -127,7 +127,10 @@ export const manifestDevelopmentHandlers = [ name: 'My Custom Auth Provider', forProviderName: 'Umbraco.Google', meta: { - label: 'Sign in with Google', + label: 'Google', + defaultView: { + icon: 'icon-google', + }, }, }, { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts index c6c10745c1..4f4afc1726 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts @@ -17,19 +17,23 @@ export class UmbAuthProviderDefaultElement extends UmbLitElement implements UmbA this.setAttribute('part', 'auth-provider-default'); } + get #label() { + return this.localize.term('login_signInWith', this.manifest.meta?.label ?? this.manifest.forProviderName); + } + render() { return html` this.onSubmit(this.manifest.forProviderName)} id="auth-provider-button" - .label=${this.manifest.meta?.label ?? this.manifest.forProviderName} + .label=${this.#label} .look=${this.manifest.meta?.defaultView?.look ?? 'outline'} .color=${this.manifest.meta?.defaultView?.color ?? 'default'}> ${this.manifest.meta?.defaultView?.icon ? html`` : nothing} - ${this.manifest.meta?.label ?? this.manifest.forProviderName} + ${this.#label} `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/providers/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/providers/manifests.ts index 5c8e7dc2de..bab8a1ac83 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/providers/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/providers/manifests.ts @@ -8,7 +8,7 @@ export const manifests: Array = [ forProviderName: 'Umbraco', weight: 1000, meta: { - label: 'Sign in with Umbraco', + label: 'Umbraco', defaultView: { icon: 'icon-umbraco', look: 'primary', From 87dd8e36f0c9a6be1aef9b35c1b0ffc908fd31c4 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 19 Apr 2024 14:37:20 +0200 Subject: [PATCH 07/45] remove unused icons --- .../modals/current-user-mfa/current-user-mfa-modal.element.ts | 1 - .../packages/user/user/modals/user-mfa/user-mfa-modal.element.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.element.ts index c2a2f04bd0..ae3d0ba65b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.element.ts @@ -98,7 +98,6 @@ export class UmbCurrentUserMfaModalElement extends UmbLitElement { () => html`

This two-factor provider is enabled -

html`

This two-factor provider is enabled -

Date: Fri, 19 Apr 2024 14:42:57 +0200 Subject: [PATCH 08/45] add missing all manifests endpoint handler --- .../src/mocks/handlers/manifests.handlers.ts | 287 +++++++++--------- 1 file changed, 147 insertions(+), 140 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts index bd7ddaacdb..a7ee3e2aac 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts @@ -3,154 +3,158 @@ const { rest } = window.MockServiceWorker; import type { PackageManifestResponse } from '../../packages/packages/types.js'; import { umbracoPath } from '@umbraco-cms/backoffice/utils'; +const privateManifests: PackageManifestResponse = [ + { + name: 'My Package Name', + version: '1.0.0', + extensions: [ + { + type: 'bundle', + alias: 'My.Package.Bundle', + name: 'My Package Bundle', + js: '/App_Plugins/custom-bundle-package/index.js', + }, + ], + }, + { + name: 'Named Package', + version: '1.0.0', + extensions: [ + { + type: 'section', + alias: 'My.Section.Custom', + name: 'Custom Section', + js: '/App_Plugins/section.js', + elementName: 'my-section-custom', + weight: 1, + meta: { + label: 'Custom', + pathname: 'my-custom', + }, + }, + { + type: 'propertyEditorUi', + alias: 'My.PropertyEditorUI.Custom', + name: 'My Custom Property Editor UI', + js: '/App_Plugins/property-editor.js', + elementName: 'my-property-editor-ui-custom', + meta: { + label: 'My Custom Property', + icon: 'document', + group: 'Common', + propertyEditorSchema: 'Umbraco.TextBox', + }, + }, + ], + }, + { + name: 'Package with an entry point', + extensions: [ + { + type: 'backofficeEntryPoint', + name: 'My Custom Entry Point', + alias: 'My.Entrypoint.Custom', + js: '/App_Plugins/custom-entrypoint.js', + }, + ], + }, + { + name: 'My MFA Package', + extensions: [ + { + type: 'mfaLoginProvider', + alias: 'My.MfaLoginProvider.Custom.Google', + name: 'My Custom Google MFA Provider', + forProviderName: 'Google Authenticator', + }, + { + type: 'mfaLoginProvider', + alias: 'My.MfaLoginProvider.Custom.SMS', + name: 'My Custom SMS MFA Provider', + forProviderName: 'sms', + meta: { + label: 'Setup SMS Verification', + }, + }, + ], + }, + { + name: 'Package with a view', + extensions: [ + { + type: 'packageView', + alias: 'My.PackageView.Custom', + name: 'My Custom Package View', + js: '/App_Plugins/package-view.js', + meta: { + packageName: 'Package with a view', + }, + }, + ], + }, + { + name: 'My MFA Package', + extensions: [ + { + type: 'mfaLoginProvider', + alias: 'My.MfaLoginProvider.Custom', + name: 'My Custom MFA Provider', + forProviderName: 'sms', + meta: { + label: 'Setup SMS Verification', + }, + }, + ], + }, +]; + +const publicManifests: PackageManifestResponse = [ + { + name: 'My Auth Package', + extensions: [ + { + type: 'authProvider', + alias: 'My.AuthProvider.Google', + name: 'My Custom Auth Provider', + forProviderName: 'Umbraco.Google', + meta: { + label: 'Google', + defaultView: { + icon: 'icon-google', + }, + }, + }, + { + type: 'authProvider', + alias: 'My.AuthProvider.Github', + name: 'My Github Auth Provider', + forProviderName: 'Umbraco.Github', + meta: { + label: 'GitHub', + defaultView: { + look: 'primary', + icon: 'icon-github', + color: 'success', + }, + }, + }, + ], + }, +]; + export const manifestDevelopmentHandlers = [ rest.get(umbracoPath('/manifest/manifest/private'), (_req, res, ctx) => { return res( // Respond with a 200 status code ctx.status(200), - ctx.json([ - { - name: 'My Package Name', - version: '1.0.0', - extensions: [ - { - type: 'bundle', - alias: 'My.Package.Bundle', - name: 'My Package Bundle', - js: '/App_Plugins/custom-bundle-package/index.js', - }, - ], - }, - { - name: 'Named Package', - version: '1.0.0', - extensions: [ - { - type: 'section', - alias: 'My.Section.Custom', - name: 'Custom Section', - js: '/App_Plugins/section.js', - elementName: 'my-section-custom', - weight: 1, - meta: { - label: 'Custom', - pathname: 'my-custom', - }, - }, - { - type: 'propertyEditorUi', - alias: 'My.PropertyEditorUI.Custom', - name: 'My Custom Property Editor UI', - js: '/App_Plugins/property-editor.js', - elementName: 'my-property-editor-ui-custom', - meta: { - label: 'My Custom Property', - icon: 'document', - group: 'Common', - propertyEditorSchema: 'Umbraco.TextBox', - }, - }, - ], - }, - { - name: 'Package with an entry point', - extensions: [ - { - type: 'backofficeEntryPoint', - name: 'My Custom Entry Point', - alias: 'My.Entrypoint.Custom', - js: '/App_Plugins/custom-entrypoint.js', - }, - ], - }, - { - name: 'My MFA Package', - extensions: [ - { - type: 'mfaLoginProvider', - alias: 'My.MfaLoginProvider.Custom.Google', - name: 'My Custom Google MFA Provider', - forProviderName: 'Google Authenticator', - }, - { - type: 'mfaLoginProvider', - alias: 'My.MfaLoginProvider.Custom.SMS', - name: 'My Custom SMS MFA Provider', - forProviderName: 'sms', - meta: { - label: 'Setup SMS Verification', - }, - }, - ], - }, - { - name: 'Package with a view', - extensions: [ - { - type: 'packageView', - alias: 'My.PackageView.Custom', - name: 'My Custom Package View', - js: '/App_Plugins/package-view.js', - meta: { - packageName: 'Package with a view', - }, - }, - ], - }, - { - name: 'My MFA Package', - extensions: [ - { - type: 'mfaLoginProvider', - alias: 'My.MfaLoginProvider.Custom', - name: 'My Custom MFA Provider', - forProviderName: 'sms', - meta: { - label: 'Setup SMS Verification', - }, - }, - ], - }, - ]), + ctx.json(privateManifests), ); }), rest.get(umbracoPath('/manifest/manifest/public'), (_req, res, ctx) => { - return res( - ctx.status(200), - ctx.json([ - { - name: 'My Auth Package', - extensions: [ - { - type: 'authProvider', - alias: 'My.AuthProvider.Google', - name: 'My Custom Auth Provider', - forProviderName: 'Umbraco.Google', - meta: { - label: 'Google', - defaultView: { - icon: 'icon-google', - }, - }, - }, - { - type: 'authProvider', - alias: 'My.AuthProvider.Github', - name: 'My Github Auth Provider', - forProviderName: 'Umbraco.Github', - meta: { - label: 'GitHub', - defaultView: { - look: 'primary', - icon: 'icon-github', - color: 'success', - }, - }, - }, - ], - }, - ]), - ); + return res(ctx.status(200), ctx.json(publicManifests)); + }), + rest.get(umbracoPath('/manifest/manifest'), (_req, res, ctx) => { + return res(ctx.status(200), ctx.json([...privateManifests, ...publicManifests])); }), ]; @@ -161,4 +165,7 @@ export const manifestEmptyHandlers = [ rest.get(umbracoPath('/manifest/manifest/public'), (_req, res, ctx) => { return res(ctx.status(200), ctx.json([])); }), + rest.get(umbracoPath('/manifest/manifest'), (_req, res, ctx) => { + return res(ctx.status(200), ctx.json([])); + }), ]; From 394c8d0f76b31c6cca1bc22c60934682133dd06c Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 19 Apr 2024 14:52:45 +0200 Subject: [PATCH 09/45] load only public extensions in storybook --- .../external-login/modals/external-login-modal.stories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.stories.ts index 0a887bba9d..0cb3740eee 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.stories.ts @@ -10,7 +10,7 @@ import './external-login-modal.element.js'; class UmbServerExtensionsHostElement extends UmbLitElement { constructor() { super(); - new UmbServerExtensionRegistrator(this, umbExtensionsRegistry).registerAllExtensions(); + new UmbServerExtensionRegistrator(this, umbExtensionsRegistry).registerPublicExtensions(); } render() { From c623aa13f5451adb0e527f1f7056e5e59664e7dc Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 19 Apr 2024 14:52:56 +0200 Subject: [PATCH 10/45] allow manual linking on providers --- .../src/mocks/handlers/manifests.handlers.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts index a7ee3e2aac..4806994c0d 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts @@ -122,6 +122,9 @@ const publicManifests: PackageManifestResponse = [ defaultView: { icon: 'icon-google', }, + linking: { + allowManualLinking: true, + }, }, }, { @@ -136,6 +139,9 @@ const publicManifests: PackageManifestResponse = [ icon: 'icon-github', color: 'success', }, + linking: { + allowManualLinking: true, + }, }, }, ], From d63aaee94b2d03b0cc918768a9c090353b1c8dec Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 19 Apr 2024 14:53:06 +0200 Subject: [PATCH 11/45] add missing endpoint for current user logins --- .../src/mocks/handlers/user/current.handlers.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/current.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/current.handlers.ts index 248e9e92a8..3eff4ac51d 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/current.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/current.handlers.ts @@ -1,5 +1,6 @@ const { rest } = window.MockServiceWorker; import { umbUserMockDb } from '../../data/user/user.db.js'; +import type { LinkedLoginsRequestModel } from '@umbraco-cms/backoffice/external/backend-api'; import { UMB_SLUG } from './slug.js'; import { umbracoPath } from '@umbraco-cms/backoffice/utils'; @@ -8,6 +9,19 @@ export const handlers = [ const loggedInUser = umbUserMockDb.getCurrentUser(); return res(ctx.status(200), ctx.json(loggedInUser)); }), + rest.get(umbracoPath(`${UMB_SLUG}/current/logins`), (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + linkedLogins: [ + { + providerKey: 'google', + providerName: 'Umbraco.Google', + }, + ], + }), + ); + }), rest.get(umbracoPath(`${UMB_SLUG}/current/2fa`), (_req, res, ctx) => { const mfaLoginProviders = umbUserMockDb.getMfaLoginProviders(); return res(ctx.status(200), ctx.json(mfaLoginProviders)); From 7112b35fca47a76ac5cdba49f0db6e686fdf6a9c Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 19 Apr 2024 14:53:19 +0200 Subject: [PATCH 12/45] sort imports --- .../src/mocks/handlers/user/current.handlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/current.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/current.handlers.ts index 3eff4ac51d..9454d23c56 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/current.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/current.handlers.ts @@ -1,7 +1,7 @@ const { rest } = window.MockServiceWorker; import { umbUserMockDb } from '../../data/user/user.db.js'; -import type { LinkedLoginsRequestModel } from '@umbraco-cms/backoffice/external/backend-api'; import { UMB_SLUG } from './slug.js'; +import type { LinkedLoginsRequestModel } from '@umbraco-cms/backoffice/external/backend-api'; import { umbracoPath } from '@umbraco-cms/backoffice/utils'; export const handlers = [ From e7d2960e6b186a3f2e6ce856257ee9f024189726 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 19 Apr 2024 15:28:58 +0200 Subject: [PATCH 13/45] remove duplicate label localization --- .../external-login/modals/external-login-modal.element.ts | 4 +--- .../modals/current-user-mfa/current-user-mfa-modal.element.ts | 4 +--- .../user/user/modals/user-mfa/user-mfa-modal.element.ts | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts index 1c2bd7e203..9cc289f702 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts @@ -84,9 +84,7 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { )}
- - ${this.localize.term('general_close')} - +
`; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.element.ts index ae3d0ba65b..ffa32541ae 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.element.ts @@ -79,9 +79,7 @@ export class UmbCurrentUserMfaModalElement extends UmbLitElement { )}
- - ${this.localize.term('general_close')} - +
`; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-mfa/user-mfa-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-mfa/user-mfa-modal.element.ts index 9f49fd2da0..c10e921421 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-mfa/user-mfa-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-mfa/user-mfa-modal.element.ts @@ -79,9 +79,7 @@ export class UmbUserMfaModalElement extends UmbLitElement { )}
- - ${this.localize.term('general_close')} - +
`; From 16c55a3cdabe23f872f78a1ec965ab8580573d87 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 19 Apr 2024 15:32:50 +0200 Subject: [PATCH 14/45] add popout icon --- .../core/icon-registry/icon-dictionary.json | 4 ++++ .../icon-registry/icons/icon-window-popout.js | 16 ++++++++++++++++ .../packages/core/icon-registry/icons/icons.json | 2 +- 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-window-popout.js diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-dictionary.json b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-dictionary.json index b573765d43..1306241f52 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-dictionary.json +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-dictionary.json @@ -2404,6 +2404,10 @@ "name": "icon-window-popin", "file": "square-arrow-down-left.svg" }, + { + "name": "icon-window-popout", + "file": "square-arrow-up-right.svg" + }, { "name": "icon-window-sizes", "file": "scaling.svg" diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-window-popout.js b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-window-popout.js new file mode 100644 index 0000000000..16eeec336b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-window-popout.js @@ -0,0 +1,16 @@ +export default ` + + + + + +`; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icons.json b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icons.json index 70d9dce5be..18d343a1dc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icons.json +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icons.json @@ -1 +1 @@ -[{"name":"icon-activity","path":"./icons/icon-activity.js"},{"name":"icon-add","path":"./icons/icon-add.js"},{"name":"icon-addressbook","path":"./icons/icon-addressbook.js"},{"name":"icon-alarm-clock","path":"./icons/icon-alarm-clock.js"},{"name":"icon-alert-alt","path":"./icons/icon-alert-alt.js"},{"name":"icon-alert","path":"./icons/icon-alert.js"},{"name":"icon-alt","path":"./icons/icon-alt.js"},{"name":"icon-anchor","path":"./icons/icon-anchor.js"},{"name":"icon-app","path":"./icons/icon-app.js"},{"name":"icon-application-error","path":"./icons/icon-application-error.js"},{"name":"icon-application-window-alt","path":"./icons/icon-application-window-alt.js"},{"name":"icon-application-window","path":"./icons/icon-application-window.js"},{"name":"icon-arrivals","path":"./icons/icon-arrivals.js"},{"name":"icon-arrow-down","path":"./icons/icon-arrow-down.js"},{"name":"icon-arrow-left","path":"./icons/icon-arrow-left.js"},{"name":"icon-arrow-right","path":"./icons/icon-arrow-right.js"},{"name":"icon-arrow-up","path":"./icons/icon-arrow-up.js"},{"name":"icon-attachment","path":"./icons/icon-attachment.js"},{"name":"icon-autofill","path":"./icons/icon-autofill.js"},{"name":"icon-award","path":"./icons/icon-award.js"},{"name":"icon-axis-rotation-2","path":"./icons/icon-axis-rotation-2.js"},{"name":"icon-axis-rotation-3","path":"./icons/icon-axis-rotation-3.js"},{"name":"icon-axis-rotation","path":"./icons/icon-axis-rotation.js"},{"name":"icon-backspace","path":"./icons/icon-backspace.js"},{"name":"icon-badge-add","path":"./icons/icon-badge-add.js"},{"name":"icon-badge-remove","path":"./icons/icon-badge-remove.js"},{"name":"icon-badge-restricted","legacy":true,"path":"./icons/icon-badge-restricted.js"},{"name":"icon-ball","path":"./icons/icon-ball.js"},{"name":"icon-bar-chart","path":"./icons/icon-bar-chart.js"},{"name":"icon-barcode","path":"./icons/icon-barcode.js"},{"name":"icon-bars","path":"./icons/icon-bars.js"},{"name":"icon-battery-full","path":"./icons/icon-battery-full.js"},{"name":"icon-battery-low","path":"./icons/icon-battery-low.js"},{"name":"icon-beer-glass","path":"./icons/icon-beer-glass.js"},{"name":"icon-bell-off","path":"./icons/icon-bell-off.js"},{"name":"icon-bell","path":"./icons/icon-bell.js"},{"name":"icon-binarycode","path":"./icons/icon-binarycode.js"},{"name":"icon-bird","path":"./icons/icon-bird.js"},{"name":"icon-birthday-cake","path":"./icons/icon-birthday-cake.js"},{"name":"icon-block","path":"./icons/icon-block.js"},{"name":"icon-bluetooth","path":"./icons/icon-bluetooth.js"},{"name":"icon-boat-shipping","path":"./icons/icon-boat-shipping.js"},{"name":"icon-bones","path":"./icons/icon-bones.js"},{"name":"icon-book-alt-2","path":"./icons/icon-book-alt-2.js"},{"name":"icon-book-alt","path":"./icons/icon-book-alt.js"},{"name":"icon-book","path":"./icons/icon-book.js"},{"name":"icon-bookmark","path":"./icons/icon-bookmark.js"},{"name":"icon-books","path":"./icons/icon-books.js"},{"name":"icon-box-alt","path":"./icons/icon-box-alt.js"},{"name":"icon-box-open","path":"./icons/icon-box-open.js"},{"name":"icon-box","path":"./icons/icon-box.js"},{"name":"icon-brackets","path":"./icons/icon-brackets.js"},{"name":"icon-brick","path":"./icons/icon-brick.js"},{"name":"icon-briefcase","path":"./icons/icon-briefcase.js"},{"name":"icon-browser-window","path":"./icons/icon-browser-window.js"},{"name":"icon-brush-alt-2","path":"./icons/icon-brush-alt-2.js"},{"name":"icon-brush-alt","path":"./icons/icon-brush-alt.js"},{"name":"icon-brush","path":"./icons/icon-brush.js"},{"name":"icon-bug","path":"./icons/icon-bug.js"},{"name":"icon-bulleted-list","path":"./icons/icon-bulleted-list.js"},{"name":"icon-burn","path":"./icons/icon-burn.js"},{"name":"icon-bus","path":"./icons/icon-bus.js"},{"name":"icon-calculator","path":"./icons/icon-calculator.js"},{"name":"icon-calendar-alt","path":"./icons/icon-calendar-alt.js"},{"name":"icon-calendar","path":"./icons/icon-calendar.js"},{"name":"icon-camcorder","legacy":true,"path":"./icons/icon-camcorder.js"},{"name":"icon-camera-roll","path":"./icons/icon-camera-roll.js"},{"name":"icon-candy","path":"./icons/icon-candy.js"},{"name":"icon-caps-lock","path":"./icons/icon-caps-lock.js"},{"name":"icon-car","path":"./icons/icon-car.js"},{"name":"icon-categories","path":"./icons/icon-categories.js"},{"name":"icon-certificate","path":"./icons/icon-certificate.js"},{"name":"icon-chart-curve","path":"./icons/icon-chart-curve.js"},{"name":"icon-chart","path":"./icons/icon-chart.js"},{"name":"icon-chat-active","legacy":true,"path":"./icons/icon-chat-active.js"},{"name":"icon-chat","path":"./icons/icon-chat.js"},{"name":"icon-check","path":"./icons/icon-check.js"},{"name":"icon-checkbox-dotted","path":"./icons/icon-checkbox-dotted.js"},{"name":"icon-checkbox-empty","legacy":true,"path":"./icons/icon-checkbox-empty.js"},{"name":"icon-checkbox","path":"./icons/icon-checkbox.js"},{"name":"icon-chip-alt","legacy":true,"path":"./icons/icon-chip-alt.js"},{"name":"icon-chip","path":"./icons/icon-chip.js"},{"name":"icon-cinema","path":"./icons/icon-cinema.js"},{"name":"icon-circle-dotted-active","path":"./icons/icon-circle-dotted-active.js"},{"name":"icon-circle-dotted","path":"./icons/icon-circle-dotted.js"},{"name":"icon-circuits","path":"./icons/icon-circuits.js"},{"name":"icon-client","legacy":true,"path":"./icons/icon-client.js"},{"name":"icon-cloud-drive","path":"./icons/icon-cloud-drive.js"},{"name":"icon-cloud-upload","path":"./icons/icon-cloud-upload.js"},{"name":"icon-cloud","path":"./icons/icon-cloud.js"},{"name":"icon-cloudy","path":"./icons/icon-cloudy.js"},{"name":"icon-clubs","path":"./icons/icon-clubs.js"},{"name":"icon-cocktail","path":"./icons/icon-cocktail.js"},{"name":"icon-code","path":"./icons/icon-code.js"},{"name":"icon-coffee","path":"./icons/icon-coffee.js"},{"name":"icon-coin-dollar","path":"./icons/icon-coin-dollar.js"},{"name":"icon-coin-euro","path":"./icons/icon-coin-euro.js"},{"name":"icon-coin-pound","path":"./icons/icon-coin-pound.js"},{"name":"icon-coin-yen","path":"./icons/icon-coin-yen.js"},{"name":"icon-coins-alt","legacy":true,"path":"./icons/icon-coins-alt.js"},{"name":"icon-coins","path":"./icons/icon-coins.js"},{"name":"icon-color-bucket","path":"./icons/icon-color-bucket.js"},{"name":"icon-colorpicker","path":"./icons/icon-colorpicker.js"},{"name":"icon-columns","path":"./icons/icon-columns.js"},{"name":"icon-combination-lock-open","path":"./icons/icon-combination-lock-open.js"},{"name":"icon-combination-lock","path":"./icons/icon-combination-lock.js"},{"name":"icon-command","path":"./icons/icon-command.js"},{"name":"icon-company","path":"./icons/icon-company.js"},{"name":"icon-compress","path":"./icons/icon-compress.js"},{"name":"icon-connection","path":"./icons/icon-connection.js"},{"name":"icon-console","path":"./icons/icon-console.js"},{"name":"icon-contrast","path":"./icons/icon-contrast.js"},{"name":"icon-conversation-alt","path":"./icons/icon-conversation-alt.js"},{"name":"icon-conversation","legacy":true,"path":"./icons/icon-conversation.js"},{"name":"icon-coverflow","path":"./icons/icon-coverflow.js"},{"name":"icon-credit-card-alt","legacy":true,"path":"./icons/icon-credit-card-alt.js"},{"name":"icon-credit-card","path":"./icons/icon-credit-card.js"},{"name":"icon-crop","path":"./icons/icon-crop.js"},{"name":"icon-crosshair","path":"./icons/icon-crosshair.js"},{"name":"icon-crown-alt","legacy":true,"path":"./icons/icon-crown-alt.js"},{"name":"icon-crown","path":"./icons/icon-crown.js"},{"name":"icon-cupcake","legacy":true,"path":"./icons/icon-cupcake.js"},{"name":"icon-curve","path":"./icons/icon-curve.js"},{"name":"icon-cut","path":"./icons/icon-cut.js"},{"name":"icon-dashboard","path":"./icons/icon-dashboard.js"},{"name":"icon-defrag","path":"./icons/icon-defrag.js"},{"name":"icon-delete-key","path":"./icons/icon-delete-key.js"},{"name":"icon-delete","path":"./icons/icon-delete.js"},{"name":"icon-departure","path":"./icons/icon-departure.js"},{"name":"icon-desktop","legacy":true,"path":"./icons/icon-desktop.js"},{"name":"icon-diagnostics","path":"./icons/icon-diagnostics.js"},{"name":"icon-diagonal-arrow-alt","path":"./icons/icon-diagonal-arrow-alt.js"},{"name":"icon-diagonal-arrow","path":"./icons/icon-diagonal-arrow.js"},{"name":"icon-diamond","path":"./icons/icon-diamond.js"},{"name":"icon-diamonds","path":"./icons/icon-diamonds.js"},{"name":"icon-dice","path":"./icons/icon-dice.js"},{"name":"icon-diploma-alt","legacy":true,"path":"./icons/icon-diploma-alt.js"},{"name":"icon-diploma","path":"./icons/icon-diploma.js"},{"name":"icon-directions-alt","path":"./icons/icon-directions-alt.js"},{"name":"icon-directions","path":"./icons/icon-directions.js"},{"name":"icon-disc","path":"./icons/icon-disc.js"},{"name":"icon-disk-image","path":"./icons/icon-disk-image.js"},{"name":"icon-display","path":"./icons/icon-display.js"},{"name":"icon-dna","path":"./icons/icon-dna.js"},{"name":"icon-dock-connector","path":"./icons/icon-dock-connector.js"},{"name":"icon-document-dashed-line","path":"./icons/icon-document-dashed-line.js"},{"name":"icon-document","path":"./icons/icon-document.js"},{"name":"icon-documents","path":"./icons/icon-documents.js"},{"name":"icon-donate","legacy":true,"path":"./icons/icon-donate.js"},{"name":"icon-door-open-alt","legacy":true,"path":"./icons/icon-door-open-alt.js"},{"name":"icon-door-open","path":"./icons/icon-door-open.js"},{"name":"icon-download-alt","path":"./icons/icon-download-alt.js"},{"name":"icon-download","path":"./icons/icon-download.js"},{"name":"icon-drop","path":"./icons/icon-drop.js"},{"name":"icon-eco","path":"./icons/icon-eco.js"},{"name":"icon-economy","legacy":true,"path":"./icons/icon-economy.js"},{"name":"icon-edit","path":"./icons/icon-edit.js"},{"name":"icon-employee","legacy":true,"path":"./icons/icon-employee.js"},{"name":"icon-energy-saving-bulb","path":"./icons/icon-energy-saving-bulb.js"},{"name":"icon-enter","path":"./icons/icon-enter.js"},{"name":"icon-equalizer","path":"./icons/icon-equalizer.js"},{"name":"icon-escape","path":"./icons/icon-escape.js"},{"name":"icon-ethernet","path":"./icons/icon-ethernet.js"},{"name":"icon-eye","path":"./icons/icon-eye.js"},{"name":"icon-exit-fullscreen","path":"./icons/icon-exit-fullscreen.js"},{"name":"icon-facebook-like","path":"./icons/icon-facebook-like.js"},{"name":"icon-factory","path":"./icons/icon-factory.js"},{"name":"icon-favorite","path":"./icons/icon-favorite.js"},{"name":"icon-file-cabinet","path":"./icons/icon-file-cabinet.js"},{"name":"icon-files","path":"./icons/icon-files.js"},{"name":"icon-filter-arrows","path":"./icons/icon-filter-arrows.js"},{"name":"icon-filter","path":"./icons/icon-filter.js"},{"name":"icon-fingerprint","path":"./icons/icon-fingerprint.js"},{"name":"icon-fire","path":"./icons/icon-fire.js"},{"name":"icon-firewire","legacy":true,"path":"./icons/icon-firewire.js"},{"name":"icon-flag-alt","path":"./icons/icon-flag-alt.js"},{"name":"icon-flag","path":"./icons/icon-flag.js"},{"name":"icon-flash","path":"./icons/icon-flash.js"},{"name":"icon-flashlight","path":"./icons/icon-flashlight.js"},{"name":"icon-flowerpot","path":"./icons/icon-flowerpot.js"},{"name":"icon-folder","path":"./icons/icon-folder.js"},{"name":"icon-folders","path":"./icons/icon-folders.js"},{"name":"icon-font","path":"./icons/icon-font.js"},{"name":"icon-food","path":"./icons/icon-food.js"},{"name":"icon-footprints","path":"./icons/icon-footprints.js"},{"name":"icon-forking","path":"./icons/icon-forking.js"},{"name":"icon-frame-alt","legacy":true,"path":"./icons/icon-frame-alt.js"},{"name":"icon-frame","path":"./icons/icon-frame.js"},{"name":"icon-fullscreen-alt","path":"./icons/icon-fullscreen-alt.js"},{"name":"icon-fullscreen","path":"./icons/icon-fullscreen.js"},{"name":"icon-game","path":"./icons/icon-game.js"},{"name":"icon-geometry","legacy":true,"path":"./icons/icon-geometry.js"},{"name":"icon-gift","path":"./icons/icon-gift.js"},{"name":"icon-glasses","path":"./icons/icon-glasses.js"},{"name":"icon-globe-alt","path":"./icons/icon-globe-alt.js"},{"name":"icon-globe-asia","legacy":true,"path":"./icons/icon-globe-asia.js"},{"name":"icon-globe-europe-africa","legacy":true,"path":"./icons/icon-globe-europe-africa.js"},{"name":"icon-globe-inverted-america","legacy":true,"path":"./icons/icon-globe-inverted-america.js"},{"name":"icon-globe-inverted-asia","legacy":true,"path":"./icons/icon-globe-inverted-asia.js"},{"name":"icon-globe-inverted-europe-africa","legacy":true,"path":"./icons/icon-globe-inverted-europe-africa.js"},{"name":"icon-globe","path":"./icons/icon-globe.js"},{"name":"icon-gps","path":"./icons/icon-gps.js"},{"name":"icon-graduate","path":"./icons/icon-graduate.js"},{"name":"icon-grid","path":"./icons/icon-grid.js"},{"name":"icon-hammer","path":"./icons/icon-hammer.js"},{"name":"icon-hand-active-alt","legacy":true,"path":"./icons/icon-hand-active-alt.js"},{"name":"icon-hand-active","path":"./icons/icon-hand-active.js"},{"name":"icon-hand-pointer-alt","legacy":true,"path":"./icons/icon-hand-pointer-alt.js"},{"name":"icon-hand-pointer","path":"./icons/icon-hand-pointer.js"},{"name":"icon-handshake","path":"./icons/icon-handshake.js"},{"name":"icon-handtool-alt","legacy":true,"path":"./icons/icon-handtool-alt.js"},{"name":"icon-handtool","path":"./icons/icon-handtool.js"},{"name":"icon-hard-drive-alt","legacy":true,"path":"./icons/icon-hard-drive-alt.js"},{"name":"icon-hard-drive","legacy":true,"path":"./icons/icon-hard-drive.js"},{"name":"icon-headphones","path":"./icons/icon-headphones.js"},{"name":"icon-headset","legacy":true,"path":"./icons/icon-headset.js"},{"name":"icon-hearts","path":"./icons/icon-hearts.js"},{"name":"icon-height","path":"./icons/icon-height.js"},{"name":"icon-help-alt","path":"./icons/icon-help-alt.js"},{"name":"icon-help","path":"./icons/icon-help.js"},{"name":"icon-home","path":"./icons/icon-home.js"},{"name":"icon-hourglass","path":"./icons/icon-hourglass.js"},{"name":"icon-imac","legacy":true,"path":"./icons/icon-imac.js"},{"name":"icon-inbox-full","legacy":true,"path":"./icons/icon-inbox-full.js"},{"name":"icon-inbox","path":"./icons/icon-inbox.js"},{"name":"icon-indent","path":"./icons/icon-indent.js"},{"name":"icon-infinity","path":"./icons/icon-infinity.js"},{"name":"icon-info","path":"./icons/icon-info.js"},{"name":"icon-invoice","legacy":true,"path":"./icons/icon-invoice.js"},{"name":"icon-ipad","legacy":true,"path":"./icons/icon-ipad.js"},{"name":"icon-iphone","legacy":true,"path":"./icons/icon-iphone.js"},{"name":"icon-item-arrangement","legacy":true,"path":"./icons/icon-item-arrangement.js"},{"name":"icon-junk","path":"./icons/icon-junk.js"},{"name":"icon-key","path":"./icons/icon-key.js"},{"name":"icon-keyboard","path":"./icons/icon-keyboard.js"},{"name":"icon-lab","path":"./icons/icon-lab.js"},{"name":"icon-laptop","path":"./icons/icon-laptop.js"},{"name":"icon-layers-alt","legacy":true,"path":"./icons/icon-layers-alt.js"},{"name":"icon-layers","path":"./icons/icon-layers.js"},{"name":"icon-layout","path":"./icons/icon-layout.js"},{"name":"icon-left-double-arrow","path":"./icons/icon-left-double-arrow.js"},{"name":"icon-legal","path":"./icons/icon-legal.js"},{"name":"icon-lense","legacy":true,"path":"./icons/icon-lense.js"},{"name":"icon-library","path":"./icons/icon-library.js"},{"name":"icon-light-down","path":"./icons/icon-light-down.js"},{"name":"icon-light-up","path":"./icons/icon-light-up.js"},{"name":"icon-lightbulb-active","path":"./icons/icon-lightbulb-active.js"},{"name":"icon-lightbulb","legacy":true,"path":"./icons/icon-lightbulb.js"},{"name":"icon-lightning","path":"./icons/icon-lightning.js"},{"name":"icon-link","path":"./icons/icon-link.js"},{"name":"icon-list","path":"./icons/icon-list.js"},{"name":"icon-load","legacy":true,"path":"./icons/icon-load.js"},{"name":"icon-loading","legacy":true,"path":"./icons/icon-loading.js"},{"name":"icon-locate","path":"./icons/icon-locate.js"},{"name":"icon-location-near-me","legacy":true,"path":"./icons/icon-location-near-me.js"},{"name":"icon-location-nearby","path":"./icons/icon-location-nearby.js"},{"name":"icon-lock","path":"./icons/icon-lock.js"},{"name":"icon-log-out","path":"./icons/icon-log-out.js"},{"name":"icon-logout","legacy":true,"path":"./icons/icon-logout.js"},{"name":"icon-loupe","legacy":true,"path":"./icons/icon-loupe.js"},{"name":"icon-magnet","path":"./icons/icon-magnet.js"},{"name":"icon-mailbox","path":"./icons/icon-mailbox.js"},{"name":"icon-map-alt","path":"./icons/icon-map-alt.js"},{"name":"icon-map-location","legacy":true,"path":"./icons/icon-map-location.js"},{"name":"icon-map-marker","path":"./icons/icon-map-marker.js"},{"name":"icon-map","path":"./icons/icon-map.js"},{"name":"icon-medal","path":"./icons/icon-medal.js"},{"name":"icon-medical-emergency","path":"./icons/icon-medical-emergency.js"},{"name":"icon-medicine","path":"./icons/icon-medicine.js"},{"name":"icon-meeting","legacy":true,"path":"./icons/icon-meeting.js"},{"name":"icon-megaphone","path":"./icons/icon-megaphone.js"},{"name":"icon-merge","path":"./icons/icon-merge.js"},{"name":"icon-message-open","path":"./icons/icon-message-open.js"},{"name":"icon-message-unopened","legacy":true,"path":"./icons/icon-message-unopened.js"},{"name":"icon-message","path":"./icons/icon-message.js"},{"name":"icon-microscope","path":"./icons/icon-microscope.js"},{"name":"icon-mindmap","legacy":true,"path":"./icons/icon-mindmap.js"},{"name":"icon-mobile","path":"./icons/icon-mobile.js"},{"name":"icon-mountain","path":"./icons/icon-mountain.js"},{"name":"icon-mouse-cursor","path":"./icons/icon-mouse-cursor.js"},{"name":"icon-mouse","path":"./icons/icon-mouse.js"},{"name":"icon-movie-alt","path":"./icons/icon-movie-alt.js"},{"name":"icon-movie","path":"./icons/icon-movie.js"},{"name":"icon-multiple-credit-cards","path":"./icons/icon-multiple-credit-cards.js"},{"name":"icon-multiple-windows","path":"./icons/icon-multiple-windows.js"},{"name":"icon-music","path":"./icons/icon-music.js"},{"name":"icon-name-badge","legacy":true,"path":"./icons/icon-name-badge.js"},{"name":"icon-navigation-bottom","legacy":true,"path":"./icons/icon-navigation-bottom.js"},{"name":"icon-navigation-down","legacy":true,"path":"./icons/icon-navigation-down.js"},{"name":"icon-navigation-first","legacy":true,"path":"./icons/icon-navigation-first.js"},{"name":"icon-navigation-horizontal","legacy":true,"path":"./icons/icon-navigation-horizontal.js"},{"name":"icon-navigation-last","legacy":true,"path":"./icons/icon-navigation-last.js"},{"name":"icon-navigation-left","legacy":true,"path":"./icons/icon-navigation-left.js"},{"name":"icon-navigation-right","legacy":true,"path":"./icons/icon-navigation-right.js"},{"name":"icon-navigation-road","legacy":true,"path":"./icons/icon-navigation-road.js"},{"name":"icon-navigation-top","legacy":true,"path":"./icons/icon-navigation-top.js"},{"name":"icon-navigation-up","legacy":true,"path":"./icons/icon-navigation-up.js"},{"name":"icon-navigation-vertical","legacy":true,"path":"./icons/icon-navigation-vertical.js"},{"name":"icon-navigation","legacy":true,"path":"./icons/icon-navigation.js"},{"name":"icon-navigational-arrow","path":"./icons/icon-navigational-arrow.js"},{"name":"icon-network-alt","path":"./icons/icon-network-alt.js"},{"name":"icon-newspaper-alt","legacy":true,"path":"./icons/icon-newspaper-alt.js"},{"name":"icon-newspaper","path":"./icons/icon-newspaper.js"},{"name":"icon-next-media","legacy":true,"path":"./icons/icon-next-media.js"},{"name":"icon-next","legacy":true,"path":"./icons/icon-next.js"},{"name":"icon-nodes","legacy":true,"path":"./icons/icon-nodes.js"},{"name":"icon-notepad-alt","legacy":true,"path":"./icons/icon-notepad-alt.js"},{"name":"icon-notepad","path":"./icons/icon-notepad.js"},{"name":"icon-old-key","path":"./icons/icon-old-key.js"},{"name":"icon-old-phone","legacy":true,"path":"./icons/icon-old-phone.js"},{"name":"icon-operator","path":"./icons/icon-operator.js"},{"name":"icon-ordered-list","path":"./icons/icon-ordered-list.js"},{"name":"icon-out","path":"./icons/icon-out.js"},{"name":"icon-outbox","legacy":true,"path":"./icons/icon-outbox.js"},{"name":"icon-outdent","path":"./icons/icon-outdent.js"},{"name":"icon-page-add","path":"./icons/icon-page-add.js"},{"name":"icon-page-down","path":"./icons/icon-page-down.js"},{"name":"icon-page-remove","path":"./icons/icon-page-remove.js"},{"name":"icon-page-restricted","path":"./icons/icon-page-restricted.js"},{"name":"icon-page-up","path":"./icons/icon-page-up.js"},{"name":"icon-paint-roller","legacy":true,"path":"./icons/icon-paint-roller.js"},{"name":"icon-palette","path":"./icons/icon-palette.js"},{"name":"icon-panel-show","path":"./icons/icon-panel-show.js"},{"name":"icon-pannel-close","path":"./icons/icon-pannel-close.js"},{"name":"icon-paper-bag","legacy":true,"path":"./icons/icon-paper-bag.js"},{"name":"icon-paper-plane-alt","path":"./icons/icon-paper-plane-alt.js"},{"name":"icon-paper-plane","path":"./icons/icon-paper-plane.js"},{"name":"icon-partly-cloudy","path":"./icons/icon-partly-cloudy.js"},{"name":"icon-paste-in","legacy":true,"path":"./icons/icon-paste-in.js"},{"name":"icon-pause","path":"./icons/icon-pause.js"},{"name":"icon-pc","legacy":true,"path":"./icons/icon-pc.js"},{"name":"icon-people-alt-2","legacy":true,"path":"./icons/icon-people-alt-2.js"},{"name":"icon-people-alt","legacy":true,"path":"./icons/icon-people-alt.js"},{"name":"icon-people-female","legacy":true,"path":"./icons/icon-people-female.js"},{"name":"icon-people","path":"./icons/icon-people.js"},{"name":"icon-phone-ring","path":"./icons/icon-phone-ring.js"},{"name":"icon-phone","path":"./icons/icon-phone.js"},{"name":"icon-photo-album","path":"./icons/icon-photo-album.js"},{"name":"icon-picture","path":"./icons/icon-picture.js"},{"name":"icon-pictures-alt-2","path":"./icons/icon-pictures-alt-2.js"},{"name":"icon-pictures-alt","legacy":true,"path":"./icons/icon-pictures-alt.js"},{"name":"icon-pictures","path":"./icons/icon-pictures.js"},{"name":"icon-pie-chart","path":"./icons/icon-pie-chart.js"},{"name":"icon-piggy-bank","path":"./icons/icon-piggy-bank.js"},{"name":"icon-pin-location","path":"./icons/icon-pin-location.js"},{"name":"icon-plane","path":"./icons/icon-plane.js"},{"name":"icon-planet","legacy":true,"path":"./icons/icon-planet.js"},{"name":"icon-play","path":"./icons/icon-play.js"},{"name":"icon-playing-cards","legacy":true,"path":"./icons/icon-playing-cards.js"},{"name":"icon-playlist","path":"./icons/icon-playlist.js"},{"name":"icon-plugin","path":"./icons/icon-plugin.js"},{"name":"icon-podcast","path":"./icons/icon-podcast.js"},{"name":"icon-poll","legacy":true,"path":"./icons/icon-poll.js"},{"name":"icon-post-it","path":"./icons/icon-post-it.js"},{"name":"icon-power-outlet","legacy":true,"path":"./icons/icon-power-outlet.js"},{"name":"icon-power","path":"./icons/icon-power.js"},{"name":"icon-presentation","path":"./icons/icon-presentation.js"},{"name":"icon-previous-media","path":"./icons/icon-previous-media.js"},{"name":"icon-previous","path":"./icons/icon-previous.js"},{"name":"icon-price-dollar","legacy":true,"path":"./icons/icon-price-dollar.js"},{"name":"icon-price-euro","legacy":true,"path":"./icons/icon-price-euro.js"},{"name":"icon-price-pound","legacy":true,"path":"./icons/icon-price-pound.js"},{"name":"icon-price-yen","legacy":true,"path":"./icons/icon-price-yen.js"},{"name":"icon-print","path":"./icons/icon-print.js"},{"name":"icon-printer-alt","legacy":true,"path":"./icons/icon-printer-alt.js"},{"name":"icon-projector","path":"./icons/icon-projector.js"},{"name":"icon-pulse","path":"./icons/icon-pulse.js"},{"name":"icon-pushpin","path":"./icons/icon-pushpin.js"},{"name":"icon-qr-code","path":"./icons/icon-qr-code.js"},{"name":"icon-quote","path":"./icons/icon-quote.js"},{"name":"icon-radio-alt","path":"./icons/icon-radio-alt.js"},{"name":"icon-radio-receiver","path":"./icons/icon-radio-receiver.js"},{"name":"icon-radio","path":"./icons/icon-radio.js"},{"name":"icon-rain","path":"./icons/icon-rain.js"},{"name":"icon-rate","legacy":true,"path":"./icons/icon-rate.js"},{"name":"icon-re-post","path":"./icons/icon-re-post.js"},{"name":"icon-readonly","legacy":true,"path":"./icons/icon-readonly.js"},{"name":"icon-receipt-alt","path":"./icons/icon-receipt-alt.js"},{"name":"icon-reception","path":"./icons/icon-reception.js"},{"name":"icon-record","legacy":true,"path":"./icons/icon-record.js"},{"name":"icon-rectangle-ellipsis","path":"./icons/icon-rectangle-ellipsis.js"},{"name":"icon-redo","path":"./icons/icon-redo.js"},{"name":"icon-refresh","path":"./icons/icon-refresh.js"},{"name":"icon-remote","legacy":true,"path":"./icons/icon-remote.js"},{"name":"icon-remove","path":"./icons/icon-remove.js"},{"name":"icon-repeat-one","path":"./icons/icon-repeat-one.js"},{"name":"icon-repeat","path":"./icons/icon-repeat.js"},{"name":"icon-reply-arrow","path":"./icons/icon-reply-arrow.js"},{"name":"icon-resize","path":"./icons/icon-resize.js"},{"name":"icon-return-to-top","legacy":true,"path":"./icons/icon-return-to-top.js"},{"name":"icon-right-double-arrow","legacy":true,"path":"./icons/icon-right-double-arrow.js"},{"name":"icon-roadsign","legacy":true,"path":"./icons/icon-roadsign.js"},{"name":"icon-rocket","path":"./icons/icon-rocket.js"},{"name":"icon-rss","path":"./icons/icon-rss.js"},{"name":"icon-ruler-alt","path":"./icons/icon-ruler-alt.js"},{"name":"icon-ruler","path":"./icons/icon-ruler.js"},{"name":"icon-satellite-dish","path":"./icons/icon-satellite-dish.js"},{"name":"icon-save","path":"./icons/icon-save.js"},{"name":"icon-scan","path":"./icons/icon-scan.js"},{"name":"icon-school","path":"./icons/icon-school.js"},{"name":"icon-screensharing","path":"./icons/icon-screensharing.js"},{"name":"icon-script-alt","legacy":true,"path":"./icons/icon-script-alt.js"},{"name":"icon-script","path":"./icons/icon-script.js"},{"name":"icon-scull","path":"./icons/icon-scull.js"},{"name":"icon-search","path":"./icons/icon-search.js"},{"name":"icon-sensor","path":"./icons/icon-sensor.js"},{"name":"icon-server-alt","legacy":true,"path":"./icons/icon-server-alt.js"},{"name":"icon-server","path":"./icons/icon-server.js"},{"name":"icon-settings-alt","legacy":true,"path":"./icons/icon-settings-alt.js"},{"name":"icon-settings","path":"./icons/icon-settings.js"},{"name":"icon-share-alt","path":"./icons/icon-share-alt.js"},{"name":"icon-share","path":"./icons/icon-share.js"},{"name":"icon-sharing-iphone","path":"./icons/icon-sharing-iphone.js"},{"name":"icon-shield","path":"./icons/icon-shield.js"},{"name":"icon-shift","path":"./icons/icon-shift.js"},{"name":"icon-shipping-box","path":"./icons/icon-shipping-box.js"},{"name":"icon-shipping","path":"./icons/icon-shipping.js"},{"name":"icon-shoe","path":"./icons/icon-shoe.js"},{"name":"icon-shopping-basket-alt-2","legacy":true,"path":"./icons/icon-shopping-basket-alt-2.js"},{"name":"icon-shopping-basket-alt","path":"./icons/icon-shopping-basket-alt.js"},{"name":"icon-shopping-basket","path":"./icons/icon-shopping-basket.js"},{"name":"icon-shuffle","path":"./icons/icon-shuffle.js"},{"name":"icon-sience","path":"./icons/icon-sience.js"},{"name":"icon-single-note","path":"./icons/icon-single-note.js"},{"name":"icon-sitemap","legacy":true,"path":"./icons/icon-sitemap.js"},{"name":"icon-sleep","path":"./icons/icon-sleep.js"},{"name":"icon-slideshow","legacy":true,"path":"./icons/icon-slideshow.js"},{"name":"icon-smiley-inverted","legacy":true,"path":"./icons/icon-smiley-inverted.js"},{"name":"icon-smiley","path":"./icons/icon-smiley.js"},{"name":"icon-snow","path":"./icons/icon-snow.js"},{"name":"icon-sound-low","path":"./icons/icon-sound-low.js"},{"name":"icon-sound-medium","legacy":true,"path":"./icons/icon-sound-medium.js"},{"name":"icon-sound-off","path":"./icons/icon-sound-off.js"},{"name":"icon-sound-waves","path":"./icons/icon-sound-waves.js"},{"name":"icon-sound","path":"./icons/icon-sound.js"},{"name":"icon-spades","path":"./icons/icon-spades.js"},{"name":"icon-speaker","path":"./icons/icon-speaker.js"},{"name":"icon-speed-gauge","path":"./icons/icon-speed-gauge.js"},{"name":"icon-split-alt","path":"./icons/icon-split-alt.js"},{"name":"icon-split","path":"./icons/icon-split.js"},{"name":"icon-sprout","path":"./icons/icon-sprout.js"},{"name":"icon-squiggly-line","legacy":true,"path":"./icons/icon-squiggly-line.js"},{"name":"icon-ssd","legacy":true,"path":"./icons/icon-ssd.js"},{"name":"icon-stacked-disks","legacy":true,"path":"./icons/icon-stacked-disks.js"},{"name":"icon-stamp","legacy":true,"path":"./icons/icon-stamp.js"},{"name":"icon-stop-alt","path":"./icons/icon-stop-alt.js"},{"name":"icon-stop-hand","legacy":true,"path":"./icons/icon-stop-hand.js"},{"name":"icon-stop","path":"./icons/icon-stop.js"},{"name":"icon-store","path":"./icons/icon-store.js"},{"name":"icon-stream","legacy":true,"path":"./icons/icon-stream.js"},{"name":"icon-sunny","path":"./icons/icon-sunny.js"},{"name":"icon-sweatshirt","legacy":true,"path":"./icons/icon-sweatshirt.js"},{"name":"icon-sync","path":"./icons/icon-sync.js"},{"name":"icon-t-shirt","path":"./icons/icon-t-shirt.js"},{"name":"icon-tab-key","path":"./icons/icon-tab-key.js"},{"name":"icon-tag","path":"./icons/icon-tag.js"},{"name":"icon-tags","path":"./icons/icon-tags.js"},{"name":"icon-takeaway-cup","legacy":true,"path":"./icons/icon-takeaway-cup.js"},{"name":"icon-target","path":"./icons/icon-target.js"},{"name":"icon-temperatrure-alt","path":"./icons/icon-temperatrure-alt.js"},{"name":"icon-temperature","path":"./icons/icon-temperature.js"},{"name":"icon-terminal","path":"./icons/icon-terminal.js"},{"name":"icon-theater","path":"./icons/icon-theater.js"},{"name":"icon-thumb-down","path":"./icons/icon-thumb-down.js"},{"name":"icon-thumb-up","path":"./icons/icon-thumb-up.js"},{"name":"icon-thumbnail-list","path":"./icons/icon-thumbnail-list.js"},{"name":"icon-thumbnails-small","path":"./icons/icon-thumbnails-small.js"},{"name":"icon-thumbnails","path":"./icons/icon-thumbnails.js"},{"name":"icon-ticket","path":"./icons/icon-ticket.js"},{"name":"icon-time","path":"./icons/icon-time.js"},{"name":"icon-timer","path":"./icons/icon-timer.js"},{"name":"icon-tools","legacy":true,"path":"./icons/icon-tools.js"},{"name":"icon-top","legacy":true,"path":"./icons/icon-top.js"},{"name":"icon-traffic-alt","legacy":true,"path":"./icons/icon-traffic-alt.js"},{"name":"icon-trafic","path":"./icons/icon-trafic.js"},{"name":"icon-train","path":"./icons/icon-train.js"},{"name":"icon-trash-alt-2","legacy":true,"path":"./icons/icon-trash-alt-2.js"},{"name":"icon-trash-alt","legacy":true,"path":"./icons/icon-trash-alt.js"},{"name":"icon-trash","path":"./icons/icon-trash.js"},{"name":"icon-tree","path":"./icons/icon-tree.js"},{"name":"icon-trophy","path":"./icons/icon-trophy.js"},{"name":"icon-truck","path":"./icons/icon-truck.js"},{"name":"icon-tv-old","path":"./icons/icon-tv-old.js"},{"name":"icon-tv","path":"./icons/icon-tv.js"},{"name":"icon-umb-content","legacy":true,"path":"./icons/icon-umb-content.js"},{"name":"icon-umb-developer","legacy":true,"path":"./icons/icon-umb-developer.js"},{"name":"icon-umb-media","legacy":true,"path":"./icons/icon-umb-media.js"},{"name":"icon-umb-settings","legacy":true,"path":"./icons/icon-umb-settings.js"},{"name":"icon-umb-users","legacy":true,"path":"./icons/icon-umb-users.js"},{"name":"icon-umbrella","path":"./icons/icon-umbrella.js"},{"name":"icon-undo","path":"./icons/icon-undo.js"},{"name":"icon-unlocked","path":"./icons/icon-unlocked.js"},{"name":"icon-untitled","legacy":true,"path":"./icons/icon-untitled.js"},{"name":"icon-usb-connector","legacy":true,"path":"./icons/icon-usb-connector.js"},{"name":"icon-usb","path":"./icons/icon-usb.js"},{"name":"icon-user-female","legacy":true,"path":"./icons/icon-user-female.js"},{"name":"icon-user-females-alt","legacy":true,"path":"./icons/icon-user-females-alt.js"},{"name":"icon-user-females","legacy":true,"path":"./icons/icon-user-females.js"},{"name":"icon-user-glasses","legacy":true,"path":"./icons/icon-user-glasses.js"},{"name":"icon-user","path":"./icons/icon-user.js"},{"name":"icon-users-alt","legacy":true,"path":"./icons/icon-users-alt.js"},{"name":"icon-users","path":"./icons/icon-users.js"},{"name":"icon-utilities","path":"./icons/icon-utilities.js"},{"name":"icon-vcard","path":"./icons/icon-vcard.js"},{"name":"icon-video","path":"./icons/icon-video.js"},{"name":"icon-voice","path":"./icons/icon-voice.js"},{"name":"icon-wall-plug","path":"./icons/icon-wall-plug.js"},{"name":"icon-wallet","path":"./icons/icon-wallet.js"},{"name":"icon-wand","path":"./icons/icon-wand.js"},{"name":"icon-webhook","path":"./icons/icon-webhook.js"},{"name":"icon-weight","path":"./icons/icon-weight.js"},{"name":"icon-width","path":"./icons/icon-width.js"},{"name":"icon-wifi","path":"./icons/icon-wifi.js"},{"name":"icon-window-popin","path":"./icons/icon-window-popin.js"},{"name":"icon-window-sizes","path":"./icons/icon-window-sizes.js"},{"name":"icon-wine-glass","path":"./icons/icon-wine-glass.js"},{"name":"icon-wrench","path":"./icons/icon-wrench.js"},{"name":"icon-wrong","path":"./icons/icon-wrong.js"},{"name":"icon-zip","path":"./icons/icon-zip.js"},{"name":"icon-zom-out","legacy":true,"path":"./icons/icon-zom-out.js"},{"name":"icon-zoom-in","path":"./icons/icon-zoom-in.js"},{"name":"icon-zoom-out","path":"./icons/icon-zoom-out.js"},{"name":"icon-star","path":"./icons/icon-star.js"},{"name":"icon-database","path":"./icons/icon-database.js"},{"name":"icon-azure","path":"./icons/icon-azure.js"},{"name":"icon-facebook","path":"./icons/icon-facebook.js"},{"name":"icon-gitbook","path":"./icons/icon-gitbook.js"},{"name":"icon-github","path":"./icons/icon-github.js"},{"name":"icon-gitlab","path":"./icons/icon-gitlab.js"},{"name":"icon-google","path":"./icons/icon-google.js"},{"name":"icon-linkedin","path":"./icons/icon-linkedin.js"},{"name":"icon-mastodon","path":"./icons/icon-mastodon.js"},{"name":"icon-microsoft","path":"./icons/icon-microsoft.js"},{"name":"icon-twitter-x","path":"./icons/icon-twitter-x.js"},{"name":"icon-umbraco","path":"./icons/icon-umbraco.js"},{"name":"icon-art-easel","legacy":true,"path":"./icons/icon-art-easel.js"},{"name":"icon-article","legacy":true,"path":"./icons/icon-article.js"},{"name":"icon-auction-hammer","legacy":true,"path":"./icons/icon-auction-hammer.js"},{"name":"icon-baby-stroller","legacy":true,"path":"./icons/icon-baby-stroller.js"},{"name":"icon-badge-count","legacy":true,"path":"./icons/icon-badge-count.js"},{"name":"icon-band-aid","legacy":true,"path":"./icons/icon-band-aid.js"},{"name":"icon-bill-dollar","legacy":true,"path":"./icons/icon-bill-dollar.js"},{"name":"icon-bill-euro","legacy":true,"path":"./icons/icon-bill-euro.js"},{"name":"icon-bill-pound","legacy":true,"path":"./icons/icon-bill-pound.js"},{"name":"icon-bill-yen","legacy":true,"path":"./icons/icon-bill-yen.js"},{"name":"icon-bill","legacy":true,"path":"./icons/icon-bill.js"},{"name":"icon-billboard","legacy":true,"path":"./icons/icon-billboard.js"},{"name":"icon-bills-dollar","legacy":true,"path":"./icons/icon-bills-dollar.js"},{"name":"icon-bills-euro","legacy":true,"path":"./icons/icon-bills-euro.js"},{"name":"icon-bills-pound","legacy":true,"path":"./icons/icon-bills-pound.js"},{"name":"icon-bills-yen","legacy":true,"path":"./icons/icon-bills-yen.js"},{"name":"icon-bills","legacy":true,"path":"./icons/icon-bills.js"},{"name":"icon-binoculars","legacy":true,"path":"./icons/icon-binoculars.js"},{"name":"icon-blueprint","legacy":true,"path":"./icons/icon-blueprint.js"},{"name":"icon-bomb","legacy":true,"path":"./icons/icon-bomb.js"},{"name":"icon-cash-register","legacy":true,"path":"./icons/icon-cash-register.js"},{"name":"icon-checkbox-dotted-active","legacy":true,"path":"./icons/icon-checkbox-dotted-active.js"},{"name":"icon-chess","legacy":true,"path":"./icons/icon-chess.js"},{"name":"icon-circus","legacy":true,"path":"./icons/icon-circus.js"},{"name":"icon-clothes-hanger","legacy":true,"path":"./icons/icon-clothes-hanger.js"},{"name":"icon-coin","legacy":true,"path":"./icons/icon-coin.js"},{"name":"icon-coins-dollar-alt","legacy":true,"path":"./icons/icon-coins-dollar-alt.js"},{"name":"icon-coins-dollar","legacy":true,"path":"./icons/icon-coins-dollar.js"},{"name":"icon-coins-euro-alt","legacy":true,"path":"./icons/icon-coins-euro-alt.js"},{"name":"icon-coins-euro","legacy":true,"path":"./icons/icon-coins-euro.js"},{"name":"icon-coins-pound-alt","legacy":true,"path":"./icons/icon-coins-pound-alt.js"},{"name":"icon-coins-pound","legacy":true,"path":"./icons/icon-coins-pound.js"},{"name":"icon-coins-yen-alt","legacy":true,"path":"./icons/icon-coins-yen-alt.js"},{"name":"icon-coins-yen","legacy":true,"path":"./icons/icon-coins-yen.js"},{"name":"icon-comb","legacy":true,"path":"./icons/icon-comb.js"},{"name":"icon-desk","legacy":true,"path":"./icons/icon-desk.js"},{"name":"icon-dollar-bag","legacy":true,"path":"./icons/icon-dollar-bag.js"},{"name":"icon-eject","legacy":true,"path":"./icons/icon-eject.js"},{"name":"icon-euro-bag","legacy":true,"path":"./icons/icon-euro-bag.js"},{"name":"icon-female-symbol","legacy":true,"path":"./icons/icon-female-symbol.js"},{"name":"icon-firewall","legacy":true,"path":"./icons/icon-firewall.js"},{"name":"icon-folder-open","legacy":true,"path":"./icons/icon-folder-open.js"},{"name":"icon-folder-outline","legacy":true,"path":"./icons/icon-folder-outline.js"},{"name":"icon-handprint","legacy":true,"path":"./icons/icon-handprint.js"},{"name":"icon-hat","legacy":true,"path":"./icons/icon-hat.js"},{"name":"icon-hd","legacy":true,"path":"./icons/icon-hd.js"},{"name":"icon-inactive-line","legacy":true,"path":"./icons/icon-inactive-line.js"},{"name":"icon-keychain","legacy":true,"path":"./icons/icon-keychain.js"},{"name":"icon-keyhole","legacy":true,"path":"./icons/icon-keyhole.js"},{"name":"icon-linux-tux","legacy":true,"path":"./icons/icon-linux-tux.js"},{"name":"icon-male-and-female","legacy":true,"path":"./icons/icon-male-and-female.js"},{"name":"icon-male-symbol","legacy":true,"path":"./icons/icon-male-symbol.js"},{"name":"icon-molecular-network","legacy":true,"path":"./icons/icon-molecular-network.js"},{"name":"icon-molecular","legacy":true,"path":"./icons/icon-molecular.js"},{"name":"icon-os-x","legacy":true,"path":"./icons/icon-os-x.js"},{"name":"icon-pants","legacy":true,"path":"./icons/icon-pants.js"},{"name":"icon-parachute-drop","legacy":true,"path":"./icons/icon-parachute-drop.js"},{"name":"icon-parental-control","legacy":true,"path":"./icons/icon-parental-control.js"},{"name":"icon-path","legacy":true,"path":"./icons/icon-path.js"},{"name":"icon-piracy","legacy":true,"path":"./icons/icon-piracy.js"},{"name":"icon-poker-chip","legacy":true,"path":"./icons/icon-poker-chip.js"},{"name":"icon-pound-bag","legacy":true,"path":"./icons/icon-pound-bag.js"},{"name":"icon-receipt-dollar","legacy":true,"path":"./icons/icon-receipt-dollar.js"},{"name":"icon-receipt-euro","legacy":true,"path":"./icons/icon-receipt-euro.js"},{"name":"icon-receipt-pound","legacy":true,"path":"./icons/icon-receipt-pound.js"},{"name":"icon-receipt-yen","legacy":true,"path":"./icons/icon-receipt-yen.js"},{"name":"icon-road","legacy":true,"path":"./icons/icon-road.js"},{"name":"icon-safe","legacy":true,"path":"./icons/icon-safe.js"},{"name":"icon-safedial","legacy":true,"path":"./icons/icon-safedial.js"},{"name":"icon-sandbox-toys","legacy":true,"path":"./icons/icon-sandbox-toys.js"},{"name":"icon-security-camera","legacy":true,"path":"./icons/icon-security-camera.js"},{"name":"icon-settings-alt-2","legacy":true,"path":"./icons/icon-settings-alt-2.js"},{"name":"icon-share-alt-2","legacy":true,"path":"./icons/icon-share-alt-2.js"},{"name":"icon-shorts","legacy":true,"path":"./icons/icon-shorts.js"},{"name":"icon-simcard","legacy":true,"path":"./icons/icon-simcard.js"},{"name":"icon-tab","legacy":true,"path":"./icons/icon-tab.js"},{"name":"icon-tactics","legacy":true,"path":"./icons/icon-tactics.js"},{"name":"icon-theif","legacy":true,"path":"./icons/icon-theif.js"},{"name":"icon-thought-bubble","legacy":true,"path":"./icons/icon-thought-bubble.js"},{"name":"icon-twitter","legacy":true,"path":"./icons/icon-twitter.js"},{"name":"icon-umb-contour","legacy":true,"path":"./icons/icon-umb-contour.js"},{"name":"icon-umb-deploy","legacy":true,"path":"./icons/icon-umb-deploy.js"},{"name":"icon-umb-members","legacy":true,"path":"./icons/icon-umb-members.js"},{"name":"icon-universal","legacy":true,"path":"./icons/icon-universal.js"},{"name":"icon-war","legacy":true,"path":"./icons/icon-war.js"},{"name":"icon-windows","legacy":true,"path":"./icons/icon-windows.js"},{"name":"icon-yen-bag","legacy":true,"path":"./icons/icon-yen-bag.js"}] \ No newline at end of file +[{"name":"icon-activity","path":"./icons/icon-activity.js"},{"name":"icon-add","path":"./icons/icon-add.js"},{"name":"icon-addressbook","path":"./icons/icon-addressbook.js"},{"name":"icon-alarm-clock","path":"./icons/icon-alarm-clock.js"},{"name":"icon-alert-alt","path":"./icons/icon-alert-alt.js"},{"name":"icon-alert","path":"./icons/icon-alert.js"},{"name":"icon-alt","path":"./icons/icon-alt.js"},{"name":"icon-anchor","path":"./icons/icon-anchor.js"},{"name":"icon-app","path":"./icons/icon-app.js"},{"name":"icon-application-error","path":"./icons/icon-application-error.js"},{"name":"icon-application-window-alt","path":"./icons/icon-application-window-alt.js"},{"name":"icon-application-window","path":"./icons/icon-application-window.js"},{"name":"icon-arrivals","path":"./icons/icon-arrivals.js"},{"name":"icon-arrow-down","path":"./icons/icon-arrow-down.js"},{"name":"icon-arrow-left","path":"./icons/icon-arrow-left.js"},{"name":"icon-arrow-right","path":"./icons/icon-arrow-right.js"},{"name":"icon-arrow-up","path":"./icons/icon-arrow-up.js"},{"name":"icon-attachment","path":"./icons/icon-attachment.js"},{"name":"icon-autofill","path":"./icons/icon-autofill.js"},{"name":"icon-award","path":"./icons/icon-award.js"},{"name":"icon-axis-rotation-2","path":"./icons/icon-axis-rotation-2.js"},{"name":"icon-axis-rotation-3","path":"./icons/icon-axis-rotation-3.js"},{"name":"icon-axis-rotation","path":"./icons/icon-axis-rotation.js"},{"name":"icon-backspace","path":"./icons/icon-backspace.js"},{"name":"icon-badge-add","path":"./icons/icon-badge-add.js"},{"name":"icon-badge-remove","path":"./icons/icon-badge-remove.js"},{"name":"icon-badge-restricted","legacy":true,"path":"./icons/icon-badge-restricted.js"},{"name":"icon-ball","path":"./icons/icon-ball.js"},{"name":"icon-bar-chart","path":"./icons/icon-bar-chart.js"},{"name":"icon-barcode","path":"./icons/icon-barcode.js"},{"name":"icon-bars","path":"./icons/icon-bars.js"},{"name":"icon-battery-full","path":"./icons/icon-battery-full.js"},{"name":"icon-battery-low","path":"./icons/icon-battery-low.js"},{"name":"icon-beer-glass","path":"./icons/icon-beer-glass.js"},{"name":"icon-bell-off","path":"./icons/icon-bell-off.js"},{"name":"icon-bell","path":"./icons/icon-bell.js"},{"name":"icon-binarycode","path":"./icons/icon-binarycode.js"},{"name":"icon-bird","path":"./icons/icon-bird.js"},{"name":"icon-birthday-cake","path":"./icons/icon-birthday-cake.js"},{"name":"icon-block","path":"./icons/icon-block.js"},{"name":"icon-bluetooth","path":"./icons/icon-bluetooth.js"},{"name":"icon-boat-shipping","path":"./icons/icon-boat-shipping.js"},{"name":"icon-bones","path":"./icons/icon-bones.js"},{"name":"icon-book-alt-2","path":"./icons/icon-book-alt-2.js"},{"name":"icon-book-alt","path":"./icons/icon-book-alt.js"},{"name":"icon-book","path":"./icons/icon-book.js"},{"name":"icon-bookmark","path":"./icons/icon-bookmark.js"},{"name":"icon-books","path":"./icons/icon-books.js"},{"name":"icon-box-alt","path":"./icons/icon-box-alt.js"},{"name":"icon-box-open","path":"./icons/icon-box-open.js"},{"name":"icon-box","path":"./icons/icon-box.js"},{"name":"icon-brackets","path":"./icons/icon-brackets.js"},{"name":"icon-brick","path":"./icons/icon-brick.js"},{"name":"icon-briefcase","path":"./icons/icon-briefcase.js"},{"name":"icon-browser-window","path":"./icons/icon-browser-window.js"},{"name":"icon-brush-alt-2","path":"./icons/icon-brush-alt-2.js"},{"name":"icon-brush-alt","path":"./icons/icon-brush-alt.js"},{"name":"icon-brush","path":"./icons/icon-brush.js"},{"name":"icon-bug","path":"./icons/icon-bug.js"},{"name":"icon-bulleted-list","path":"./icons/icon-bulleted-list.js"},{"name":"icon-burn","path":"./icons/icon-burn.js"},{"name":"icon-bus","path":"./icons/icon-bus.js"},{"name":"icon-calculator","path":"./icons/icon-calculator.js"},{"name":"icon-calendar-alt","path":"./icons/icon-calendar-alt.js"},{"name":"icon-calendar","path":"./icons/icon-calendar.js"},{"name":"icon-camcorder","legacy":true,"path":"./icons/icon-camcorder.js"},{"name":"icon-camera-roll","path":"./icons/icon-camera-roll.js"},{"name":"icon-candy","path":"./icons/icon-candy.js"},{"name":"icon-caps-lock","path":"./icons/icon-caps-lock.js"},{"name":"icon-car","path":"./icons/icon-car.js"},{"name":"icon-categories","path":"./icons/icon-categories.js"},{"name":"icon-certificate","path":"./icons/icon-certificate.js"},{"name":"icon-chart-curve","path":"./icons/icon-chart-curve.js"},{"name":"icon-chart","path":"./icons/icon-chart.js"},{"name":"icon-chat-active","legacy":true,"path":"./icons/icon-chat-active.js"},{"name":"icon-chat","path":"./icons/icon-chat.js"},{"name":"icon-check","path":"./icons/icon-check.js"},{"name":"icon-checkbox-dotted","path":"./icons/icon-checkbox-dotted.js"},{"name":"icon-checkbox-empty","legacy":true,"path":"./icons/icon-checkbox-empty.js"},{"name":"icon-checkbox","path":"./icons/icon-checkbox.js"},{"name":"icon-chip-alt","legacy":true,"path":"./icons/icon-chip-alt.js"},{"name":"icon-chip","path":"./icons/icon-chip.js"},{"name":"icon-cinema","path":"./icons/icon-cinema.js"},{"name":"icon-circle-dotted-active","path":"./icons/icon-circle-dotted-active.js"},{"name":"icon-circle-dotted","path":"./icons/icon-circle-dotted.js"},{"name":"icon-circuits","path":"./icons/icon-circuits.js"},{"name":"icon-client","legacy":true,"path":"./icons/icon-client.js"},{"name":"icon-cloud-drive","path":"./icons/icon-cloud-drive.js"},{"name":"icon-cloud-upload","path":"./icons/icon-cloud-upload.js"},{"name":"icon-cloud","path":"./icons/icon-cloud.js"},{"name":"icon-cloudy","path":"./icons/icon-cloudy.js"},{"name":"icon-clubs","path":"./icons/icon-clubs.js"},{"name":"icon-cocktail","path":"./icons/icon-cocktail.js"},{"name":"icon-code","path":"./icons/icon-code.js"},{"name":"icon-coffee","path":"./icons/icon-coffee.js"},{"name":"icon-coin-dollar","path":"./icons/icon-coin-dollar.js"},{"name":"icon-coin-euro","path":"./icons/icon-coin-euro.js"},{"name":"icon-coin-pound","path":"./icons/icon-coin-pound.js"},{"name":"icon-coin-yen","path":"./icons/icon-coin-yen.js"},{"name":"icon-coins-alt","legacy":true,"path":"./icons/icon-coins-alt.js"},{"name":"icon-coins","path":"./icons/icon-coins.js"},{"name":"icon-color-bucket","path":"./icons/icon-color-bucket.js"},{"name":"icon-colorpicker","path":"./icons/icon-colorpicker.js"},{"name":"icon-columns","path":"./icons/icon-columns.js"},{"name":"icon-combination-lock-open","path":"./icons/icon-combination-lock-open.js"},{"name":"icon-combination-lock","path":"./icons/icon-combination-lock.js"},{"name":"icon-command","path":"./icons/icon-command.js"},{"name":"icon-company","path":"./icons/icon-company.js"},{"name":"icon-compress","path":"./icons/icon-compress.js"},{"name":"icon-connection","path":"./icons/icon-connection.js"},{"name":"icon-console","path":"./icons/icon-console.js"},{"name":"icon-contrast","path":"./icons/icon-contrast.js"},{"name":"icon-conversation-alt","path":"./icons/icon-conversation-alt.js"},{"name":"icon-conversation","legacy":true,"path":"./icons/icon-conversation.js"},{"name":"icon-coverflow","path":"./icons/icon-coverflow.js"},{"name":"icon-credit-card-alt","legacy":true,"path":"./icons/icon-credit-card-alt.js"},{"name":"icon-credit-card","path":"./icons/icon-credit-card.js"},{"name":"icon-crop","path":"./icons/icon-crop.js"},{"name":"icon-crosshair","path":"./icons/icon-crosshair.js"},{"name":"icon-crown-alt","legacy":true,"path":"./icons/icon-crown-alt.js"},{"name":"icon-crown","path":"./icons/icon-crown.js"},{"name":"icon-cupcake","legacy":true,"path":"./icons/icon-cupcake.js"},{"name":"icon-curve","path":"./icons/icon-curve.js"},{"name":"icon-cut","path":"./icons/icon-cut.js"},{"name":"icon-dashboard","path":"./icons/icon-dashboard.js"},{"name":"icon-defrag","path":"./icons/icon-defrag.js"},{"name":"icon-delete-key","path":"./icons/icon-delete-key.js"},{"name":"icon-delete","path":"./icons/icon-delete.js"},{"name":"icon-departure","path":"./icons/icon-departure.js"},{"name":"icon-desktop","legacy":true,"path":"./icons/icon-desktop.js"},{"name":"icon-diagnostics","path":"./icons/icon-diagnostics.js"},{"name":"icon-diagonal-arrow-alt","path":"./icons/icon-diagonal-arrow-alt.js"},{"name":"icon-diagonal-arrow","path":"./icons/icon-diagonal-arrow.js"},{"name":"icon-diamond","path":"./icons/icon-diamond.js"},{"name":"icon-diamonds","path":"./icons/icon-diamonds.js"},{"name":"icon-dice","path":"./icons/icon-dice.js"},{"name":"icon-diploma-alt","legacy":true,"path":"./icons/icon-diploma-alt.js"},{"name":"icon-diploma","path":"./icons/icon-diploma.js"},{"name":"icon-directions-alt","path":"./icons/icon-directions-alt.js"},{"name":"icon-directions","path":"./icons/icon-directions.js"},{"name":"icon-disc","path":"./icons/icon-disc.js"},{"name":"icon-disk-image","path":"./icons/icon-disk-image.js"},{"name":"icon-display","path":"./icons/icon-display.js"},{"name":"icon-dna","path":"./icons/icon-dna.js"},{"name":"icon-dock-connector","path":"./icons/icon-dock-connector.js"},{"name":"icon-document-dashed-line","path":"./icons/icon-document-dashed-line.js"},{"name":"icon-document","path":"./icons/icon-document.js"},{"name":"icon-documents","path":"./icons/icon-documents.js"},{"name":"icon-donate","legacy":true,"path":"./icons/icon-donate.js"},{"name":"icon-door-open-alt","legacy":true,"path":"./icons/icon-door-open-alt.js"},{"name":"icon-door-open","path":"./icons/icon-door-open.js"},{"name":"icon-download-alt","path":"./icons/icon-download-alt.js"},{"name":"icon-download","path":"./icons/icon-download.js"},{"name":"icon-drop","path":"./icons/icon-drop.js"},{"name":"icon-eco","path":"./icons/icon-eco.js"},{"name":"icon-economy","legacy":true,"path":"./icons/icon-economy.js"},{"name":"icon-edit","path":"./icons/icon-edit.js"},{"name":"icon-employee","legacy":true,"path":"./icons/icon-employee.js"},{"name":"icon-energy-saving-bulb","path":"./icons/icon-energy-saving-bulb.js"},{"name":"icon-enter","path":"./icons/icon-enter.js"},{"name":"icon-equalizer","path":"./icons/icon-equalizer.js"},{"name":"icon-escape","path":"./icons/icon-escape.js"},{"name":"icon-ethernet","path":"./icons/icon-ethernet.js"},{"name":"icon-eye","path":"./icons/icon-eye.js"},{"name":"icon-exit-fullscreen","path":"./icons/icon-exit-fullscreen.js"},{"name":"icon-facebook-like","path":"./icons/icon-facebook-like.js"},{"name":"icon-factory","path":"./icons/icon-factory.js"},{"name":"icon-favorite","path":"./icons/icon-favorite.js"},{"name":"icon-file-cabinet","path":"./icons/icon-file-cabinet.js"},{"name":"icon-files","path":"./icons/icon-files.js"},{"name":"icon-filter-arrows","path":"./icons/icon-filter-arrows.js"},{"name":"icon-filter","path":"./icons/icon-filter.js"},{"name":"icon-fingerprint","path":"./icons/icon-fingerprint.js"},{"name":"icon-fire","path":"./icons/icon-fire.js"},{"name":"icon-firewire","legacy":true,"path":"./icons/icon-firewire.js"},{"name":"icon-flag-alt","path":"./icons/icon-flag-alt.js"},{"name":"icon-flag","path":"./icons/icon-flag.js"},{"name":"icon-flash","path":"./icons/icon-flash.js"},{"name":"icon-flashlight","path":"./icons/icon-flashlight.js"},{"name":"icon-flowerpot","path":"./icons/icon-flowerpot.js"},{"name":"icon-folder","path":"./icons/icon-folder.js"},{"name":"icon-folders","path":"./icons/icon-folders.js"},{"name":"icon-font","path":"./icons/icon-font.js"},{"name":"icon-food","path":"./icons/icon-food.js"},{"name":"icon-footprints","path":"./icons/icon-footprints.js"},{"name":"icon-forking","path":"./icons/icon-forking.js"},{"name":"icon-frame-alt","legacy":true,"path":"./icons/icon-frame-alt.js"},{"name":"icon-frame","path":"./icons/icon-frame.js"},{"name":"icon-fullscreen-alt","path":"./icons/icon-fullscreen-alt.js"},{"name":"icon-fullscreen","path":"./icons/icon-fullscreen.js"},{"name":"icon-game","path":"./icons/icon-game.js"},{"name":"icon-geometry","legacy":true,"path":"./icons/icon-geometry.js"},{"name":"icon-gift","path":"./icons/icon-gift.js"},{"name":"icon-glasses","path":"./icons/icon-glasses.js"},{"name":"icon-globe-alt","path":"./icons/icon-globe-alt.js"},{"name":"icon-globe-asia","legacy":true,"path":"./icons/icon-globe-asia.js"},{"name":"icon-globe-europe-africa","legacy":true,"path":"./icons/icon-globe-europe-africa.js"},{"name":"icon-globe-inverted-america","legacy":true,"path":"./icons/icon-globe-inverted-america.js"},{"name":"icon-globe-inverted-asia","legacy":true,"path":"./icons/icon-globe-inverted-asia.js"},{"name":"icon-globe-inverted-europe-africa","legacy":true,"path":"./icons/icon-globe-inverted-europe-africa.js"},{"name":"icon-globe","path":"./icons/icon-globe.js"},{"name":"icon-gps","path":"./icons/icon-gps.js"},{"name":"icon-graduate","path":"./icons/icon-graduate.js"},{"name":"icon-grid","path":"./icons/icon-grid.js"},{"name":"icon-hammer","path":"./icons/icon-hammer.js"},{"name":"icon-hand-active-alt","legacy":true,"path":"./icons/icon-hand-active-alt.js"},{"name":"icon-hand-active","path":"./icons/icon-hand-active.js"},{"name":"icon-hand-pointer-alt","legacy":true,"path":"./icons/icon-hand-pointer-alt.js"},{"name":"icon-hand-pointer","path":"./icons/icon-hand-pointer.js"},{"name":"icon-handshake","path":"./icons/icon-handshake.js"},{"name":"icon-handtool-alt","legacy":true,"path":"./icons/icon-handtool-alt.js"},{"name":"icon-handtool","path":"./icons/icon-handtool.js"},{"name":"icon-hard-drive-alt","legacy":true,"path":"./icons/icon-hard-drive-alt.js"},{"name":"icon-hard-drive","legacy":true,"path":"./icons/icon-hard-drive.js"},{"name":"icon-headphones","path":"./icons/icon-headphones.js"},{"name":"icon-headset","legacy":true,"path":"./icons/icon-headset.js"},{"name":"icon-hearts","path":"./icons/icon-hearts.js"},{"name":"icon-height","path":"./icons/icon-height.js"},{"name":"icon-help-alt","path":"./icons/icon-help-alt.js"},{"name":"icon-help","path":"./icons/icon-help.js"},{"name":"icon-home","path":"./icons/icon-home.js"},{"name":"icon-hourglass","path":"./icons/icon-hourglass.js"},{"name":"icon-imac","legacy":true,"path":"./icons/icon-imac.js"},{"name":"icon-inbox-full","legacy":true,"path":"./icons/icon-inbox-full.js"},{"name":"icon-inbox","path":"./icons/icon-inbox.js"},{"name":"icon-indent","path":"./icons/icon-indent.js"},{"name":"icon-infinity","path":"./icons/icon-infinity.js"},{"name":"icon-info","path":"./icons/icon-info.js"},{"name":"icon-invoice","legacy":true,"path":"./icons/icon-invoice.js"},{"name":"icon-ipad","legacy":true,"path":"./icons/icon-ipad.js"},{"name":"icon-iphone","legacy":true,"path":"./icons/icon-iphone.js"},{"name":"icon-item-arrangement","legacy":true,"path":"./icons/icon-item-arrangement.js"},{"name":"icon-junk","path":"./icons/icon-junk.js"},{"name":"icon-key","path":"./icons/icon-key.js"},{"name":"icon-keyboard","path":"./icons/icon-keyboard.js"},{"name":"icon-lab","path":"./icons/icon-lab.js"},{"name":"icon-laptop","path":"./icons/icon-laptop.js"},{"name":"icon-layers-alt","legacy":true,"path":"./icons/icon-layers-alt.js"},{"name":"icon-layers","path":"./icons/icon-layers.js"},{"name":"icon-layout","path":"./icons/icon-layout.js"},{"name":"icon-left-double-arrow","path":"./icons/icon-left-double-arrow.js"},{"name":"icon-legal","path":"./icons/icon-legal.js"},{"name":"icon-lense","legacy":true,"path":"./icons/icon-lense.js"},{"name":"icon-library","path":"./icons/icon-library.js"},{"name":"icon-light-down","path":"./icons/icon-light-down.js"},{"name":"icon-light-up","path":"./icons/icon-light-up.js"},{"name":"icon-lightbulb-active","path":"./icons/icon-lightbulb-active.js"},{"name":"icon-lightbulb","legacy":true,"path":"./icons/icon-lightbulb.js"},{"name":"icon-lightning","path":"./icons/icon-lightning.js"},{"name":"icon-link","path":"./icons/icon-link.js"},{"name":"icon-list","path":"./icons/icon-list.js"},{"name":"icon-load","legacy":true,"path":"./icons/icon-load.js"},{"name":"icon-loading","legacy":true,"path":"./icons/icon-loading.js"},{"name":"icon-locate","path":"./icons/icon-locate.js"},{"name":"icon-location-near-me","legacy":true,"path":"./icons/icon-location-near-me.js"},{"name":"icon-location-nearby","path":"./icons/icon-location-nearby.js"},{"name":"icon-lock","path":"./icons/icon-lock.js"},{"name":"icon-log-out","path":"./icons/icon-log-out.js"},{"name":"icon-logout","legacy":true,"path":"./icons/icon-logout.js"},{"name":"icon-loupe","legacy":true,"path":"./icons/icon-loupe.js"},{"name":"icon-magnet","path":"./icons/icon-magnet.js"},{"name":"icon-mailbox","path":"./icons/icon-mailbox.js"},{"name":"icon-map-alt","path":"./icons/icon-map-alt.js"},{"name":"icon-map-location","legacy":true,"path":"./icons/icon-map-location.js"},{"name":"icon-map-marker","path":"./icons/icon-map-marker.js"},{"name":"icon-map","path":"./icons/icon-map.js"},{"name":"icon-medal","path":"./icons/icon-medal.js"},{"name":"icon-medical-emergency","path":"./icons/icon-medical-emergency.js"},{"name":"icon-medicine","path":"./icons/icon-medicine.js"},{"name":"icon-meeting","legacy":true,"path":"./icons/icon-meeting.js"},{"name":"icon-megaphone","path":"./icons/icon-megaphone.js"},{"name":"icon-merge","path":"./icons/icon-merge.js"},{"name":"icon-message-open","path":"./icons/icon-message-open.js"},{"name":"icon-message-unopened","legacy":true,"path":"./icons/icon-message-unopened.js"},{"name":"icon-message","path":"./icons/icon-message.js"},{"name":"icon-microscope","path":"./icons/icon-microscope.js"},{"name":"icon-mindmap","legacy":true,"path":"./icons/icon-mindmap.js"},{"name":"icon-mobile","path":"./icons/icon-mobile.js"},{"name":"icon-mountain","path":"./icons/icon-mountain.js"},{"name":"icon-mouse-cursor","path":"./icons/icon-mouse-cursor.js"},{"name":"icon-mouse","path":"./icons/icon-mouse.js"},{"name":"icon-movie-alt","path":"./icons/icon-movie-alt.js"},{"name":"icon-movie","path":"./icons/icon-movie.js"},{"name":"icon-multiple-credit-cards","path":"./icons/icon-multiple-credit-cards.js"},{"name":"icon-multiple-windows","path":"./icons/icon-multiple-windows.js"},{"name":"icon-music","path":"./icons/icon-music.js"},{"name":"icon-name-badge","legacy":true,"path":"./icons/icon-name-badge.js"},{"name":"icon-navigation-bottom","legacy":true,"path":"./icons/icon-navigation-bottom.js"},{"name":"icon-navigation-down","legacy":true,"path":"./icons/icon-navigation-down.js"},{"name":"icon-navigation-first","legacy":true,"path":"./icons/icon-navigation-first.js"},{"name":"icon-navigation-horizontal","legacy":true,"path":"./icons/icon-navigation-horizontal.js"},{"name":"icon-navigation-last","legacy":true,"path":"./icons/icon-navigation-last.js"},{"name":"icon-navigation-left","legacy":true,"path":"./icons/icon-navigation-left.js"},{"name":"icon-navigation-right","legacy":true,"path":"./icons/icon-navigation-right.js"},{"name":"icon-navigation-road","legacy":true,"path":"./icons/icon-navigation-road.js"},{"name":"icon-navigation-top","legacy":true,"path":"./icons/icon-navigation-top.js"},{"name":"icon-navigation-up","legacy":true,"path":"./icons/icon-navigation-up.js"},{"name":"icon-navigation-vertical","legacy":true,"path":"./icons/icon-navigation-vertical.js"},{"name":"icon-navigation","legacy":true,"path":"./icons/icon-navigation.js"},{"name":"icon-navigational-arrow","path":"./icons/icon-navigational-arrow.js"},{"name":"icon-network-alt","path":"./icons/icon-network-alt.js"},{"name":"icon-newspaper-alt","legacy":true,"path":"./icons/icon-newspaper-alt.js"},{"name":"icon-newspaper","path":"./icons/icon-newspaper.js"},{"name":"icon-next-media","legacy":true,"path":"./icons/icon-next-media.js"},{"name":"icon-next","legacy":true,"path":"./icons/icon-next.js"},{"name":"icon-nodes","legacy":true,"path":"./icons/icon-nodes.js"},{"name":"icon-notepad-alt","legacy":true,"path":"./icons/icon-notepad-alt.js"},{"name":"icon-notepad","path":"./icons/icon-notepad.js"},{"name":"icon-old-key","path":"./icons/icon-old-key.js"},{"name":"icon-old-phone","legacy":true,"path":"./icons/icon-old-phone.js"},{"name":"icon-operator","path":"./icons/icon-operator.js"},{"name":"icon-ordered-list","path":"./icons/icon-ordered-list.js"},{"name":"icon-out","path":"./icons/icon-out.js"},{"name":"icon-outbox","legacy":true,"path":"./icons/icon-outbox.js"},{"name":"icon-outdent","path":"./icons/icon-outdent.js"},{"name":"icon-page-add","path":"./icons/icon-page-add.js"},{"name":"icon-page-down","path":"./icons/icon-page-down.js"},{"name":"icon-page-remove","path":"./icons/icon-page-remove.js"},{"name":"icon-page-restricted","path":"./icons/icon-page-restricted.js"},{"name":"icon-page-up","path":"./icons/icon-page-up.js"},{"name":"icon-paint-roller","legacy":true,"path":"./icons/icon-paint-roller.js"},{"name":"icon-palette","path":"./icons/icon-palette.js"},{"name":"icon-panel-show","path":"./icons/icon-panel-show.js"},{"name":"icon-pannel-close","path":"./icons/icon-pannel-close.js"},{"name":"icon-paper-bag","legacy":true,"path":"./icons/icon-paper-bag.js"},{"name":"icon-paper-plane-alt","path":"./icons/icon-paper-plane-alt.js"},{"name":"icon-paper-plane","path":"./icons/icon-paper-plane.js"},{"name":"icon-partly-cloudy","path":"./icons/icon-partly-cloudy.js"},{"name":"icon-paste-in","legacy":true,"path":"./icons/icon-paste-in.js"},{"name":"icon-pause","path":"./icons/icon-pause.js"},{"name":"icon-pc","legacy":true,"path":"./icons/icon-pc.js"},{"name":"icon-people-alt-2","legacy":true,"path":"./icons/icon-people-alt-2.js"},{"name":"icon-people-alt","legacy":true,"path":"./icons/icon-people-alt.js"},{"name":"icon-people-female","legacy":true,"path":"./icons/icon-people-female.js"},{"name":"icon-people","path":"./icons/icon-people.js"},{"name":"icon-phone-ring","path":"./icons/icon-phone-ring.js"},{"name":"icon-phone","path":"./icons/icon-phone.js"},{"name":"icon-photo-album","path":"./icons/icon-photo-album.js"},{"name":"icon-picture","path":"./icons/icon-picture.js"},{"name":"icon-pictures-alt-2","path":"./icons/icon-pictures-alt-2.js"},{"name":"icon-pictures-alt","legacy":true,"path":"./icons/icon-pictures-alt.js"},{"name":"icon-pictures","path":"./icons/icon-pictures.js"},{"name":"icon-pie-chart","path":"./icons/icon-pie-chart.js"},{"name":"icon-piggy-bank","path":"./icons/icon-piggy-bank.js"},{"name":"icon-pin-location","path":"./icons/icon-pin-location.js"},{"name":"icon-plane","path":"./icons/icon-plane.js"},{"name":"icon-planet","legacy":true,"path":"./icons/icon-planet.js"},{"name":"icon-play","path":"./icons/icon-play.js"},{"name":"icon-playing-cards","legacy":true,"path":"./icons/icon-playing-cards.js"},{"name":"icon-playlist","path":"./icons/icon-playlist.js"},{"name":"icon-plugin","path":"./icons/icon-plugin.js"},{"name":"icon-podcast","path":"./icons/icon-podcast.js"},{"name":"icon-poll","legacy":true,"path":"./icons/icon-poll.js"},{"name":"icon-post-it","path":"./icons/icon-post-it.js"},{"name":"icon-power-outlet","legacy":true,"path":"./icons/icon-power-outlet.js"},{"name":"icon-power","path":"./icons/icon-power.js"},{"name":"icon-presentation","path":"./icons/icon-presentation.js"},{"name":"icon-previous-media","path":"./icons/icon-previous-media.js"},{"name":"icon-previous","path":"./icons/icon-previous.js"},{"name":"icon-price-dollar","legacy":true,"path":"./icons/icon-price-dollar.js"},{"name":"icon-price-euro","legacy":true,"path":"./icons/icon-price-euro.js"},{"name":"icon-price-pound","legacy":true,"path":"./icons/icon-price-pound.js"},{"name":"icon-price-yen","legacy":true,"path":"./icons/icon-price-yen.js"},{"name":"icon-print","path":"./icons/icon-print.js"},{"name":"icon-printer-alt","legacy":true,"path":"./icons/icon-printer-alt.js"},{"name":"icon-projector","path":"./icons/icon-projector.js"},{"name":"icon-pulse","path":"./icons/icon-pulse.js"},{"name":"icon-pushpin","path":"./icons/icon-pushpin.js"},{"name":"icon-qr-code","path":"./icons/icon-qr-code.js"},{"name":"icon-quote","path":"./icons/icon-quote.js"},{"name":"icon-radio-alt","path":"./icons/icon-radio-alt.js"},{"name":"icon-radio-receiver","path":"./icons/icon-radio-receiver.js"},{"name":"icon-radio","path":"./icons/icon-radio.js"},{"name":"icon-rain","path":"./icons/icon-rain.js"},{"name":"icon-rate","legacy":true,"path":"./icons/icon-rate.js"},{"name":"icon-re-post","path":"./icons/icon-re-post.js"},{"name":"icon-readonly","legacy":true,"path":"./icons/icon-readonly.js"},{"name":"icon-receipt-alt","path":"./icons/icon-receipt-alt.js"},{"name":"icon-reception","path":"./icons/icon-reception.js"},{"name":"icon-record","legacy":true,"path":"./icons/icon-record.js"},{"name":"icon-rectangle-ellipsis","path":"./icons/icon-rectangle-ellipsis.js"},{"name":"icon-redo","path":"./icons/icon-redo.js"},{"name":"icon-refresh","path":"./icons/icon-refresh.js"},{"name":"icon-remote","legacy":true,"path":"./icons/icon-remote.js"},{"name":"icon-remove","path":"./icons/icon-remove.js"},{"name":"icon-repeat-one","path":"./icons/icon-repeat-one.js"},{"name":"icon-repeat","path":"./icons/icon-repeat.js"},{"name":"icon-reply-arrow","path":"./icons/icon-reply-arrow.js"},{"name":"icon-resize","path":"./icons/icon-resize.js"},{"name":"icon-return-to-top","legacy":true,"path":"./icons/icon-return-to-top.js"},{"name":"icon-right-double-arrow","legacy":true,"path":"./icons/icon-right-double-arrow.js"},{"name":"icon-roadsign","legacy":true,"path":"./icons/icon-roadsign.js"},{"name":"icon-rocket","path":"./icons/icon-rocket.js"},{"name":"icon-rss","path":"./icons/icon-rss.js"},{"name":"icon-ruler-alt","path":"./icons/icon-ruler-alt.js"},{"name":"icon-ruler","path":"./icons/icon-ruler.js"},{"name":"icon-satellite-dish","path":"./icons/icon-satellite-dish.js"},{"name":"icon-save","path":"./icons/icon-save.js"},{"name":"icon-scan","path":"./icons/icon-scan.js"},{"name":"icon-school","path":"./icons/icon-school.js"},{"name":"icon-screensharing","path":"./icons/icon-screensharing.js"},{"name":"icon-script-alt","legacy":true,"path":"./icons/icon-script-alt.js"},{"name":"icon-script","path":"./icons/icon-script.js"},{"name":"icon-scull","path":"./icons/icon-scull.js"},{"name":"icon-search","path":"./icons/icon-search.js"},{"name":"icon-sensor","path":"./icons/icon-sensor.js"},{"name":"icon-server-alt","legacy":true,"path":"./icons/icon-server-alt.js"},{"name":"icon-server","path":"./icons/icon-server.js"},{"name":"icon-settings-alt","legacy":true,"path":"./icons/icon-settings-alt.js"},{"name":"icon-settings","path":"./icons/icon-settings.js"},{"name":"icon-share-alt","path":"./icons/icon-share-alt.js"},{"name":"icon-share","path":"./icons/icon-share.js"},{"name":"icon-sharing-iphone","path":"./icons/icon-sharing-iphone.js"},{"name":"icon-shield","path":"./icons/icon-shield.js"},{"name":"icon-shift","path":"./icons/icon-shift.js"},{"name":"icon-shipping-box","path":"./icons/icon-shipping-box.js"},{"name":"icon-shipping","path":"./icons/icon-shipping.js"},{"name":"icon-shoe","path":"./icons/icon-shoe.js"},{"name":"icon-shopping-basket-alt-2","legacy":true,"path":"./icons/icon-shopping-basket-alt-2.js"},{"name":"icon-shopping-basket-alt","path":"./icons/icon-shopping-basket-alt.js"},{"name":"icon-shopping-basket","path":"./icons/icon-shopping-basket.js"},{"name":"icon-shuffle","path":"./icons/icon-shuffle.js"},{"name":"icon-sience","path":"./icons/icon-sience.js"},{"name":"icon-single-note","path":"./icons/icon-single-note.js"},{"name":"icon-sitemap","legacy":true,"path":"./icons/icon-sitemap.js"},{"name":"icon-sleep","path":"./icons/icon-sleep.js"},{"name":"icon-slideshow","legacy":true,"path":"./icons/icon-slideshow.js"},{"name":"icon-smiley-inverted","legacy":true,"path":"./icons/icon-smiley-inverted.js"},{"name":"icon-smiley","path":"./icons/icon-smiley.js"},{"name":"icon-snow","path":"./icons/icon-snow.js"},{"name":"icon-sound-low","path":"./icons/icon-sound-low.js"},{"name":"icon-sound-medium","legacy":true,"path":"./icons/icon-sound-medium.js"},{"name":"icon-sound-off","path":"./icons/icon-sound-off.js"},{"name":"icon-sound-waves","path":"./icons/icon-sound-waves.js"},{"name":"icon-sound","path":"./icons/icon-sound.js"},{"name":"icon-spades","path":"./icons/icon-spades.js"},{"name":"icon-speaker","path":"./icons/icon-speaker.js"},{"name":"icon-speed-gauge","path":"./icons/icon-speed-gauge.js"},{"name":"icon-split-alt","path":"./icons/icon-split-alt.js"},{"name":"icon-split","path":"./icons/icon-split.js"},{"name":"icon-sprout","path":"./icons/icon-sprout.js"},{"name":"icon-squiggly-line","legacy":true,"path":"./icons/icon-squiggly-line.js"},{"name":"icon-ssd","legacy":true,"path":"./icons/icon-ssd.js"},{"name":"icon-stacked-disks","legacy":true,"path":"./icons/icon-stacked-disks.js"},{"name":"icon-stamp","legacy":true,"path":"./icons/icon-stamp.js"},{"name":"icon-stop-alt","path":"./icons/icon-stop-alt.js"},{"name":"icon-stop-hand","legacy":true,"path":"./icons/icon-stop-hand.js"},{"name":"icon-stop","path":"./icons/icon-stop.js"},{"name":"icon-store","path":"./icons/icon-store.js"},{"name":"icon-stream","legacy":true,"path":"./icons/icon-stream.js"},{"name":"icon-sunny","path":"./icons/icon-sunny.js"},{"name":"icon-sweatshirt","legacy":true,"path":"./icons/icon-sweatshirt.js"},{"name":"icon-sync","path":"./icons/icon-sync.js"},{"name":"icon-t-shirt","path":"./icons/icon-t-shirt.js"},{"name":"icon-tab-key","path":"./icons/icon-tab-key.js"},{"name":"icon-tag","path":"./icons/icon-tag.js"},{"name":"icon-tags","path":"./icons/icon-tags.js"},{"name":"icon-takeaway-cup","legacy":true,"path":"./icons/icon-takeaway-cup.js"},{"name":"icon-target","path":"./icons/icon-target.js"},{"name":"icon-temperatrure-alt","path":"./icons/icon-temperatrure-alt.js"},{"name":"icon-temperature","path":"./icons/icon-temperature.js"},{"name":"icon-terminal","path":"./icons/icon-terminal.js"},{"name":"icon-theater","path":"./icons/icon-theater.js"},{"name":"icon-thumb-down","path":"./icons/icon-thumb-down.js"},{"name":"icon-thumb-up","path":"./icons/icon-thumb-up.js"},{"name":"icon-thumbnail-list","path":"./icons/icon-thumbnail-list.js"},{"name":"icon-thumbnails-small","path":"./icons/icon-thumbnails-small.js"},{"name":"icon-thumbnails","path":"./icons/icon-thumbnails.js"},{"name":"icon-ticket","path":"./icons/icon-ticket.js"},{"name":"icon-time","path":"./icons/icon-time.js"},{"name":"icon-timer","path":"./icons/icon-timer.js"},{"name":"icon-tools","legacy":true,"path":"./icons/icon-tools.js"},{"name":"icon-top","legacy":true,"path":"./icons/icon-top.js"},{"name":"icon-traffic-alt","legacy":true,"path":"./icons/icon-traffic-alt.js"},{"name":"icon-trafic","path":"./icons/icon-trafic.js"},{"name":"icon-train","path":"./icons/icon-train.js"},{"name":"icon-trash-alt-2","legacy":true,"path":"./icons/icon-trash-alt-2.js"},{"name":"icon-trash-alt","legacy":true,"path":"./icons/icon-trash-alt.js"},{"name":"icon-trash","path":"./icons/icon-trash.js"},{"name":"icon-tree","path":"./icons/icon-tree.js"},{"name":"icon-trophy","path":"./icons/icon-trophy.js"},{"name":"icon-truck","path":"./icons/icon-truck.js"},{"name":"icon-tv-old","path":"./icons/icon-tv-old.js"},{"name":"icon-tv","path":"./icons/icon-tv.js"},{"name":"icon-umb-content","legacy":true,"path":"./icons/icon-umb-content.js"},{"name":"icon-umb-developer","legacy":true,"path":"./icons/icon-umb-developer.js"},{"name":"icon-umb-media","legacy":true,"path":"./icons/icon-umb-media.js"},{"name":"icon-umb-settings","legacy":true,"path":"./icons/icon-umb-settings.js"},{"name":"icon-umb-users","legacy":true,"path":"./icons/icon-umb-users.js"},{"name":"icon-umbrella","path":"./icons/icon-umbrella.js"},{"name":"icon-undo","path":"./icons/icon-undo.js"},{"name":"icon-unlocked","path":"./icons/icon-unlocked.js"},{"name":"icon-untitled","legacy":true,"path":"./icons/icon-untitled.js"},{"name":"icon-usb-connector","legacy":true,"path":"./icons/icon-usb-connector.js"},{"name":"icon-usb","path":"./icons/icon-usb.js"},{"name":"icon-user-female","legacy":true,"path":"./icons/icon-user-female.js"},{"name":"icon-user-females-alt","legacy":true,"path":"./icons/icon-user-females-alt.js"},{"name":"icon-user-females","legacy":true,"path":"./icons/icon-user-females.js"},{"name":"icon-user-glasses","legacy":true,"path":"./icons/icon-user-glasses.js"},{"name":"icon-user","path":"./icons/icon-user.js"},{"name":"icon-users-alt","legacy":true,"path":"./icons/icon-users-alt.js"},{"name":"icon-users","path":"./icons/icon-users.js"},{"name":"icon-utilities","path":"./icons/icon-utilities.js"},{"name":"icon-vcard","path":"./icons/icon-vcard.js"},{"name":"icon-video","path":"./icons/icon-video.js"},{"name":"icon-voice","path":"./icons/icon-voice.js"},{"name":"icon-wall-plug","path":"./icons/icon-wall-plug.js"},{"name":"icon-wallet","path":"./icons/icon-wallet.js"},{"name":"icon-wand","path":"./icons/icon-wand.js"},{"name":"icon-webhook","path":"./icons/icon-webhook.js"},{"name":"icon-weight","path":"./icons/icon-weight.js"},{"name":"icon-width","path":"./icons/icon-width.js"},{"name":"icon-wifi","path":"./icons/icon-wifi.js"},{"name":"icon-window-popin","path":"./icons/icon-window-popin.js"},{"name":"icon-window-popout","path":"./icons/icon-window-popout.js"},{"name":"icon-window-sizes","path":"./icons/icon-window-sizes.js"},{"name":"icon-wine-glass","path":"./icons/icon-wine-glass.js"},{"name":"icon-wrench","path":"./icons/icon-wrench.js"},{"name":"icon-wrong","path":"./icons/icon-wrong.js"},{"name":"icon-zip","path":"./icons/icon-zip.js"},{"name":"icon-zom-out","legacy":true,"path":"./icons/icon-zom-out.js"},{"name":"icon-zoom-in","path":"./icons/icon-zoom-in.js"},{"name":"icon-zoom-out","path":"./icons/icon-zoom-out.js"},{"name":"icon-star","path":"./icons/icon-star.js"},{"name":"icon-database","path":"./icons/icon-database.js"},{"name":"icon-azure","path":"./icons/icon-azure.js"},{"name":"icon-facebook","path":"./icons/icon-facebook.js"},{"name":"icon-gitbook","path":"./icons/icon-gitbook.js"},{"name":"icon-github","path":"./icons/icon-github.js"},{"name":"icon-gitlab","path":"./icons/icon-gitlab.js"},{"name":"icon-google","path":"./icons/icon-google.js"},{"name":"icon-linkedin","path":"./icons/icon-linkedin.js"},{"name":"icon-mastodon","path":"./icons/icon-mastodon.js"},{"name":"icon-microsoft","path":"./icons/icon-microsoft.js"},{"name":"icon-twitter-x","path":"./icons/icon-twitter-x.js"},{"name":"icon-umbraco","path":"./icons/icon-umbraco.js"},{"name":"icon-art-easel","legacy":true,"path":"./icons/icon-art-easel.js"},{"name":"icon-article","legacy":true,"path":"./icons/icon-article.js"},{"name":"icon-auction-hammer","legacy":true,"path":"./icons/icon-auction-hammer.js"},{"name":"icon-baby-stroller","legacy":true,"path":"./icons/icon-baby-stroller.js"},{"name":"icon-badge-count","legacy":true,"path":"./icons/icon-badge-count.js"},{"name":"icon-band-aid","legacy":true,"path":"./icons/icon-band-aid.js"},{"name":"icon-bill-dollar","legacy":true,"path":"./icons/icon-bill-dollar.js"},{"name":"icon-bill-euro","legacy":true,"path":"./icons/icon-bill-euro.js"},{"name":"icon-bill-pound","legacy":true,"path":"./icons/icon-bill-pound.js"},{"name":"icon-bill-yen","legacy":true,"path":"./icons/icon-bill-yen.js"},{"name":"icon-bill","legacy":true,"path":"./icons/icon-bill.js"},{"name":"icon-billboard","legacy":true,"path":"./icons/icon-billboard.js"},{"name":"icon-bills-dollar","legacy":true,"path":"./icons/icon-bills-dollar.js"},{"name":"icon-bills-euro","legacy":true,"path":"./icons/icon-bills-euro.js"},{"name":"icon-bills-pound","legacy":true,"path":"./icons/icon-bills-pound.js"},{"name":"icon-bills-yen","legacy":true,"path":"./icons/icon-bills-yen.js"},{"name":"icon-bills","legacy":true,"path":"./icons/icon-bills.js"},{"name":"icon-binoculars","legacy":true,"path":"./icons/icon-binoculars.js"},{"name":"icon-blueprint","legacy":true,"path":"./icons/icon-blueprint.js"},{"name":"icon-bomb","legacy":true,"path":"./icons/icon-bomb.js"},{"name":"icon-cash-register","legacy":true,"path":"./icons/icon-cash-register.js"},{"name":"icon-checkbox-dotted-active","legacy":true,"path":"./icons/icon-checkbox-dotted-active.js"},{"name":"icon-chess","legacy":true,"path":"./icons/icon-chess.js"},{"name":"icon-circus","legacy":true,"path":"./icons/icon-circus.js"},{"name":"icon-clothes-hanger","legacy":true,"path":"./icons/icon-clothes-hanger.js"},{"name":"icon-coin","legacy":true,"path":"./icons/icon-coin.js"},{"name":"icon-coins-dollar-alt","legacy":true,"path":"./icons/icon-coins-dollar-alt.js"},{"name":"icon-coins-dollar","legacy":true,"path":"./icons/icon-coins-dollar.js"},{"name":"icon-coins-euro-alt","legacy":true,"path":"./icons/icon-coins-euro-alt.js"},{"name":"icon-coins-euro","legacy":true,"path":"./icons/icon-coins-euro.js"},{"name":"icon-coins-pound-alt","legacy":true,"path":"./icons/icon-coins-pound-alt.js"},{"name":"icon-coins-pound","legacy":true,"path":"./icons/icon-coins-pound.js"},{"name":"icon-coins-yen-alt","legacy":true,"path":"./icons/icon-coins-yen-alt.js"},{"name":"icon-coins-yen","legacy":true,"path":"./icons/icon-coins-yen.js"},{"name":"icon-comb","legacy":true,"path":"./icons/icon-comb.js"},{"name":"icon-desk","legacy":true,"path":"./icons/icon-desk.js"},{"name":"icon-dollar-bag","legacy":true,"path":"./icons/icon-dollar-bag.js"},{"name":"icon-eject","legacy":true,"path":"./icons/icon-eject.js"},{"name":"icon-euro-bag","legacy":true,"path":"./icons/icon-euro-bag.js"},{"name":"icon-female-symbol","legacy":true,"path":"./icons/icon-female-symbol.js"},{"name":"icon-firewall","legacy":true,"path":"./icons/icon-firewall.js"},{"name":"icon-folder-open","legacy":true,"path":"./icons/icon-folder-open.js"},{"name":"icon-folder-outline","legacy":true,"path":"./icons/icon-folder-outline.js"},{"name":"icon-handprint","legacy":true,"path":"./icons/icon-handprint.js"},{"name":"icon-hat","legacy":true,"path":"./icons/icon-hat.js"},{"name":"icon-hd","legacy":true,"path":"./icons/icon-hd.js"},{"name":"icon-inactive-line","legacy":true,"path":"./icons/icon-inactive-line.js"},{"name":"icon-keychain","legacy":true,"path":"./icons/icon-keychain.js"},{"name":"icon-keyhole","legacy":true,"path":"./icons/icon-keyhole.js"},{"name":"icon-linux-tux","legacy":true,"path":"./icons/icon-linux-tux.js"},{"name":"icon-male-and-female","legacy":true,"path":"./icons/icon-male-and-female.js"},{"name":"icon-male-symbol","legacy":true,"path":"./icons/icon-male-symbol.js"},{"name":"icon-molecular-network","legacy":true,"path":"./icons/icon-molecular-network.js"},{"name":"icon-molecular","legacy":true,"path":"./icons/icon-molecular.js"},{"name":"icon-os-x","legacy":true,"path":"./icons/icon-os-x.js"},{"name":"icon-pants","legacy":true,"path":"./icons/icon-pants.js"},{"name":"icon-parachute-drop","legacy":true,"path":"./icons/icon-parachute-drop.js"},{"name":"icon-parental-control","legacy":true,"path":"./icons/icon-parental-control.js"},{"name":"icon-path","legacy":true,"path":"./icons/icon-path.js"},{"name":"icon-piracy","legacy":true,"path":"./icons/icon-piracy.js"},{"name":"icon-poker-chip","legacy":true,"path":"./icons/icon-poker-chip.js"},{"name":"icon-pound-bag","legacy":true,"path":"./icons/icon-pound-bag.js"},{"name":"icon-receipt-dollar","legacy":true,"path":"./icons/icon-receipt-dollar.js"},{"name":"icon-receipt-euro","legacy":true,"path":"./icons/icon-receipt-euro.js"},{"name":"icon-receipt-pound","legacy":true,"path":"./icons/icon-receipt-pound.js"},{"name":"icon-receipt-yen","legacy":true,"path":"./icons/icon-receipt-yen.js"},{"name":"icon-road","legacy":true,"path":"./icons/icon-road.js"},{"name":"icon-safe","legacy":true,"path":"./icons/icon-safe.js"},{"name":"icon-safedial","legacy":true,"path":"./icons/icon-safedial.js"},{"name":"icon-sandbox-toys","legacy":true,"path":"./icons/icon-sandbox-toys.js"},{"name":"icon-security-camera","legacy":true,"path":"./icons/icon-security-camera.js"},{"name":"icon-settings-alt-2","legacy":true,"path":"./icons/icon-settings-alt-2.js"},{"name":"icon-share-alt-2","legacy":true,"path":"./icons/icon-share-alt-2.js"},{"name":"icon-shorts","legacy":true,"path":"./icons/icon-shorts.js"},{"name":"icon-simcard","legacy":true,"path":"./icons/icon-simcard.js"},{"name":"icon-tab","legacy":true,"path":"./icons/icon-tab.js"},{"name":"icon-tactics","legacy":true,"path":"./icons/icon-tactics.js"},{"name":"icon-theif","legacy":true,"path":"./icons/icon-theif.js"},{"name":"icon-thought-bubble","legacy":true,"path":"./icons/icon-thought-bubble.js"},{"name":"icon-twitter","legacy":true,"path":"./icons/icon-twitter.js"},{"name":"icon-umb-contour","legacy":true,"path":"./icons/icon-umb-contour.js"},{"name":"icon-umb-deploy","legacy":true,"path":"./icons/icon-umb-deploy.js"},{"name":"icon-umb-members","legacy":true,"path":"./icons/icon-umb-members.js"},{"name":"icon-universal","legacy":true,"path":"./icons/icon-universal.js"},{"name":"icon-war","legacy":true,"path":"./icons/icon-war.js"},{"name":"icon-windows","legacy":true,"path":"./icons/icon-windows.js"},{"name":"icon-yen-bag","legacy":true,"path":"./icons/icon-yen-bag.js"}] \ No newline at end of file From 8c7a099a95f18ae3ac0c5ce2d47ded8b57eb6b93 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 19 Apr 2024 15:32:58 +0200 Subject: [PATCH 15/45] add localization and icons --- .../src/assets/lang/da-dk.ts | 6 +-- .../src/assets/lang/en-us.ts | 6 +-- .../modals/external-login-modal.element.ts | 38 +++++++++++++++---- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts index 15ae7c0767..e4c0f277e9 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts @@ -557,9 +557,9 @@ export default { exceptionDetail: 'Undtagelsesdetaljer', stacktrace: 'Stacktrace', innerException: 'Indre undtagelse', - linkYour: 'Link dit', - unLinkYour: 'Fjern link fra dit', - account: 'konto', + linkYour: 'Link din {0} konto', + unLinkYour: 'Fjern link fra din {0} konto', + linkedToService: 'Din konto er linket til denne service', selectEditor: 'Vælg editor', selectEditorConfiguration: 'Vælg konfiguration', selectSnippet: 'Vælg snippet', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts index ebfa4a1fae..7c2cb63c8e 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts @@ -564,9 +564,9 @@ export default { exceptionDetail: 'Exception Details', stacktrace: 'Stacktrace', innerException: 'Inner Exception', - linkYour: 'Link your', - unLinkYour: 'Un-link your', - account: 'account', + linkYour: 'Link your {0} account', + unLinkYour: 'Un-link your {0} account', + linkedToService: 'Your account is linked to this service', selectEditor: 'Select editor', selectEditorConfiguration: 'Select configuration', selectSnippet: 'Select snippet', diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts index 9cc289f702..4f37cd5565 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts @@ -9,6 +9,7 @@ import { mergeObservables } from '@umbraco-cms/backoffice/observable-api'; type UmbExternalLoginProviderOption = UmbCurrentUserExternalLoginProviderModel & { displayName: string; + icon?: string; isEnabledOnUser: boolean; }; @@ -44,6 +45,7 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { ); return { isEnabledOnUser: !!serverLoginProvider, + icon: manifestLoginProvider.meta?.defaultView?.icon, providerKey: manifestLoginProvider.forProviderName, providerName: manifestLoginProvider.forProviderName, displayName: @@ -95,27 +97,40 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { */ #renderProvider(item: UmbExternalLoginProviderOption) { return html` - + +
+ + ${item.displayName} +
${when( item.isEnabledOnUser, () => html`

- This provider is enabled - + Your account is linked to this service

this.#onProviderDisable(item)}> + .label=${this.localize.term('defaultdialogs_unLinkYour', item.displayName)} + @click=${() => this.#onProviderDisable(item)}> + + Unlink your ${item.displayName} account + + +
`, () => html` this.#onProviderEnable(item)}> + .label=${this.localize.term('defaultdialogs_linkYour', item.displayName)} + @click=${() => this.#onProviderEnable(item)}> + + Link your ${item.displayName} account + + +
`, )} @@ -136,6 +151,15 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { uui-box { margin-bottom: var(--uui-size-space-3); } + + .header { + display: flex; + align-items: center; + } + + .header uui-icon { + margin-right: var(--uui-size-space-4); + } `, ]; } From acbfcd011cc7ec3ccb1cf22e8ea0cac18298162d Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 19 Apr 2024 16:03:45 +0200 Subject: [PATCH 16/45] implement methods to link and unlink an account --- .../src/packages/core/auth/auth-flow.ts | 60 +++++++++++++++++++ .../src/packages/core/auth/auth.context.ts | 8 +++ .../modals/external-login-modal.element.ts | 19 +++++- 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts index 55680d1879..284145fb44 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts @@ -96,6 +96,10 @@ export class UmbAuthFlow { // tokens #tokenResponse?: TokenResponse; + // external login + #link_endpoint; + #unlink_endpoint; + constructor( openIdConnectUrl: string, redirectUri: string, @@ -115,6 +119,9 @@ export class UmbAuthFlow { end_session_endpoint: `${openIdConnectUrl}/umbraco/management/api/v1/security/back-office/signout`, }); + this.#link_endpoint = `${openIdConnectUrl}/umbraco/management/api/v1/security/back-office/link-login`; + this.#unlink_endpoint = `${openIdConnectUrl}/umbraco/management/api/v1/security/back-office/unlink-login`; + this.#notifier = new AuthorizationNotifier(); this.#tokenHandler = new BaseTokenRequestHandler(requestor); this.#storageBackend = new LocalStorageBackend(); @@ -333,6 +340,59 @@ export class UmbAuthFlow { : Promise.reject('Missing accessToken.'); } + /** + * This method will link the current user to the specified provider. + * @param provider The provider to link to. + */ + async linkLogin(provider: string) { + const token = await this.performWithFreshTokens(); + const url = new URL(this.#link_endpoint); + url.searchParams.set('provider', provider); + const request = new Request(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, + body: JSON.stringify({ provider }), + redirect: 'manual', + }); + + const result = await fetch(request); + + if (result.status === 301 || result.status === 302) { + const redirectUrl = result.headers.get('Location'); + if (redirectUrl) { + location.href = redirectUrl; + } + } + + if (!result.ok) { + throw new Error('Failed to link login'); + } + + return true; + } + + /** + * This method will unlink the current user from the specified provider. + */ + async unlinkLogin(loginProvider: string, providerKey: string) { + const token = await this.performWithFreshTokens(); + const request = new Request(this.#unlink_endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, + body: JSON.stringify({ loginProvider, providerKey }), + }); + + const result = await fetch(request); + + if (!result.ok) { + throw new Error('Failed to unlink login'); + } + + await this.signOut(); + + return true; + } + /** * Save the current token response to local storage. */ diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts index 370bd3d124..09f2c773bb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts @@ -175,4 +175,12 @@ export class UmbAuthContext extends UmbContextBase { getPostLogoutRedirectUrl() { return `${window.location.origin}${this.#backofficePath.endsWith('/') ? this.#backofficePath : this.#backofficePath + '/'}logout`; } + + linkLogin(provider: string) { + return this.#authFlow.linkLogin(provider); + } + + unlinkLogin(providerName: string, providerKey: string) { + return this.#authFlow.unlinkLogin(providerName, providerKey); + } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts index 4f37cd5565..4a3afd5969 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts @@ -2,10 +2,11 @@ import { UmbCurrentUserRepository } from '../../repository/index.js'; import type { UmbCurrentUserExternalLoginProviderModel } from '../../types.js'; import { css, customElement, html, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import type { UmbModalContext } from '@umbraco-cms/backoffice/modal'; +import { umbConfirmModal, type UmbModalContext } from '@umbraco-cms/backoffice/modal'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { mergeObservables } from '@umbraco-cms/backoffice/observable-api'; +import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'; type UmbExternalLoginProviderOption = UmbCurrentUserExternalLoginProviderModel & { displayName: string; @@ -138,11 +139,23 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { } async #onProviderEnable(item: UmbExternalLoginProviderOption) { - alert('Enable provider ' + item.providerName); + const authContext = await this.getContext(UMB_AUTH_CONTEXT); + authContext.linkLogin(item.providerName); } async #onProviderDisable(item: UmbExternalLoginProviderOption) { - alert('Disable provider ' + item.providerName); + try { + await umbConfirmModal(this, { + headline: this.localize.term('defaultdialogs_unLinkYour', item.displayName), + content: this.localize.term('defaultdialogs_unLinkYourConfirm', item.displayName), + confirmLabel: this.localize.term('general_unlink'), + color: 'danger', + }); + const authContext = await this.getContext(UMB_AUTH_CONTEXT); + authContext.unlinkLogin(item.providerName, item.providerKey); + } catch { + // Do nothing + } } static styles = [ From cc3fd67bdb17399add48f349c62eefcd812ac196 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 22 Apr 2024 09:22:44 +0200 Subject: [PATCH 17/45] add spacing to icons --- .../core/auth/components/auth-provider-default.element.ts | 6 +++++- .../external-login/modals/external-login-modal.element.ts | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts index 4f4afc1726..07567de04c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts @@ -31,7 +31,7 @@ export class UmbAuthProviderDefaultElement extends UmbLitElement implements UmbA .look=${this.manifest.meta?.defaultView?.look ?? 'outline'} .color=${this.manifest.meta?.defaultView?.color ?? 'default'}> ${this.manifest.meta?.defaultView?.icon - ? html`` + ? html`` : nothing} ${this.#label} @@ -48,6 +48,10 @@ export class UmbAuthProviderDefaultElement extends UmbLitElement implements UmbA #auth-provider-button { width: 100%; } + + #icon { + margin-right: var(--uui-size-space-2); + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts index 4a3afd5969..e909160652 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts @@ -100,7 +100,7 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { return html`
- + ${item.displayName}
${when( @@ -170,7 +170,7 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { align-items: center; } - .header uui-icon { + .header-icon { margin-right: var(--uui-size-space-4); } `, From 20fb6a3e1d8b26dbec7361fd8ad8ebd0a5ee7064 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 22 Apr 2024 09:54:10 +0200 Subject: [PATCH 18/45] adjust margin --- .../core/auth/components/auth-provider-default.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts index 07567de04c..5b218e076e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts @@ -50,7 +50,7 @@ export class UmbAuthProviderDefaultElement extends UmbLitElement implements UmbA } #icon { - margin-right: var(--uui-size-space-2); + margin-right: var(--uui-size-space-1); } `, ]; From ca5c4caf713838ac5c010940b22011621b180cc9 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 22 Apr 2024 10:00:28 +0200 Subject: [PATCH 19/45] add string localization --- .../components/auth-provider-default.element.ts | 4 +++- .../modals/external-login-modal.element.ts | 17 +++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts index 5b218e076e..9af11c8bff 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts @@ -18,7 +18,9 @@ export class UmbAuthProviderDefaultElement extends UmbLitElement implements UmbA } get #label() { - return this.localize.term('login_signInWith', this.manifest.meta?.label ?? this.manifest.forProviderName); + const label = this.manifest.meta?.label ?? this.manifest.forProviderName; + const labelLocalized = this.localize.string(label); + return this.localize.term('login_signInWith', labelLocalized); } render() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts index e909160652..ae5cc86d94 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts @@ -101,7 +101,7 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement {
- ${item.displayName} + ${this.localize.string(item.displayName)}
${when( item.isEnabledOnUser, @@ -113,10 +113,10 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { type="button" look="secondary" color="danger" - .label=${this.localize.term('defaultdialogs_unLinkYour', item.displayName)} + .label=${this.localize.term('defaultdialogs_unLinkYour', this.localize.string(item.displayName))} @click=${() => this.#onProviderDisable(item)}> - - Unlink your ${item.displayName} account + + Unlink your ${this.localize.string(item.displayName)} account @@ -127,8 +127,8 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { look="secondary" .label=${this.localize.term('defaultdialogs_linkYour', item.displayName)} @click=${() => this.#onProviderEnable(item)}> - - Link your ${item.displayName} account + + Link your ${this.localize.string(item.displayName)} account @@ -145,9 +145,10 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { async #onProviderDisable(item: UmbExternalLoginProviderOption) { try { + const providerDisplayName = this.localize.string(item.displayName); await umbConfirmModal(this, { - headline: this.localize.term('defaultdialogs_unLinkYour', item.displayName), - content: this.localize.term('defaultdialogs_unLinkYourConfirm', item.displayName), + headline: this.localize.term('defaultdialogs_unLinkYour', providerDisplayName), + content: this.localize.term('defaultdialogs_unLinkYourConfirm', providerDisplayName), confirmLabel: this.localize.term('general_unlink'), color: 'danger', }); From 2e56114b9288f3984f99789e07aff8d7ecf1b6d6 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 22 Apr 2024 10:18:09 +0200 Subject: [PATCH 20/45] use providerKey from server --- .../external-login/modals/external-login-modal.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts index ae5cc86d94..4cd29e13a0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts @@ -47,7 +47,7 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { return { isEnabledOnUser: !!serverLoginProvider, icon: manifestLoginProvider.meta?.defaultView?.icon, - providerKey: manifestLoginProvider.forProviderName, + providerKey: serverLoginProvider?.providerKey ?? '', providerName: manifestLoginProvider.forProviderName, displayName: manifestLoginProvider.meta?.label ?? manifestLoginProvider.forProviderName ?? manifestLoginProvider.name, From 9c682d51b520c740c0bc14759c277e109b6d64e7 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 22 Apr 2024 11:06:41 +0200 Subject: [PATCH 21/45] add path to show an error page --- .../src/apps/app/app-error.element.ts | 20 +++++++++++++++---- .../src/apps/app/app.element.ts | 9 +++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts index a071967bc6..651854b75e 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts @@ -7,13 +7,21 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; */ @customElement('umb-app-error') export class UmbAppErrorElement extends UmbLitElement { + /** + * The headline to display + * + * @attr + */ + @property() + errorHeadline?: string | null; + /** * The error message to display * * @attr */ @property() - errorMessage?: string; + errorMessage?: string | null; /** * The error to display @@ -62,15 +70,19 @@ export class UmbAppErrorElement extends UmbLitElement {
-

Something went wrong

+

+ ${this.errorHeadline + ? html` An unknown failure has occured ` + : this.errorHeadline} +

${this.errorMessage}

${this.error ? html`
- Details + Details ${this.renderError(this.error)}
- ` + ` : nothing}
diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts index 3991d8ad3e..063b7a4d60 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts @@ -66,6 +66,15 @@ export class UmbAppElement extends UmbLitElement { this.#authController.makeAuthorizationRequest('loggedOut'); }, }, + { + path: 'error', + component: () => import('./app-error.element.js'), + setup: (component) => { + const searchParams = new URLSearchParams(window.location.search); + (component as UmbAppErrorElement).errorHeadline = searchParams.get('status'); + (component as UmbAppErrorElement).errorMessage = searchParams.get('error_description'); + }, + }, { path: '**', component: () => import('../backoffice/backoffice.element.js'), From bd930eaa55ed8a279abc99b33b4b7846fe918926 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 22 Apr 2024 15:21:46 +0200 Subject: [PATCH 22/45] add jsdoc --- .../src/packages/core/auth/auth.context.ts | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts index 09f2c773bb..a174004202 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts @@ -15,7 +15,7 @@ export class UmbAuthContext extends UmbContextBase { #isInitialized = new ReplaySubject(1); readonly isInitialized = this.#isInitialized.asObservable().pipe(filter((isInitialized) => isInitialized)); - #isBypassed = false; + #isBypassed; #serverUrl; #backofficePath; #authFlow; @@ -158,28 +158,49 @@ export class UmbAuthContext extends UmbContextBase { }; } + /** + * Sets the auth context as initialized, which means that the auth context is ready to be used. + * @remark This is used to let the app context know that the core module is ready, which means that the core auth providers are available. + */ setInitialized() { this.#isInitialized.next(true); } + /** + * Gets all registered auth providers. + */ getAuthProviders(extensionsRegistry: UmbBackofficeExtensionRegistry) { return this.isInitialized.pipe( switchMap(() => extensionsRegistry.byType<'authProvider', ManifestAuthProvider>('authProvider')), ); } + /** + * Gets the authorized redirect url. + * @returns The redirect url, which is the backoffice path. + */ getRedirectUrl() { return `${window.location.origin}${this.#backofficePath}`; } + /** + * Gets the post logout redirect url. + * @returns The post logout redirect url, which is the backoffice path with the logout path appended. + */ getPostLogoutRedirectUrl() { return `${window.location.origin}${this.#backofficePath.endsWith('/') ? this.#backofficePath : this.#backofficePath + '/'}logout`; } + /** + * @see UmbAuthFlow#linkLogin + */ linkLogin(provider: string) { return this.#authFlow.linkLogin(provider); } + /** + * @see UmbAuthFlow#unlinkLogin + */ unlinkLogin(providerName: string, providerKey: string) { return this.#authFlow.unlinkLogin(providerName, providerKey); } From 25c6c52cc56f9a0cd1b43545d47ac2381bc2a257 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 22 Apr 2024 15:22:19 +0200 Subject: [PATCH 23/45] when an external account is being linked, we need to redirect the whole user session to the backend api to issue a challenge --- .../src/packages/core/auth/auth-flow.ts | 29 +++---------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts index 284145fb44..bc5278ba3f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts @@ -341,40 +341,19 @@ export class UmbAuthFlow { } /** - * This method will link the current user to the specified provider. + * This method will link the current user to the specified provider by redirecting the user to the link endpoint. * @param provider The provider to link to. */ - async linkLogin(provider: string) { - const token = await this.performWithFreshTokens(); + linkLogin(provider: string): void { const url = new URL(this.#link_endpoint); url.searchParams.set('provider', provider); - const request = new Request(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, - body: JSON.stringify({ provider }), - redirect: 'manual', - }); - - const result = await fetch(request); - - if (result.status === 301 || result.status === 302) { - const redirectUrl = result.headers.get('Location'); - if (redirectUrl) { - location.href = redirectUrl; - } - } - - if (!result.ok) { - throw new Error('Failed to link login'); - } - - return true; + location.href = url.href; } /** * This method will unlink the current user from the specified provider. */ - async unlinkLogin(loginProvider: string, providerKey: string) { + async unlinkLogin(loginProvider: string, providerKey: string): Promise { const token = await this.performWithFreshTokens(); const request = new Request(this.#unlink_endpoint, { method: 'POST', From 4e1ee5e4f6d87494fce4cca89c2526761007b42d Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 22 Apr 2024 15:35:16 +0200 Subject: [PATCH 24/45] add localization to confirm --- .../src/assets/lang/da-dk.ts | 6 +++++- .../src/assets/lang/en-us.ts | 6 +++++- .../modals/external-login-modal.element.ts | 17 ++++++++++++++--- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts index e4c0f277e9..59f8bc50f5 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts @@ -553,12 +553,16 @@ export default { noIconsFound: 'Ingen ikoner blev fundet', noMacroParams: 'Der er ingen parametre for denne makro', noMacros: 'Der er ikke tilføjet nogen makroer', - externalLoginProviders: 'Eksterne login-udbydere', + externalLoginProviders: 'Eksternt login', exceptionDetail: 'Undtagelsesdetaljer', stacktrace: 'Stacktrace', innerException: 'Indre undtagelse', linkYour: 'Link din {0} konto', + linkYourConfirm: + 'For at linke dine Umbraco og {0} konti, vil du blive sendt til {0} for at bekræfte. Er du sikker på, at du vil fortsætte?', unLinkYour: 'Fjern link fra din {0} konto', + unLinkYourConfirm: + 'Du er ved at fjerne linket mellem dine Umbraco og {0} konti og du vil blive logget ud. Er du sikker på, at du vil fortsætte?', linkedToService: 'Din konto er linket til denne service', selectEditor: 'Vælg editor', selectEditorConfiguration: 'Vælg konfiguration', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts index 7c2cb63c8e..6b7064785b 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts @@ -560,12 +560,16 @@ export default { noIconsFound: 'No icons were found', noMacroParams: 'There are no parameters for this macro', noMacros: 'There are no macros available to insert', - externalLoginProviders: 'External login providers', + externalLoginProviders: 'External logins', exceptionDetail: 'Exception Details', stacktrace: 'Stacktrace', innerException: 'Inner Exception', linkYour: 'Link your {0} account', + linkYourConfirm: + 'You are about to link your Umbraco and {0} accounts and you will be redirected to {0} to confirm. Do you want to continue?', unLinkYour: 'Un-link your {0} account', + unLinkYourConfirm: + 'You are about to un-link your Umbraco and {0} accounts and you will be logged out. Do you want to continue?', linkedToService: 'Your account is linked to this service', selectEditor: 'Select editor', selectEditorConfiguration: 'Select configuration', diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts index 4cd29e13a0..ffe724cf29 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts @@ -139,8 +139,19 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { } async #onProviderEnable(item: UmbExternalLoginProviderOption) { - const authContext = await this.getContext(UMB_AUTH_CONTEXT); - authContext.linkLogin(item.providerName); + try { + const providerDisplayName = this.localize.string(item.displayName); + await umbConfirmModal(this, { + headline: this.localize.term('defaultdialogs_linkYour', providerDisplayName), + content: this.localize.term('defaultdialogs_linkYourConfirm', providerDisplayName), + confirmLabel: this.localize.term('general_continue'), + color: 'positive', + }); + const authContext = await this.getContext(UMB_AUTH_CONTEXT); + authContext.linkLogin(item.providerName); + } catch { + // Do nothing + } } async #onProviderDisable(item: UmbExternalLoginProviderOption) { @@ -149,7 +160,7 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { await umbConfirmModal(this, { headline: this.localize.term('defaultdialogs_unLinkYour', providerDisplayName), content: this.localize.term('defaultdialogs_unLinkYourConfirm', providerDisplayName), - confirmLabel: this.localize.term('general_unlink'), + confirmLabel: this.localize.term('general_continue'), color: 'danger', }); const authContext = await this.getContext(UMB_AUTH_CONTEXT); From 41af0e9ffb9cf31c08d6eac39153f4f3906550cb Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 22 Apr 2024 15:38:48 +0200 Subject: [PATCH 25/45] add correct asset paths --- src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts index 651854b75e..c40bdd36a5 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts @@ -65,7 +65,7 @@ export class UmbAppErrorElement extends UmbLitElement {
@@ -95,7 +95,7 @@ export class UmbAppErrorElement extends UmbLitElement { background-position: 50%; background-repeat: no-repeat; background-size: cover; - background-image: url('/umbraco/backoffice/assets/umbraco_background.jpg'); + background-image: url('/umbraco/backoffice/assets/installer-illustration.svg'); width: 100vw; height: 100vh; } From ca31e24ce633700c7bd7eac0ec1b009a5bd92d6f Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:14:49 +0200 Subject: [PATCH 26/45] add error handling for umbraco external login errors with openid --- .../src/apps/app/app-error.element.ts | 121 ++++++++++++++++-- .../src/apps/app/app.element.ts | 13 +- .../src/assets/lang/da-dk.ts | 20 ++- .../src/assets/lang/en-us.ts | 16 +++ 4 files changed, 147 insertions(+), 23 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts index c40bdd36a5..5f5c46710d 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts @@ -31,31 +31,128 @@ export class UmbAppErrorElement extends UmbLitElement { @property() error?: unknown; - private renderProblemDetails = (problemDetails: ProblemDetails) => html` + constructor() { + super(); + + this.#generateErrorFromSearchParams(); + } + + /** + * Generates an error from the search params before the properties are set + */ + #generateErrorFromSearchParams() { + const searchParams = new URLSearchParams(window.location.search); + + const flow = searchParams.get('flow'); + + if (flow === 'external-login-callback') { + this.errorHeadline = this.localize.term('errors_externalLoginError'); + console.log('External login error', searchParams.get('error')); + + const status = searchParams.get('status'); + + // "Status" is controlled by Umbraco and is a string + if (status) { + switch (status) { + case 'unauthorized': + this.errorMessage = this.localize.term('errors_unauthorized'); + break; + case 'user-not-found': + this.errorMessage = this.localize.term('errors_userNotFound'); + break; + case 'external-info-not-found': + this.errorMessage = this.localize.term('errors_externalInfoNotFound'); + break; + case 'failed': + this.errorMessage = this.localize.term('errors_externalLoginFailed'); + break; + default: + this.errorMessage = this.localize.term('errors_defaultError'); + break; + } + } + return; + } + + if (flow === 'external-login') { + /** + * "Error" is controlled by OpenID and is a string + * @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1 + */ + const error = searchParams.get('error'); + + this.errorHeadline = this.localize.term('errors_externalLoginError'); + + switch (error) { + case 'access_denied': + this.errorMessage = this.localize.term('openidErrors_accessDenied'); + break; + case 'invalid_request': + this.errorMessage = this.localize.term('openidErrors_invalidRequest'); + break; + case 'invalid_client': + this.errorMessage = this.localize.term('openidErrors_invalidClient'); + break; + case 'invalid_grant': + this.errorMessage = this.localize.term('openidErrors_invalidGrant'); + break; + case 'unauthorized_client': + this.errorMessage = this.localize.term('openidErrors_unauthorizedClient'); + break; + case 'unsupported_grant_type': + this.errorMessage = this.localize.term('openidErrors_unsupportedGrantType'); + break; + case 'invalid_scope': + this.errorMessage = this.localize.term('openidErrors_invalidScope'); + break; + case 'server_error': + this.errorMessage = this.localize.term('openidErrors_serverError'); + break; + case 'temporarily_unavailable': + this.errorMessage = this.localize.term('openidErrors_temporarilyUnavailable'); + break; + default: + this.errorMessage = this.localize.term('errors_defaultError'); + break; + } + + // Set the error object with the original error parameters from the search params + let detail = searchParams.get('error_description'); + const errorUri = searchParams.get('error_uri'); + if (errorUri) { + detail = `${detail} (${errorUri})`; + } + this.error = { title: `External error code: ${error}`, detail }; + + return; + } + } + + #renderProblemDetails = (problemDetails: ProblemDetails) => html`

${problemDetails.title}

${problemDetails.detail}

${problemDetails.stack}
`; - private renderErrorObj = (error: Error) => html` + #renderErrorObj = (error: Error) => html`

${error.name}

${error.message}

${error.stack}
`; - private isProblemDetails(error: unknown): error is ProblemDetails { + #isProblemDetails(error: unknown): error is ProblemDetails { return typeof error === 'object' && error !== null && 'detail' in error && 'title' in error; } - private isError(error: unknown): error is Error { + #isError(error: unknown): error is Error { return typeof error === 'object' && error !== null && error instanceof Error; } - private renderError(error: unknown) { - if (this.isProblemDetails(error)) { - return this.renderProblemDetails(error); - } else if (this.isError(error)) { - return this.renderErrorObj(error); + #renderError(error: unknown) { + if (this.#isProblemDetails(error)) { + return this.#renderProblemDetails(error); + } else if (this.#isError(error)) { + return this.#renderErrorObj(error); } return nothing; @@ -72,15 +169,15 @@ export class UmbAppErrorElement extends UmbLitElement {

${this.errorHeadline - ? html` An unknown failure has occured ` - : this.errorHeadline} + ? this.errorHeadline + : html` An unknown failure has occured `}

${this.errorMessage}

${this.error ? html`
Details - ${this.renderError(this.error)} + ${this.#renderError(this.error)}
` : nothing} diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts index 063b7a4d60..a00de17c1f 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts @@ -50,6 +50,10 @@ export class UmbAppElement extends UmbLitElement { bypassAuth = false; private _routes: UmbRoute[] = [ + { + path: 'error', + component: () => import('./app-error.element.js'), + }, { path: 'install', component: () => import('../installer/installer.element.js'), @@ -66,15 +70,6 @@ export class UmbAppElement extends UmbLitElement { this.#authController.makeAuthorizationRequest('loggedOut'); }, }, - { - path: 'error', - component: () => import('./app-error.element.js'), - setup: (component) => { - const searchParams = new URLSearchParams(window.location.search); - (component as UmbAppErrorElement).errorHeadline = searchParams.get('status'); - (component as UmbAppErrorElement).errorMessage = searchParams.get('error_description'); - }, - }, { path: '**', component: () => import('../backoffice/backoffice.element.js'), diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts index 59f8bc50f5..e2fe6d5cab 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts @@ -694,6 +694,8 @@ export default { errorRegExpWithoutTab: '%0% er ikke i et korrekt format', }, errors: { + defaultError: 'Der er sket en ukendt fejl', + concurrencyError: 'Optimistisk samtidighedsfejl, objektet er blevet ændret', receivedErrorFromServer: 'Der skete en fejl på severen', dissallowedMediaType: 'Denne filttype er blevet deaktiveret af administratoren', codemirroriewarning: @@ -711,8 +713,22 @@ export default { tableColMergeLeft: 'Du skal stå til venstre for de 2 celler du ønsker at samle!', tableSplitNotSplittable: 'Du kan ikke opdele en celle, som ikke allerede er delt.', propertyHasErrors: 'Denne egenskab er ugyldig', - defaultError: 'An unknown failure has occurred', - concurrencyError: 'Optimistic concurrency failure, object has been modified', + externalLoginError: 'Der opstod en fejl under login med eksternt login', + unauthorized: 'Du har ikke tilladelse til at udføre denne handling', + userNotFound: 'Den angivne bruger blev ikke fundet i databasen', + externalInfoNotFound: 'Serveren kunne ikke kommunikere med den eksterne loginudbyder', + externalLoginFailed: 'Serveren mislykkedes i at logge ind med den eksterne loginudbyder', + }, + openidErrors: { + accessDenied: 'Access denied', + invalidRequest: 'Ugyldig forespørgsel', + invalidClient: 'Ugyldig klient', + invalidGrant: 'Ugyldig tildeling', + unauthorizedClient: 'Uautoriseret klient', + unsupportedGrantType: 'Ikke understøttet tildelingstype', + invalidScope: 'Ugyldigt område', + serverError: 'Serverfejl', + temporarilyUnavailable: 'Servicen er midlertidigt utilgængelig', }, general: { options: 'Valgmuligheder', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts index 6b7064785b..72513e5835 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts @@ -718,6 +718,22 @@ export default { tableColMergeLeft: 'Please place cursor at the left of the two cells you wish to merge', tableSplitNotSplittable: "You cannot split a cell that hasn't been merged.", propertyHasErrors: 'This property is invalid', + externalLoginError: 'External login', + unauthorized: 'You were not authorized before performing this action', + userNotFound: 'The local user was not found in the database', + externalInfoNotFound: 'The server did not succeed in communicating with the external login provider', + externalLoginFailed: 'The server failed to authorize you against the external login provider', + }, + openidErrors: { + accessDenied: 'Access denied', + invalidRequest: 'Invalid request', + invalidClient: 'Invalid client', + invalidGrant: 'Invalid grant', + unauthorizedClient: 'Unauthorized client', + unsupportedGrantType: 'Unsupported grant type', + invalidScope: 'Invalid scope', + serverError: 'Server error', + temporarilyUnavailable: 'The service is temporarily unavailable', }, general: { options: 'Options', From fcc440570b29b088cb1554f1e015c01584943c8d Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:25:02 +0200 Subject: [PATCH 27/45] add styling to error page --- .../src/apps/app/app-error.element.ts | 106 ++++++++++-------- 1 file changed, 57 insertions(+), 49 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts index 5f5c46710d..a85454f494 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts @@ -1,6 +1,7 @@ import { css, html, nothing, customElement, property } from '@umbraco-cms/backoffice/external/lit'; import type { ProblemDetails } from '@umbraco-cms/backoffice/external/backend-api'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; /** * A full page error element that can be used either solo or for instance as the error 500 page and BootFailed @@ -129,13 +130,13 @@ export class UmbAppErrorElement extends UmbLitElement { } #renderProblemDetails = (problemDetails: ProblemDetails) => html` -

${problemDetails.title}

+

${problemDetails.title}

${problemDetails.detail}

${problemDetails.stack}
`; #renderErrorObj = (error: Error) => html` -

${error.name}

+

${error.name}

${error.message}

${error.stack}
`; @@ -161,18 +162,18 @@ export class UmbAppErrorElement extends UmbLitElement { render = () => html`
- `; - static styles = css` - #background { - position: fixed; - overflow: hidden; - background-position: 50%; - background-repeat: no-repeat; - background-size: cover; - background-image: url('/umbraco/backoffice/assets/installer-illustration.svg'); - width: 100vw; - height: 100vh; - } + static styles = [ + UmbTextStyles, + css` + #background { + position: fixed; + overflow: hidden; + background-position: 50%; + background-repeat: no-repeat; + background-size: cover; + background-image: url('/umbraco/backoffice/assets/installer-illustration.svg'); + width: 100vw; + height: 100vh; + } - #logo { - position: fixed; - top: var(--uui-size-space-5); - left: var(--uui-size-space-5); - height: 30px; - } + #logo { + position: fixed; + top: var(--uui-size-space-5); + left: var(--uui-size-space-5); + height: 30px; + } - #logo img { - height: 100%; - } + #logo img { + height: 100%; + } - #container { - position: relative; - display: flex; - align-items: center; - justify-content: center; - width: 100vw; - height: 100vh; - } + #container { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100vw; + height: 100vh; + } - #box { - width: 50vw; - padding: var(--uui-size-space-6) var(--uui-size-space-5) var(--uui-size-space-5) var(--uui-size-space-5); - } + #box { + width: 400px; + max-width: 80vw; + } - details { - padding: var(--uui-size-space-2) var(--uui-size-space-3); - background: var(--uui-color-surface-alt); - } + #message { + margin-bottom: var(--uui-size-space-3); + } - pre { - width: 100%; - overflow: auto; - } - `; + details { + padding: var(--uui-size-space-2) var(--uui-size-space-3); + background: var(--uui-color-surface-alt); + } + + pre { + width: 100%; + overflow: auto; + } + `, + ]; } export default UmbAppErrorElement; From b475f3715f69ba34ff1d771868e67e474380aff0 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:31:19 +0200 Subject: [PATCH 28/45] make texts slightly less intrusive --- src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts | 6 ++---- src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts | 5 ++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts index e2fe6d5cab..b01b315e0b 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts @@ -558,11 +558,9 @@ export default { stacktrace: 'Stacktrace', innerException: 'Indre undtagelse', linkYour: 'Link din {0} konto', - linkYourConfirm: - 'For at linke dine Umbraco og {0} konti, vil du blive sendt til {0} for at bekræfte. Er du sikker på, at du vil fortsætte?', + linkYourConfirm: 'For at linke dine Umbraco og {0} konti, vil du blive sendt til {0} for at bekræfte.', unLinkYour: 'Fjern link fra din {0} konto', - unLinkYourConfirm: - 'Du er ved at fjerne linket mellem dine Umbraco og {0} konti og du vil blive logget ud. Er du sikker på, at du vil fortsætte?', + unLinkYourConfirm: 'Du er ved at fjerne linket mellem dine Umbraco og {0} konti og du vil blive logget ud.', linkedToService: 'Din konto er linket til denne service', selectEditor: 'Vælg editor', selectEditorConfiguration: 'Vælg konfiguration', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts index 72513e5835..c85beb56d6 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts @@ -566,10 +566,9 @@ export default { innerException: 'Inner Exception', linkYour: 'Link your {0} account', linkYourConfirm: - 'You are about to link your Umbraco and {0} accounts and you will be redirected to {0} to confirm. Do you want to continue?', + 'You are about to link your Umbraco and {0} accounts and you will be redirected to {0} to confirm.', unLinkYour: 'Un-link your {0} account', - unLinkYourConfirm: - 'You are about to un-link your Umbraco and {0} accounts and you will be logged out. Do you want to continue?', + unLinkYourConfirm: 'You are about to un-link your Umbraco and {0} accounts and you will be logged out.', linkedToService: 'Your account is linked to this service', selectEditor: 'Select editor', selectEditorConfiguration: 'Select configuration', From 0d727619196e648728538aafa9f45918eefd709a Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:31:24 +0200 Subject: [PATCH 29/45] add a back button --- src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts index a85454f494..88a9272911 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts @@ -168,6 +168,11 @@ export class UmbAppErrorElement extends UmbLitElement {
+ (location.href = '')}>
${this.errorHeadline ? this.errorHeadline From ace1962e683d90659592c4a7f79136050b822a79 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 15 May 2024 09:15:42 +0200 Subject: [PATCH 30/45] route to frontpage --- src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts index 6900aebcb6..cf6ef5c252 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts @@ -86,7 +86,7 @@ export class UmbAppElement extends UmbLitElement { : this.localize.term('errors_externalLoginFailed'); this.observe(this.#authContext.authorizationSignal, () => { - window.location.href = '/'; + history.replaceState(null, '', '/'); }); } From 7c0676d5ee38d6a8b756cad993d5545def6cc6a4 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 15 May 2024 09:16:10 +0200 Subject: [PATCH 31/45] add link key endpoint --- .../src/packages/core/auth/auth-flow.ts | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts index b96098b2f5..8486dbb711 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts @@ -100,6 +100,7 @@ export class UmbAuthFlow { // external login #link_endpoint; + #link_key_endpoint; #unlink_endpoint; /** @@ -130,6 +131,7 @@ export class UmbAuthFlow { }); this.#link_endpoint = `${openIdConnectUrl}/umbraco/management/api/v1/security/back-office/link-login`; + this.#link_key_endpoint = `${openIdConnectUrl}/umbraco/management/api/v1/security/back-office/link-login-key`; this.#unlink_endpoint = `${openIdConnectUrl}/umbraco/management/api/v1/security/back-office/unlink-login`; this.#notifier = new AuthorizationNotifier(); @@ -331,10 +333,26 @@ export class UmbAuthFlow { * This method will link the current user to the specified provider by redirecting the user to the link endpoint. * @param provider The provider to link to. */ - linkLogin(provider: string): void { - const url = new URL(this.#link_endpoint); - url.searchParams.set('provider', provider); - location.href = url.href; + async linkLogin(provider: string): Promise { + const linkKey = await this.#makeLinkTokenRequest(provider); + + const form = document.createElement('form'); + form.method = 'POST'; + form.action = this.#link_endpoint; + form.style.display = 'none'; + + const providerInput = document.createElement('input'); + providerInput.name = 'provider'; + providerInput.value = provider; + form.appendChild(providerInput); + + const linkKeyInput = document.createElement('input'); + linkKeyInput.name = 'linkKey'; + linkKeyInput.value = linkKey; + form.appendChild(linkKeyInput); + + document.body.appendChild(form); + form.submit(); } /** @@ -423,4 +441,21 @@ export class UmbAuthFlow { return false; } } + + async #makeLinkTokenRequest(provider: string) { + const token = await this.performWithFreshTokens(); + + const request = await fetch(`${this.#link_key_endpoint}?provider=${provider}`, { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (!request.ok) { + throw new Error('Failed to link login'); + } + + return request.json(); + } } From 46e880af975a39c9e9086d32df484647a5b6d1b5 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 15 May 2024 09:16:20 +0200 Subject: [PATCH 32/45] show error notifications --- .../modals/external-login-modal.element.ts | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts index ffe724cf29..f9dc8c15c2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts @@ -7,6 +7,7 @@ import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { mergeObservables } from '@umbraco-cms/backoffice/observable-api'; import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'; +import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; type UmbExternalLoginProviderOption = UmbCurrentUserExternalLoginProviderModel & { displayName: string; @@ -23,10 +24,15 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { _items: Array = []; #currentUserRepository = new UmbCurrentUserRepository(this); + #notificationContext?: typeof UMB_NOTIFICATION_CONTEXT.TYPE; constructor() { super(); this.#loadProviders(); + + this.consumeContext(UMB_NOTIFICATION_CONTEXT, (context) => { + this.#notificationContext = context; + }); } async #loadProviders() { @@ -139,8 +145,8 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { } async #onProviderEnable(item: UmbExternalLoginProviderOption) { + const providerDisplayName = this.localize.string(item.displayName); try { - const providerDisplayName = this.localize.string(item.displayName); await umbConfirmModal(this, { headline: this.localize.term('defaultdialogs_linkYour', providerDisplayName), content: this.localize.term('defaultdialogs_linkYourConfirm', providerDisplayName), @@ -149,14 +155,21 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { }); const authContext = await this.getContext(UMB_AUTH_CONTEXT); authContext.linkLogin(item.providerName); - } catch { - // Do nothing + } catch (error) { + if (error instanceof Error) { + this.#notificationContext?.peek('danger', { + data: { + headline: this.localize.term('defaultdialogs_linkYour', providerDisplayName), + message: error.message, + }, + }); + } } } async #onProviderDisable(item: UmbExternalLoginProviderOption) { + const providerDisplayName = this.localize.string(item.displayName); try { - const providerDisplayName = this.localize.string(item.displayName); await umbConfirmModal(this, { headline: this.localize.term('defaultdialogs_unLinkYour', providerDisplayName), content: this.localize.term('defaultdialogs_unLinkYourConfirm', providerDisplayName), @@ -165,8 +178,15 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { }); const authContext = await this.getContext(UMB_AUTH_CONTEXT); authContext.unlinkLogin(item.providerName, item.providerKey); - } catch { - // Do nothing + } catch (error) { + if (error instanceof Error) { + this.#notificationContext?.peek('danger', { + data: { + headline: this.localize.term('defaultdialogs_unLinkYour', providerDisplayName), + message: error.message, + }, + }); + } } } From 2895e47b36a2e4ce2cc19b97f2251cb4184191e7 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 15 May 2024 09:58:21 +0200 Subject: [PATCH 33/45] chore: add localization --- src/Umbraco.Web.UI.Client/src/assets/lang/en.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index af418baa1b..83fdadb880 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -568,12 +568,16 @@ export default { noIconsFound: 'No icons were found', noMacroParams: 'There are no parameters for this macro', noMacros: 'There are no macros available to insert', - externalLoginProviders: 'External login providers', + externalLoginProviders: 'External logins', exceptionDetail: 'Exception Details', stacktrace: 'Stacktrace', innerException: 'Inner Exception', - linkYour: 'Link your', - unLinkYour: 'Un-link your', + linkYour: 'Link your {0} account', + linkYourConfirm: + 'You are about to link your Umbraco and {0} accounts and you will be redirected to {0} to confirm.', + unLinkYour: 'Un-link your {0} account', + unLinkYourConfirm: 'You are about to un-link your Umbraco and {0} accounts and you will be logged out.', + linkedToService: 'Your account is linked to this service', account: 'account', selectEditor: 'Select editor', selectSnippet: 'Select snippet', From 53fedfc66999ba997086b3b968573623f7ecd9b0 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 15 May 2024 11:25:26 +0200 Subject: [PATCH 34/45] generate new server models and use new login-providers endpoint --- .../src/external/backend-api/src/models.ts | 36 +++++++++++++++ .../src/external/backend-api/src/services.ts | 46 ++++++++++++++++++- .../modals/external-login-modal.element.ts | 2 +- .../repository/current-user.repository.ts | 4 +- .../current-user.server.data-source.ts | 8 +--- .../repository/current-user.store.ts | 7 ++- .../src/packages/user/current-user/types.ts | 4 +- 7 files changed, 92 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models.ts index da4e84c5d8..1eb1880137 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models.ts @@ -991,6 +991,15 @@ url?: string | null type?: string | null }; +export enum ImageCropModeModel { + CROP = 'Crop', + MAX = 'Max', + STRETCH = 'Stretch', + PAD = 'Pad', + BOX_PAD = 'BoxPad', + MIN = 'Min' +} + export type ImportDictionaryRequestModel = { temporaryFile: ReferenceByIdModel parent?: ReferenceByIdModel | null @@ -2574,6 +2583,12 @@ value: string key: string }; +export type UserExternalLoginProviderModel = { + providerSchemeName: string +isLinkedOnUser: boolean +hasManualLinkingEnabled: boolean + }; + export type UserGroupItemResponseModel = { id: string name: string @@ -3510,6 +3525,26 @@ tree?: string } +export type ImagingData = { + + payloads: { + GetImagingResizeUrls: { + height?: number +id?: Array +mode?: ImageCropModeModel +width?: number + + }; + } + + + responses: { + GetImagingResizeUrls: Array + + } + + } + export type IndexerData = { payloads: { @@ -5205,6 +5240,7 @@ PostUserUnlock: { ,PostUserCurrentAvatar: string ,PostUserCurrentChangePassword: string ,GetUserCurrentConfiguration: CurrenUserConfigurationResponseModel + ,GetUserCurrentLoginProviders: Array ,GetUserCurrentLogins: LinkedLoginsRequestModel ,GetUserCurrentPermissions: UserPermissionsResponseModel ,GetUserCurrentPermissionsDocument: Array diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services.ts index 84d88c42c7..c634a46642 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services.ts @@ -1,7 +1,7 @@ import type { CancelablePromise } from './core/CancelablePromise'; import { OpenAPI } from './core/OpenAPI'; import { request as __request } from './core/request'; -import type { CultureData, DataTypeData, DictionaryData, DocumentBlueprintData, DocumentTypeData, DocumentVersionData, DocumentData, DynamicRootData, HealthCheckData, HelpData, IndexerData, InstallData, LanguageData, LogViewerData, ManifestData, MediaTypeData, MediaData, MemberGroupData, MemberTypeData, MemberData, ModelsBuilderData, ObjectTypesData, OembedData, PackageData, PartialViewData, PreviewData, ProfilingData, PropertyTypeData, PublishedCacheData, RedirectManagementData, RelationTypeData, RelationData, ScriptData, SearcherData, SecurityData, SegmentData, ServerData, StaticFileData, StylesheetData, TagData, TelemetryData, TemplateData, TemporaryFileData, UpgradeData, UserDataData, UserGroupData, UserData, WebhookData } from './models'; +import type { CultureData, DataTypeData, DictionaryData, DocumentBlueprintData, DocumentTypeData, DocumentVersionData, DocumentData, DynamicRootData, HealthCheckData, HelpData, ImagingData, IndexerData, InstallData, LanguageData, LogViewerData, ManifestData, MediaTypeData, MediaData, MemberGroupData, MemberTypeData, MemberData, ModelsBuilderData, ObjectTypesData, OembedData, PackageData, PartialViewData, PreviewData, ProfilingData, PropertyTypeData, PublishedCacheData, RedirectManagementData, RelationTypeData, RelationData, ScriptData, SearcherData, SecurityData, SegmentData, ServerData, StaticFileData, StylesheetData, TagData, TelemetryData, TemplateData, TemporaryFileData, UpgradeData, UserDataData, UserGroupData, UserData, WebhookData } from './models'; export class CultureService { @@ -2917,6 +2917,35 @@ baseUrl } +export class ImagingService { + + /** + * @returns unknown Success + * @throws ApiError + */ + public static getImagingResizeUrls(data: ImagingData['payloads']['GetImagingResizeUrls'] = {}): CancelablePromise { + const { + + id, +height, +width, +mode + } = data; + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/imaging/resize/urls', + query: { + id, height, width, mode + }, + errors: { + 401: `The resource is protected and requires an authentication token`, + 403: `The authenticated user do not have access to this resource`, + }, + }); + } + +} + export class IndexerService { /** @@ -8657,6 +8686,21 @@ requestBody }); } + /** + * @returns unknown Success + * @throws ApiError + */ + public static getUserCurrentLoginProviders(): CancelablePromise { + + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/user/current/login-providers', + errors: { + 401: `The resource is protected and requires an authentication token`, + }, + }); + } + /** * @returns unknown Success * @throws ApiError diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts index f9dc8c15c2..9f3527fc1c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts @@ -48,7 +48,7 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { ([serverLoginProviders, manifestLoginProviders]) => { const providers: UmbExternalLoginProviderOption[] = manifestLoginProviders.map((manifestLoginProvider) => { const serverLoginProvider = serverLoginProviders.find( - (serverLoginProvider) => serverLoginProvider.providerName === manifestLoginProvider.forProviderName, + (serverLoginProvider) => serverLoginProvider.providerSchemeName === manifestLoginProvider.forProviderName, ); return { isEnabledOnUser: !!serverLoginProvider, diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.repository.ts index 8361439260..ca0db09ecb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.repository.ts @@ -49,10 +49,10 @@ export class UmbCurrentUserRepository extends UmbRepositoryBase { const { data, error } = await this.#currentUserSource.getExternalLoginProviders(); if (data) { - this.#currentUserStore?.setExternalLoginProviders(data.linkedLogins); + this.#currentUserStore?.setExternalLoginProviders(data); } - return { data: data?.linkedLogins, error, asObservable: () => this.#currentUserStore!.externalLoginProviders }; + return { data, error, asObservable: () => this.#currentUserStore!.externalLoginProviders }; } /** diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.server.data-source.ts index ee646af7e9..c18b311854 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.server.data-source.ts @@ -58,13 +58,7 @@ export class UmbCurrentUserServerDataSource { * @memberof UmbCurrentUserServerDataSource */ async getExternalLoginProviders() { - const { data, error } = await tryExecuteAndNotify(this.#host, UserService.getUserCurrentLogins()); - - if (data) { - return { data }; - } - - return { error }; + return tryExecuteAndNotify(this.#host, UserService.getUserCurrentLoginProviders()); } /** diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.store.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.store.ts index de5e13a73d..684e41e7dd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.store.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.store.ts @@ -17,7 +17,10 @@ export class UmbCurrentUserStore extends UmbContextBase { #mfaProviders = new UmbArrayState([], (e) => e.providerName); readonly mfaProviders = this.#mfaProviders.asObservable(); - #externalLoginProviders = new UmbArrayState([], (e) => e.providerName); + #externalLoginProviders = new UmbArrayState( + [], + (e) => e.providerSchemeName, + ); readonly externalLoginProviders = this.#externalLoginProviders.asObservable(); constructor(host: UmbControllerHost) { @@ -98,7 +101,7 @@ export class UmbCurrentUserStore extends UmbContextBase { } updateExternalLoginProvider(data: Partial) { - this.#externalLoginProviders.updateOne(data.providerName, data); + this.#externalLoginProviders.updateOne(data.providerSchemeName, data); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/types.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/types.ts index 3fea7a03c5..551d0d8910 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/types.ts @@ -2,8 +2,8 @@ import type { ApiError, CancelError, DocumentPermissionPresentationModel, - LinkedLoginModel, UnknownTypePermissionPresentationModel, + UserExternalLoginProviderModel, UserTwoFactorProviderModel, } from '@umbraco-cms/backoffice/external/backend-api'; @@ -26,7 +26,7 @@ export interface UmbCurrentUserModel { userName: string; } -export type UmbCurrentUserExternalLoginProviderModel = LinkedLoginModel; +export type UmbCurrentUserExternalLoginProviderModel = UserExternalLoginProviderModel; export type UmbCurrentUserMfaProviderModel = UserTwoFactorProviderModel; From 2a710e5aaca4e9f953340b5b0a6961ec3c2652a5 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 15 May 2024 11:28:53 +0200 Subject: [PATCH 35/45] make sure that external login condition checks for allowManualLinking --- .../conditions/user-allow-external-login-action.condition.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/user-allow-external-login-action.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/user-allow-external-login-action.condition.ts index 89b6aa140f..05af0c57d0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/user-allow-external-login-action.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/user-allow-external-login-action.condition.ts @@ -10,7 +10,7 @@ export class UmbUserAllowExternalLoginActionCondition extends UmbConditionBase (this.permitted = exts.length > 0), + (exts) => (this.permitted = exts.length > 0 && exts.some((ext) => ext.meta?.linking?.allowManualLinking)), '_userAllowExternalLoginActionConditionProviders', ); } From c6cd4294bb1c8aa12dcccb438a6ac85c5bc83507 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 15 May 2024 11:34:45 +0200 Subject: [PATCH 36/45] map model --- .../modals/external-login-modal.element.ts | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts index 9f3527fc1c..de76f337f6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts @@ -12,7 +12,6 @@ import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; type UmbExternalLoginProviderOption = UmbCurrentUserExternalLoginProviderModel & { displayName: string; icon?: string; - isEnabledOnUser: boolean; }; @customElement('umb-current-user-external-login-modal') @@ -51,13 +50,14 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { (serverLoginProvider) => serverLoginProvider.providerSchemeName === manifestLoginProvider.forProviderName, ); return { - isEnabledOnUser: !!serverLoginProvider, - icon: manifestLoginProvider.meta?.defaultView?.icon, + hasManualLinkingEnabled: serverLoginProvider?.hasManualLinkingEnabled ?? false, + isLinkedOnUser: serverLoginProvider?.isLinkedOnUser ?? false, providerKey: serverLoginProvider?.providerKey ?? '', - providerName: manifestLoginProvider.forProviderName, + providerSchemeName: manifestLoginProvider.forProviderName, + icon: manifestLoginProvider.meta?.defaultView?.icon, displayName: manifestLoginProvider.meta?.label ?? manifestLoginProvider.forProviderName ?? manifestLoginProvider.name, - }; + } satisfies UmbExternalLoginProviderOption; }); return providers; @@ -81,15 +81,10 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { return html`
- ${when( - this._items.length > 0, - () => html` - ${repeat( - this._items, - (item) => item.providerName, - (item) => this.#renderProvider(item), - )} - `, + ${repeat( + this._items, + (item) => item.providerSchemeName, + (item) => this.#renderProvider(item), )}
@@ -110,7 +105,7 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { ${this.localize.string(item.displayName)}
${when( - item.isEnabledOnUser, + item.isLinkedOnUser, () => html`

Your account is linked to this service @@ -154,7 +149,7 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { color: 'positive', }); const authContext = await this.getContext(UMB_AUTH_CONTEXT); - authContext.linkLogin(item.providerName); + authContext.linkLogin(item.providerSchemeName); } catch (error) { if (error instanceof Error) { this.#notificationContext?.peek('danger', { @@ -177,7 +172,7 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { color: 'danger', }); const authContext = await this.getContext(UMB_AUTH_CONTEXT); - authContext.unlinkLogin(item.providerName, item.providerKey); + authContext.unlinkLogin(item.providerSchemeName, item.providerKey); } catch (error) { if (error instanceof Error) { this.#notificationContext?.peek('danger', { From 5997c985a7cf46d22af8a42ca890d13ff4a613b6 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 15 May 2024 11:45:21 +0200 Subject: [PATCH 37/45] throw the error from the server --- .../src/packages/core/auth/auth-flow.ts | 3 ++- .../modals/external-login-modal.element.ts | 22 ++++++++++++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts index 8486dbb711..b359ddf352 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts @@ -369,7 +369,8 @@ export class UmbAuthFlow { const result = await fetch(request); if (!result.ok) { - throw new Error('Failed to unlink login'); + const error = await result.json(); + throw error; } await this.signOut(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts index de76f337f6..19aaf4c2ec 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts @@ -8,6 +8,7 @@ import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registr import { mergeObservables } from '@umbraco-cms/backoffice/observable-api'; import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'; import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; +import { ApiError, ProblemDetails } from '@umbraco-cms/backoffice/external/backend-api'; type UmbExternalLoginProviderOption = UmbCurrentUserExternalLoginProviderModel & { displayName: string; @@ -149,7 +150,7 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { color: 'positive', }); const authContext = await this.getContext(UMB_AUTH_CONTEXT); - authContext.linkLogin(item.providerSchemeName); + await authContext.linkLogin(item.providerSchemeName); } catch (error) { if (error instanceof Error) { this.#notificationContext?.peek('danger', { @@ -172,16 +173,21 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { color: 'danger', }); const authContext = await this.getContext(UMB_AUTH_CONTEXT); - authContext.unlinkLogin(item.providerSchemeName, item.providerKey); + await authContext.unlinkLogin(item.providerSchemeName, item.providerKey); } catch (error) { + let message = this.localize.term('errors_receivedErrorFromServer'); if (error instanceof Error) { - this.#notificationContext?.peek('danger', { - data: { - headline: this.localize.term('defaultdialogs_unLinkYour', providerDisplayName), - message: error.message, - }, - }); + message = error.message; + } else if (typeof error === 'object' && (error as ProblemDetails).title) { + message = (error as ProblemDetails).title ?? message; } + console.error('[External Login] Error unlinking provider: ', error); + this.#notificationContext?.peek('danger', { + data: { + headline: this.localize.term('defaultdialogs_unLinkYour', providerDisplayName), + message, + }, + }); } } From 9c0a9a15cfa88276a31f54aa07700851ab5ea389 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 15 May 2024 11:51:11 +0200 Subject: [PATCH 38/45] remove unused variable --- src/Umbraco.Web.UI.Client/src/assets/lang/en.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 83fdadb880..cfef63d647 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -578,7 +578,6 @@ export default { unLinkYour: 'Un-link your {0} account', unLinkYourConfirm: 'You are about to un-link your Umbraco and {0} accounts and you will be logged out.', linkedToService: 'Your account is linked to this service', - account: 'account', selectEditor: 'Select editor', selectSnippet: 'Select snippet', variantdeletewarning: From 7d2fbf086d3ddae861e270ce73a3bc833be31ff4 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Thu, 16 May 2024 15:53:33 +0200 Subject: [PATCH 39/45] feat: make sure to save and restore the current url when logging in --- src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts | 11 +++++++++-- .../src/packages/core/auth/auth.context.ts | 5 ++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts index e6cd9a1979..c689e1e66a 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts @@ -4,7 +4,7 @@ import { UmbAppContext } from './app.context.js'; import { UmbServerConnection } from './server-connection.js'; import { UmbAppAuthController } from './app-auth.controller.js'; import type { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'; -import { UmbAuthContext } from '@umbraco-cms/backoffice/auth'; +import { UMB_STORAGE_REDIRECT_URL, UmbAuthContext } from '@umbraco-cms/backoffice/auth'; import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; import { UUIIconRegistryEssential } from '@umbraco-cms/backoffice/external/uui'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @@ -86,7 +86,14 @@ export class UmbAppElement extends UmbLitElement { : this.localize.term('errors_externalLoginFailed'); this.observe(this.#authContext.authorizationSignal, () => { - history.replaceState(null, '', ''); + // Redirect to the saved state or root + let currentRoute = ''; + const savedRoute = sessionStorage.getItem(UMB_STORAGE_REDIRECT_URL); + if (savedRoute) { + sessionStorage.removeItem(UMB_STORAGE_REDIRECT_URL); + currentRoute = savedRoute.endsWith('logout') ? currentRoute : savedRoute; + } + history.replaceState(null, '', currentRoute); }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts index 852707b59a..d405753f15 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts @@ -1,6 +1,6 @@ import type { UmbBackofficeExtensionRegistry, ManifestAuthProvider } from '../extension-registry/index.js'; import { UmbAuthFlow } from './auth-flow.js'; -import { UMB_AUTH_CONTEXT, UMB_STORAGE_TOKEN_RESPONSE_NAME } from './auth.context.token.js'; +import { UMB_AUTH_CONTEXT, UMB_STORAGE_REDIRECT_URL, UMB_STORAGE_TOKEN_RESPONSE_NAME } from './auth.context.token.js'; import type { UmbOpenApiConfiguration } from './models/openApiConfiguration.js'; import { OpenAPI } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; @@ -106,6 +106,9 @@ export class UmbAuthContext extends UmbContextBase { ) { const redirectUrl = await this.#authFlow.makeAuthorizationRequest(identityProvider, usernameHint); if (redirect) { + // Save the current state + sessionStorage.setItem(UMB_STORAGE_REDIRECT_URL, window.location.href); + location.href = redirectUrl; return; } From 739aaec052446470cecbcfd6dc480a01be60eb50 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 17 May 2024 08:50:59 +0200 Subject: [PATCH 40/45] import type --- .../external-login/modals/external-login-modal.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts index 19aaf4c2ec..a9bb34467d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts @@ -8,7 +8,7 @@ import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registr import { mergeObservables } from '@umbraco-cms/backoffice/observable-api'; import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'; import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; -import { ApiError, ProblemDetails } from '@umbraco-cms/backoffice/external/backend-api'; +import type { ProblemDetails } from '@umbraco-cms/backoffice/external/backend-api'; type UmbExternalLoginProviderOption = UmbCurrentUserExternalLoginProviderModel & { displayName: string; From 9b45672abe0a9e9fcc528886fefc1282c7af116a Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 17 May 2024 09:31:08 +0200 Subject: [PATCH 41/45] generate new server models --- .../src/external/backend-api/src/models.ts | 11 +---------- .../src/external/backend-api/src/services.ts | 15 --------------- .../modals/external-login-modal.element.ts | 4 ++++ 3 files changed, 5 insertions(+), 25 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models.ts index 18dc20cc0f..78495eea2c 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models.ts @@ -1057,15 +1057,6 @@ fallbackIsoCode?: string | null isoCode: string }; -export type LinkedLoginModel = { - providerName: string -providerKey: string - }; - -export type LinkedLoginsRequestModel = { - linkedLogins: Array - }; - export type LogLevelCountsReponseModel = { information: number debug: number @@ -2585,6 +2576,7 @@ key: string export type UserExternalLoginProviderModel = { providerSchemeName: string +providerKey?: string | null isLinkedOnUser: boolean hasManualLinkingEnabled: boolean }; @@ -5241,7 +5233,6 @@ PostUserUnlock: { ,PostUserCurrentChangePassword: string ,GetUserCurrentConfiguration: CurrenUserConfigurationResponseModel ,GetUserCurrentLoginProviders: Array - ,GetUserCurrentLogins: LinkedLoginsRequestModel ,GetUserCurrentPermissions: UserPermissionsResponseModel ,GetUserCurrentPermissionsDocument: Array ,GetUserCurrentPermissionsMedia: UserPermissionsResponseModel diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services.ts index c634a46642..861e3352db 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services.ts @@ -8701,21 +8701,6 @@ requestBody }); } - /** - * @returns unknown Success - * @throws ApiError - */ - public static getUserCurrentLogins(): CancelablePromise { - - return __request(OpenAPI, { - method: 'GET', - url: '/umbraco/management/api/v1/user/current/logins', - errors: { - 401: `The resource is protected and requires an authentication token`, - }, - }); - } - /** * @returns unknown Success * @throws ApiError diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts index a9bb34467d..80ca2a89f5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts @@ -164,6 +164,10 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { } async #onProviderDisable(item: UmbExternalLoginProviderOption) { + if (!item.providerKey) { + throw new Error('Provider key is missing'); + } + const providerDisplayName = this.localize.string(item.displayName); try { await umbConfirmModal(this, { From c198eebfab1cfa8eec8b9d9e4e8d8f7b325c1f9e Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 17 May 2024 09:41:17 +0200 Subject: [PATCH 42/45] add badge to show if provider does not exist on the server --- .../modals/external-login-modal.element.ts | 19 ++++++++++++++++++- .../current-user-mfa-modal.element.ts | 18 +++++++++++++++++- .../modals/user-mfa/user-mfa-modal.element.ts | 18 +++++++++++++++++- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts index 80ca2a89f5..7fe4bf2c90 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts @@ -1,6 +1,6 @@ import { UmbCurrentUserRepository } from '../../repository/index.js'; import type { UmbCurrentUserExternalLoginProviderModel } from '../../types.js'; -import { css, customElement, html, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { umbConfirmModal, type UmbModalContext } from '@umbraco-cms/backoffice/modal'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; @@ -13,6 +13,7 @@ import type { ProblemDetails } from '@umbraco-cms/backoffice/external/backend-ap type UmbExternalLoginProviderOption = UmbCurrentUserExternalLoginProviderModel & { displayName: string; icon?: string; + existsOnServer: boolean; }; @customElement('umb-current-user-external-login-modal') @@ -51,6 +52,7 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { (serverLoginProvider) => serverLoginProvider.providerSchemeName === manifestLoginProvider.forProviderName, ); return { + existsOnServer: !!serverLoginProvider, hasManualLinkingEnabled: serverLoginProvider?.hasManualLinkingEnabled ?? false, isLinkedOnUser: serverLoginProvider?.isLinkedOnUser ?? false, providerKey: serverLoginProvider?.providerKey ?? '', @@ -105,6 +107,20 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { ${this.localize.string(item.displayName)}

+ ${when( + item.existsOnServer, + () => nothing, + () => + html`
+ + ! + +
`, + )} ${when( item.isLinkedOnUser, () => html` @@ -127,6 +143,7 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { this.#onProviderEnable(item)}> diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.element.ts index ffa32541ae..cf59db4ed0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.element.ts @@ -2,7 +2,7 @@ import { UMB_CURRENT_USER_MFA_ENABLE_PROVIDER_MODAL } from '../current-user-mfa- import { UmbCurrentUserRepository } from '../../repository/index.js'; import { UMB_CURRENT_USER_MFA_DISABLE_PROVIDER_MODAL } from '../current-user-mfa-disable-provider/current-user-mfa-disable-provider-modal.token.js'; import type { UmbCurrentUserMfaProviderModel } from '../../types.js'; -import { css, customElement, html, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UMB_MODAL_MANAGER_CONTEXT, type UmbModalContext } from '@umbraco-cms/backoffice/modal'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; @@ -11,6 +11,7 @@ import { mergeObservables } from '@umbraco-cms/backoffice/observable-api'; type UmbMfaLoginProviderOption = UmbCurrentUserMfaProviderModel & { displayName: string; + existsOnServer: boolean; }; @customElement('umb-current-user-mfa-modal') @@ -41,6 +42,7 @@ export class UmbCurrentUserMfaModalElement extends UmbLitElement { (serverLoginProvider) => serverLoginProvider.providerName === manifestLoginProvider.forProviderName, ); return { + existsOnServer: !!serverLoginProvider, isEnabledOnUser: serverLoginProvider?.isEnabledOnUser ?? false, providerName: serverLoginProvider?.providerName ?? manifestLoginProvider.forProviderName, displayName: @@ -91,6 +93,20 @@ export class UmbCurrentUserMfaModalElement extends UmbLitElement { #renderProvider(item: UmbMfaLoginProviderOption) { return html` + ${when( + item.existsOnServer, + () => nothing, + () => + html`
+ + ! + +
`, + )} ${when( item.isEnabledOnUser, () => html` diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-mfa/user-mfa-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-mfa/user-mfa-modal.element.ts index c10e921421..74805b9d30 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-mfa/user-mfa-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-mfa/user-mfa-modal.element.ts @@ -1,7 +1,7 @@ import { UmbUserRepository } from '../../repository/index.js'; import type { UmbUserMfaProviderModel } from '../../types.js'; import type { UmbUserMfaModalConfiguration } from './user-mfa-modal.token.js'; -import { css, customElement, html, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { umbConfirmModal, type UmbModalContext } from '@umbraco-cms/backoffice/modal'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; @@ -10,6 +10,7 @@ import { mergeObservables } from '@umbraco-cms/backoffice/observable-api'; type UmbMfaLoginProviderOption = UmbUserMfaProviderModel & { displayName: string; + existsOnServer: boolean; }; @customElement('umb-user-mfa-modal') @@ -41,6 +42,7 @@ export class UmbUserMfaModalElement extends UmbLitElement { (serverLoginProvider) => serverLoginProvider.providerName === manifestLoginProvider.forProviderName, ); return { + existsOnServer: !!serverLoginProvider, isEnabledOnUser: serverLoginProvider?.isEnabledOnUser ?? false, providerName: serverLoginProvider?.providerName ?? manifestLoginProvider.forProviderName, displayName: @@ -91,6 +93,20 @@ export class UmbUserMfaModalElement extends UmbLitElement { #renderProvider(item: UmbMfaLoginProviderOption) { return html` + ${when( + item.existsOnServer, + () => nothing, + () => + html`
+ + ! + +
`, + )} ${when( item.isEnabledOnUser, () => html` From 0c7aa8d7e4fd8fa4417bf2f864e3e35d40083162 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 17 May 2024 11:28:20 +0200 Subject: [PATCH 43/45] update mock handlers --- .../src/mocks/data/user/user.data.ts | 4 --- .../src/mocks/handlers/manifests.handlers.ts | 23 +++++++---------- .../mocks/handlers/user/current.handlers.ts | 25 +++++++++++-------- 3 files changed, 23 insertions(+), 29 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/user/user.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/user/user.data.ts index ddbd634a65..a1657a2531 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/user/user.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/user/user.data.ts @@ -128,8 +128,4 @@ export const mfaLoginProviders: Array = [ isEnabledOnUser: false, providerName: 'sms', }, - { - isEnabledOnUser: true, - providerName: 'Email', - }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts index 4806994c0d..b8a500f4bb 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts @@ -76,6 +76,15 @@ const privateManifests: PackageManifestResponse = [ label: 'Setup SMS Verification', }, }, + { + type: 'mfaLoginProvider', + alias: 'My.MfaLoginProvider.Custom.Email', + name: 'My Custom Email MFA Provider', + forProviderName: 'email', + meta: { + label: 'Setup Email Verification', + }, + }, ], }, { @@ -92,20 +101,6 @@ const privateManifests: PackageManifestResponse = [ }, ], }, - { - name: 'My MFA Package', - extensions: [ - { - type: 'mfaLoginProvider', - alias: 'My.MfaLoginProvider.Custom', - name: 'My Custom MFA Provider', - forProviderName: 'sms', - meta: { - label: 'Setup SMS Verification', - }, - }, - ], - }, ]; const publicManifests: PackageManifestResponse = [ diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/current.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/current.handlers.ts index 9454d23c56..9c04e4b917 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/current.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/current.handlers.ts @@ -1,7 +1,7 @@ const { rest } = window.MockServiceWorker; import { umbUserMockDb } from '../../data/user/user.db.js'; import { UMB_SLUG } from './slug.js'; -import type { LinkedLoginsRequestModel } from '@umbraco-cms/backoffice/external/backend-api'; +import type { LinkedLoginsRequestModel, UserData } from '@umbraco-cms/backoffice/external/backend-api'; import { umbracoPath } from '@umbraco-cms/backoffice/utils'; export const handlers = [ @@ -9,19 +9,22 @@ export const handlers = [ const loggedInUser = umbUserMockDb.getCurrentUser(); return res(ctx.status(200), ctx.json(loggedInUser)); }), - rest.get(umbracoPath(`${UMB_SLUG}/current/logins`), (_req, res, ctx) => { - return res( - ctx.status(200), - ctx.json({ - linkedLogins: [ + rest.get( + umbracoPath(`${UMB_SLUG}/current/login-providers`), + (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.json([ { + hasManualLinkingEnabled: true, + isLinkedOnUser: true, providerKey: 'google', - providerName: 'Umbraco.Google', + providerSchemeName: 'Umbraco.Google', }, - ], - }), - ); - }), + ]), + ); + }, + ), rest.get(umbracoPath(`${UMB_SLUG}/current/2fa`), (_req, res, ctx) => { const mfaLoginProviders = umbUserMockDb.getMfaLoginProviders(); return res(ctx.status(200), ctx.json(mfaLoginProviders)); From 57be37678d65e5af3338ee050fff3e9119fe338b Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 17 May 2024 11:29:12 +0200 Subject: [PATCH 44/45] remove old import --- .../src/mocks/handlers/user/current.handlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/current.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/current.handlers.ts index 9c04e4b917..cff60866fd 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/current.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/current.handlers.ts @@ -1,7 +1,7 @@ const { rest } = window.MockServiceWorker; import { umbUserMockDb } from '../../data/user/user.db.js'; import { UMB_SLUG } from './slug.js'; -import type { LinkedLoginsRequestModel, UserData } from '@umbraco-cms/backoffice/external/backend-api'; +import type { UserData } from '@umbraco-cms/backoffice/external/backend-api'; import { umbracoPath } from '@umbraco-cms/backoffice/utils'; export const handlers = [ From fb39c237a5b621d531cf5da3a5a1f66da038e7ac Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 17 May 2024 11:32:55 +0200 Subject: [PATCH 45/45] disable the enable buttons if the provider does not exist on the server --- .../external-login/modals/external-login-modal.element.ts | 1 + .../modals/current-user-mfa/current-user-mfa-modal.element.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts index 7fe4bf2c90..7804f8ab92 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts @@ -145,6 +145,7 @@ export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { look="secondary" color="success" .label=${this.localize.term('defaultdialogs_linkYour', item.displayName)} + ?disabled=${!item.existsOnServer || !item.hasManualLinkingEnabled} @click=${() => this.#onProviderEnable(item)}> Link your ${this.localize.string(item.displayName)} account diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.element.ts index cf59db4ed0..a740321f32 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.element.ts @@ -124,6 +124,7 @@ export class UmbCurrentUserMfaModalElement extends UmbLitElement { this.#onProviderEnable(item)}> `,