From 8dd629613dbd4b9e033b02f333edb39989a2bb5d Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 26 Mar 2024 13:57:57 +0100 Subject: [PATCH] show activation flow --- .../src/assets/lang/en-us.ts | 6 + .../mfa-provider-default.element.ts | 103 +++++++++++++----- .../current-user-mfa-modal.stories.ts | 5 +- ...current-user-mfa-provider-modal.element.ts | 17 +-- .../current-user-mfa-provider-modal.token.ts | 24 ++-- .../current-user-mfa-modal.element.ts | 27 +---- .../current-user-mfa-modal.stories.ts | 3 + .../src/packages/user/current-user/types.ts | 20 +++- 8 files changed, 127 insertions(+), 78 deletions(-) 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 6e91f79474..e4d42eb96f 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 @@ -1935,6 +1935,12 @@ export default { '2faProviderIsDisabledMsg': 'This two-factor provider is now disabled', '2faProviderIsNotDisabledMsg': 'Something went wrong with trying to disable this two-factor provider', '2faDisableForUser': 'Do you want to disable this two-factor provider for this user?', + '2faQrCodeAlt': 'QR code for two-factor authentication with {0}', + '2faQrCodeTitle': 'QR code for two-factor authentication with {0}', + '2faQrCodeDescription': 'Scan this QR code with your authenticator app to enable two-factor authentication', + '2faCodeInput': 'Verification code', + '2faCodeInputHelp': 'Please enter the verification code', + '2faInvalidCode': 'Invalid code entered', }, validation: { validation: 'Validation', diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/components/mfa-provider-default.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/components/mfa-provider-default.element.ts index 3da6ef9f7d..4ba21c91a5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/components/mfa-provider-default.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/components/mfa-provider-default.element.ts @@ -1,6 +1,6 @@ import type { UmbMfaProviderConfigurationElementProps } from '../types.js'; import { UserResource } from '@umbraco-cms/backoffice/external/backend-api'; -import { css, customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, property, state, query } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; @@ -11,14 +11,11 @@ export class UmbMfaProviderDefaultElement extends UmbLitElement implements UmbMf @property({ attribute: false }) providerName = ''; - @property({ type: Boolean, attribute: false }) - isEnabled = false; + @property({ attribute: false }) + enableProvider: (providerName: string, code: string, secret: string) => Promise = async () => false; @property({ attribute: false }) - onSubmit: (value: { code: string; secret?: string | undefined }) => void = () => {}; - - @property({ attribute: false }) - onClose = () => {}; + close = () => {}; @state() protected _loading = true; @@ -31,6 +28,9 @@ export class UmbMfaProviderDefaultElement extends UmbLitElement implements UmbMf protected notificationContext?: typeof UMB_NOTIFICATION_CONTEXT.TYPE; + @query('#code') + protected codeField?: HTMLInputElement; + constructor() { super(); @@ -75,23 +75,55 @@ export class UmbMfaProviderDefaultElement extends UmbLitElement implements UmbMf } return html` -
- -
-
- - ${this.localize.term('general_close')} - - - ${this.localize.term('general_submit')} - -
-
-
+ +
+ +
+ + ${this._qrCodeSetupImageUrl + ? html`
+

+ + Scan this QR code with your authenticator app to enable two-factor authentication + +

+ ${this.localize.term('user_2faQrCodeAlt')} +
` + : ''} + + + + + + +
+
+
+ + ${this.localize.term('general_close')} + + + ${this.localize.term('general_submit')} + +
+
+
+
`; } @@ -112,9 +144,19 @@ export class UmbMfaProviderDefaultElement extends UmbLitElement implements UmbMf * Submit the form with the code and secret back to the opener. * @param e The submit event */ - protected submit(e: SubmitEvent) { + protected async submit(e: SubmitEvent) { e.preventDefault(); - this.onSubmit({ code: '123456', secret: '123' }); + this.codeField?.setCustomValidity(''); + const formData = new FormData(e.target as HTMLFormElement); + const code = formData.get('code') as string; + const successful = await this.enableProvider(this.providerName, code, this._secret); + debugger; + if (successful) { + this.peek('Two-factor authentication has successfully been enabled.'); + } else { + this.codeField?.setCustomValidity(this.localize.term('user_2faInvalidCode')); + this.codeField?.focus(); + } } static styles = [ @@ -123,6 +165,15 @@ export class UmbMfaProviderDefaultElement extends UmbLitElement implements UmbMf #authForm { height: 100%; } + + #code { + width: 100%; + max-width: 300px; + } + + .text-center { + text-align: center; + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa-provider/current-user-mfa-modal.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa-provider/current-user-mfa-modal.stories.ts index 040f538f65..d670626747 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa-provider/current-user-mfa-modal.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa-provider/current-user-mfa-modal.stories.ts @@ -11,10 +11,13 @@ const meta: Meta = { decorators: [(Story) => html`
${Story()}
`], args: { providerName: 'SMS', - isEnabled: true, + enableProvider: async (_provider, code) => (code === 'fail' ? false : true), }, parameters: { layout: 'centered', + actions: { + disabled: true, + }, }, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa-provider/current-user-mfa-provider-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa-provider/current-user-mfa-provider-modal.element.ts index 4ad992a8f5..e6e95cfc79 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa-provider/current-user-mfa-provider-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa-provider/current-user-mfa-provider-modal.element.ts @@ -1,8 +1,5 @@ import type { UmbMfaProviderConfigurationElementProps } from '../../types.js'; -import type { - UmbCurrentUserMfaProviderModalConfig, - UmbCurrentUserMfaProviderModalValue, -} from './current-user-mfa-provider-modal.token.js'; +import type { UmbCurrentUserMfaProviderModalConfig } from './current-user-mfa-provider-modal.token.js'; import type { ManifestMfaLoginProvider } from '@umbraco-cms/backoffice/extension-registry'; import { customElement, html } from '@umbraco-cms/backoffice/external/lit'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; @@ -12,13 +9,8 @@ import '../../components/mfa-provider-default.element.js'; @customElement('umb-current-user-mfa-provider-modal') export class UmbCurrentUserMfaProviderModalElement extends UmbModalBaseElement< UmbCurrentUserMfaProviderModalConfig, - UmbCurrentUserMfaProviderModalValue + never > { - #submit = (value: UmbCurrentUserMfaProviderModalValue) => { - this.value = value; - this._submitModal(); - }; - #close = () => { this._rejectModal(); }; @@ -26,9 +18,8 @@ export class UmbCurrentUserMfaProviderModalElement extends UmbModalBaseElement< get #extensionSlotProps(): UmbMfaProviderConfigurationElementProps { return { providerName: this.data!.providerName, - isEnabled: this.data!.isEnabled, - onSubmit: this.#submit, - onClose: this.#close, + enableProvider: this.data!.repository.enableMfaProvider, + close: this.#close, }; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa-provider/current-user-mfa-provider-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa-provider/current-user-mfa-provider-modal.token.ts index e24b424596..54da217c0c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa-provider/current-user-mfa-provider-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa-provider/current-user-mfa-provider-modal.token.ts @@ -1,21 +1,17 @@ +import type { UmbCurrentUserRepository } from '../../repository/index.js'; import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; export interface UmbCurrentUserMfaProviderModalConfig { providerName: string; - isEnabled: boolean; + repository: UmbCurrentUserRepository; } -export interface UmbCurrentUserMfaProviderModalValue { - secret?: string; - code?: string; -} - -export const UMB_CURRENT_USER_MFA_PROVIDER_MODAL = new UmbModalToken< - UmbCurrentUserMfaProviderModalConfig, - UmbCurrentUserMfaProviderModalValue ->('Umb.Modal.CurrentUserMfaProvider', { - modal: { - type: 'sidebar', - size: 'small', +export const UMB_CURRENT_USER_MFA_PROVIDER_MODAL = new UmbModalToken( + 'Umb.Modal.CurrentUserMfaProvider', + { + modal: { + type: 'sidebar', + size: 'small', + }, }, -}); +); 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 b38033b9ef..9d5e633192 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 @@ -1,7 +1,4 @@ -import { - UMB_CURRENT_USER_MFA_PROVIDER_MODAL, - type UmbCurrentUserMfaProviderModalValue, -} from '../current-user-mfa-provider/current-user-mfa-provider-modal.token.js'; +import { UMB_CURRENT_USER_MFA_PROVIDER_MODAL } from '../current-user-mfa-provider/current-user-mfa-provider-modal.token.js'; import { UmbCurrentUserRepository } from '../../repository/index.js'; import type { UmbCurrentUserMfaProviderModel } from '../../types.js'; import { customElement, html, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; @@ -80,26 +77,14 @@ export class UmbCurrentUserMfaModalElement extends UmbLitElement { event.preventDefault(); event.stopPropagation(); - const { code, secret } = await this.#openProviderModal(item); - - // If no code, do nothing - if (!code) { - return; - } - // If already enabled, disable it if (item.isEnabledOnUser) { // Disable provider - return this.#currentUserRepository.disableMfaProvider(item.providerName, code); + alert('TODO: Implement disabling provider'); } // Enable provider - // If no secret, do nothing - if (!secret) { - return; - } - - return this.#currentUserRepository.enableMfaProvider(item.providerName, code, secret); + await this.#openProviderModal(item); } /** @@ -107,11 +92,11 @@ export class UmbCurrentUserMfaModalElement extends UmbLitElement { * This will show the QR code and/or other means of validation for the given provider and return the activation code. * The activation code is then used to either enable or disable the provider. */ - async #openProviderModal(item: UmbCurrentUserMfaProviderModel): Promise { + async #openProviderModal(item: UmbCurrentUserMfaProviderModel) { const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); - return modalManager + return await modalManager .open(this, UMB_CURRENT_USER_MFA_PROVIDER_MODAL, { - data: { providerName: item.providerName, isEnabled: item.isEnabledOnUser }, + data: { providerName: item.providerName, repository: this.#currentUserRepository }, }) .onSubmit() .catch(() => ({})); diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.stories.ts index 662be9699d..e95eebc970 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.stories.ts @@ -11,6 +11,9 @@ const meta: Meta = { decorators: [(Story) => html`
${Story()}
`], parameters: { layout: 'centered', + actions: { + disabled: true, + }, }, }; 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 a9f2805220..827967c02a 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 @@ -23,8 +23,22 @@ export interface UmbCurrentUserModel { export type UmbCurrentUserMfaProviderModel = UserTwoFactorProviderModel; export interface UmbMfaProviderConfigurationElementProps { + /** + * The name of the provider reflecting the provider name in the backend. + */ providerName: string; - isEnabled: boolean; - onSubmit: (value: { code: string; secret?: string }) => void; - onClose: () => void; + + /** + * Enable the provider with the given code and secret. + * @param providerName The name of the provider to enable. + * @param code The authentication code from the authentication method. + * @param secret The secret from the authentication backend. + * @returns True if the provider was enabled successfully. + */ + enableProvider: (providerName: string, code: string, secret: string) => Promise; + + /** + * Call this function to close the modal. + */ + close: () => void; }