show activation flow

This commit is contained in:
Jacob Overgaard
2024-03-26 13:57:57 +01:00
parent 41863232d1
commit 8dd629613d
8 changed files with 127 additions and 78 deletions

View File

@@ -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',

View File

@@ -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<boolean> = 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`
<form id="authForm" name="authForm" @submit=${this.submit}>
<umb-body-layout headline=${this.providerName}>
<div id="main"></div>
<div slot="actions">
<uui-button
type="button"
look="secondary"
.label=${this.localize.term('general_close')}
@click=${this.onClose}>
${this.localize.term('general_close')}
</uui-button>
<uui-button type="submit" look="primary" .label=${this.localize.term('buttons_save')}>
${this.localize.term('general_submit')}
</uui-button>
</div>
</umb-body-layout>
</form>
<uui-form>
<form id="authForm" name="authForm" @submit=${this.submit} novalidate>
<umb-body-layout headline=${this.providerName}>
<div id="main">
<uui-box .headline=${this.localize.term('member_2fa')}>
${this._qrCodeSetupImageUrl
? html` <div class="text-center">
<p>
<umb-localize key="user_2faQrCodeDescription">
Scan this QR code with your authenticator app to enable two-factor authentication
</umb-localize>
</p>
<img
.src=${this._qrCodeSetupImageUrl}
alt=${this.localize.term('user_2faQrCodeAlt')}
title=${this.localize.term('user_2faQrCodeTitle')} />
</div>`
: ''}
<uui-form-layout-item class="text-center">
<uui-label for="code" slot="label" required>
<umb-localize key="user_2faCodeInput"></umb-localize>
</uui-label>
<uui-input
id="code"
name="code"
type="text"
inputmode="numeric"
autocomplete="one-time-code"
required
required-message=${this.localize.term('general_required')}
placeholder=${this.localize.term('user_2faCodeInputHelp')}></uui-input>
</uui-form-layout-item>
</uui-box>
</div>
<div slot="actions">
<uui-button
type="button"
look="secondary"
.label=${this.localize.term('general_close')}
@click=${this.close}>
${this.localize.term('general_close')}
</uui-button>
<uui-button type="submit" look="primary" .label=${this.localize.term('buttons_save')}>
${this.localize.term('general_submit')}
</uui-button>
</div>
</umb-body-layout>
</form>
</uui-form>
`;
}
@@ -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;
}
`,
];
}

View File

@@ -11,10 +11,13 @@ const meta: Meta<UmbMfaProviderDefaultElement> = {
decorators: [(Story) => html`<div style="width: 500px; height: 500px;">${Story()}</div>`],
args: {
providerName: 'SMS',
isEnabled: true,
enableProvider: async (_provider, code) => (code === 'fail' ? false : true),
},
parameters: {
layout: 'centered',
actions: {
disabled: true,
},
},
};

View File

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

View File

@@ -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<UmbCurrentUserMfaProviderModalConfig, never>(
'Umb.Modal.CurrentUserMfaProvider',
{
modal: {
type: 'sidebar',
size: 'small',
},
},
});
);

View File

@@ -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<UmbCurrentUserMfaProviderModalValue> {
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(() => ({}));

View File

@@ -11,6 +11,9 @@ const meta: Meta<UmbCurrentUserMfaModalElement> = {
decorators: [(Story) => html`<div style="width: 500px; height: 500px;">${Story()}</div>`],
parameters: {
layout: 'centered',
actions: {
disabled: true,
},
},
};

View File

@@ -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<boolean>;
/**
* Call this function to close the modal.
*/
close: () => void;
}