diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/mfa-login/mfa-providers-user-profile-app.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/mfa-login/mfa-providers-user-profile-app.element.ts index bafb67f477..9451fabdb5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/mfa-login/mfa-providers-user-profile-app.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/mfa-login/mfa-providers-user-profile-app.element.ts @@ -1,84 +1,43 @@ import { UmbCurrentUserRepository } from '../repository/index.js'; -import { html, customElement, state, when, repeat } from '@umbraco-cms/backoffice/external/lit'; +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 { - type UmbExtensionElementInitializer, - UmbExtensionsElementInitializer, -} from '@umbraco-cms/backoffice/extension-api'; -import { type ManifestMfaLoginProvider, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; @customElement('umb-mfa-providers-user-profile-app') export class UmbMfaProvidersUserProfileAppElement extends UmbLitElement { - @state() - _items: Array> = []; - #currentUserRepository = new UmbCurrentUserRepository(this); - #extensionsInitializer?: UmbExtensionsElementInitializer< - ManifestMfaLoginProvider, - 'mfaLoginProvider', - ManifestMfaLoginProvider - >; + + @state() + _hasProviders = false; constructor() { super(); - - this.#extensionsInitializer = new UmbExtensionsElementInitializer< - ManifestMfaLoginProvider, - 'mfaLoginProvider', - ManifestMfaLoginProvider - >( - this, - umbExtensionsRegistry, - 'mfaLoginProvider', - undefined, - (permitted) => (this._items = permitted), - '_mfaLoginProviders', - 'umb-mfa-login-provider-default', - ); - - this.#loadProviders(); + this.#init(); } - async #loadProviders() { - const { data: providers } = await this.#currentUserRepository.requestMfaLoginProviders(); - - if (!providers) return; - - for (const provider of providers) { - // Check if provider is initialized as extension - const extension = this._items.find((item) => item.manifest?.forProviderNames.includes(provider.providerName)); - if (extension) { - extension.properties = { provider }; - } else { - // Register provider as extension - const manifest: ManifestMfaLoginProvider = { - type: 'mfaLoginProvider', - alias: provider.providerName, - name: provider.providerName, - forProviderNames: [provider.providerName], - meta: { - label: provider.providerName, - }, - }; - umbExtensionsRegistry.register(manifest); - } - } + async #init() { + this._hasProviders = await this.#currentUserRepository.hasMfaLoginProviders(); } render() { - return when( - this._items.length > 0, - () => html` - - ${repeat( - this._items, - (item) => item.alias, - (item) => item.component, - )} - - `, - ); + 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]; 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 new file mode 100644 index 0000000000..db10e254e7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.element.ts @@ -0,0 +1,89 @@ +import { UmbCurrentUserRepository } from '../../repository/index.js'; +import { customElement, html, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UserTwoFactorProviderModel } from '@umbraco-cms/backoffice/external/backend-api'; +import type { UmbModalContext } from '@umbraco-cms/backoffice/modal'; + +@customElement('umb-current-user-mfa-modal') +export class UmbCurrentUserMfaModalElement extends UmbLitElement { + @property({ attribute: false }) + modalContext?: UmbModalContext; + + @state() + _items: Array = []; + + #currentUserRepository = new UmbCurrentUserRepository(this); + + constructor() { + super(); + this.#loadProviders(); + } + + async #loadProviders() { + const { data: providers } = await this.#currentUserRepository.requestMfaLoginProviders(); + + if (!providers) return; + + this._items = providers; + } + + #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: UserTwoFactorProviderModel) { + return html` +
+ this.#onProviderToggleChange(item)}> +
+ `; + } + + #onProviderToggleChange = (item: UserTwoFactorProviderModel) => { + // If already enabled, disable it + if (item.isEnabledOnUser) { + // Disable provider + return; + } + + // Enable provider + }; +} + +export default UmbCurrentUserMfaModalElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-current-user-mfa-modal': UmbCurrentUserMfaModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.token.ts new file mode 100644 index 0000000000..4282e29043 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.token.ts @@ -0,0 +1,8 @@ +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export const UMB_CURRENT_USER_MFA_MODAL = new UmbModalToken('Umb.Modal.CurrentUserMfa', { + modal: { + type: 'sidebar', + size: 'small', + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/manifests.ts index c470772b20..7717917e26 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/manifests.ts @@ -7,6 +7,12 @@ const modals: Array = [ name: 'Current User Modal', js: () => import('./current-user/current-user-modal.element.js'), }, + { + type: 'modal', + alias: 'Umb.Modal.CurrentUserMfa', + name: 'Current User MFA Modal', + js: () => import('./current-user-mfa/current-user-mfa-modal.element.js'), + }, ]; export const manifests = [...modals]; 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 3be468d40a..903a128e49 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 @@ -1,7 +1,9 @@ import { UmbCurrentUserServerDataSource } from './current-user.server.data-source.js'; import { UMB_CURRENT_USER_STORE_CONTEXT } from './current-user.store.js'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; +import { UserResource } from '@umbraco-cms/backoffice/external/backend-api'; /** * A repository for the current user @@ -46,9 +48,51 @@ export class UmbCurrentUserRepository extends UmbRepositoryBase { * Request the current user's available MFA login providers * @memberof UmbCurrentUserRepository */ - async requestMfaLoginProviders() { + requestMfaLoginProviders() { return this.#currentUserSource.getMfaLoginProviders(); } + + async hasMfaLoginProviders(): Promise { + const { data } = await this.requestMfaLoginProviders(); + + return !!data; + } + + /** + * Enable an MFA provider + * @param provider The provider to enable + * @param code The activation code of the provider to enable + */ + async enableMfaProvider(provider: string, code: string): Promise { + const { error } = await tryExecuteAndNotify( + this._host, + UserResource.postUserCurrent2FaByProviderName({ providerName: provider, requestBody: { code, secret: code } }), + ); + + if (error) { + return false; + } + + return true; + } + + /** + * Disable an MFA provider + * @param provider The provider to disable + * @param code The activation code of the provider to disable + */ + async disableMfaProvider(provider: string, code: string): Promise { + const { error } = await tryExecuteAndNotify( + this._host, + UserResource.deleteUserCurrent2FaByProviderName({ providerName: provider, code }), + ); + + if (error) { + return false; + } + + return true; + } } export default UmbCurrentUserRepository;