add modal to configure mfa providers

This commit is contained in:
Jacob Overgaard
2024-03-25 18:11:19 +01:00
parent fe5ecb032b
commit c5d33633e2
5 changed files with 173 additions and 67 deletions

View File

@@ -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<UmbExtensionElementInitializer<ManifestMfaLoginProvider>> = [];
#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`
<uui-box headline=${this.localize.term('member_2fa')}>
${repeat(
this._items,
(item) => item.alias,
(item) => item.component,
)}
</uui-box>
`,
);
if (!this._hasProviders) {
return nothing;
}
return html`
<uui-box .headline=${this.localize.term('member_2fa')}>
<uui-button type="button" look="primary" @click=${this.#onClick}>
<umb-localize key="user_configureTwoFactor">Configure Two Factor</umb-localize>
</uui-button>
</uui-box>
`;
}
async #onClick() {
const modalManagerContext = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
await modalManagerContext.open(this, UMB_CURRENT_USER_MFA_MODAL).onSubmit();
}
static styles = [UmbTextStyles];

View File

@@ -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<UserTwoFactorProviderModel> = [];
#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`
<umb-body-layout headline="${this.localize.term('member_2fa')}">
<div id="main">
${when(
this._items.length > 0,
() => html`
${repeat(
this._items,
(item) => item.providerName,
(item) => this.#renderProvider(item),
)}
`,
)}
</div>
<div slot="actions">
<uui-button @click=${this.#close} look="secondary" .label=${this.localize.term('general_close')}>
${this.localize.term('general_close')}
</uui-button>
</div>
</umb-body-layout>
`;
}
/**
* Render a provider with a toggle to enable/disable it
*/
#renderProvider(item: UserTwoFactorProviderModel) {
return html`
<div>
<uui-toggle
label=${item.providerName}
?checked=${item.isEnabledOnUser}
@change=${() => this.#onProviderToggleChange(item)}></uui-toggle>
</div>
`;
}
#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;
}
}

View File

@@ -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',
},
});

View File

@@ -7,6 +7,12 @@ const modals: Array<ManifestModal> = [
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];

View File

@@ -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<boolean> {
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<boolean> {
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<boolean> {
const { error } = await tryExecuteAndNotify(
this._host,
UserResource.deleteUserCurrent2FaByProviderName({ providerName: provider, code }),
);
if (error) {
return false;
}
return true;
}
}
export default UmbCurrentUserRepository;