Merge branch 'bugfix/document-recycle-bin' into feature/media-recycle-bin
This commit is contained in:
@@ -7,6 +7,9 @@ const IS_CI = process.env.CI === 'true';
|
||||
const IS_AZURE_PIPELINES = process.env.TF_BUILD === 'true';
|
||||
const IS_GITHUB_ACTIONS = process.env.GITHUB_ACTIONS === 'true';
|
||||
const FILE_PATH_COLOR = '\x1b[36m%s\x1b[0m';
|
||||
const ERROR_COLOR = '\x1b[31m%s\x1b[0m';
|
||||
const SUCCESS_COLOR = '\x1b[32m%s\x1b[0m';
|
||||
const processExitCode = 1; // Default to 1 to fail the build, 0 to just log the issues
|
||||
|
||||
console.log(`Checking path length in ${PROJECT_DIR} for paths exceeding ${MAX_PATH_LENGTH}...`);
|
||||
console.log('CI detected:', IS_CI);
|
||||
@@ -17,14 +20,12 @@ console.log('-----------------------------------\n');
|
||||
|
||||
function checkPathLength(dir) {
|
||||
const files = readdirSync(dir);
|
||||
let hasError = false;
|
||||
|
||||
files.forEach(file => {
|
||||
const filePath = join(dir, file);
|
||||
if (filePath.length > MAX_PATH_LENGTH) {
|
||||
|
||||
if (IS_CI) {
|
||||
//process.exitCode = 1; // TODO: Uncomment this line to fail the build
|
||||
}
|
||||
hasError = true;
|
||||
|
||||
if (IS_AZURE_PIPELINES) {
|
||||
console.error(`##vso[task.logissue type=warning;sourcepath=${filePath};]Path exceeds maximum length of ${MAX_PATH_LENGTH} characters: ${filePath} with ${filePath.length} characters`);
|
||||
@@ -36,9 +37,27 @@ function checkPathLength(dir) {
|
||||
}
|
||||
|
||||
if (statSync(filePath).isDirectory()) {
|
||||
checkPathLength(filePath, MAX_PATH_LENGTH);
|
||||
const subHasError = checkPathLength(filePath);
|
||||
if (subHasError) {
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return hasError;
|
||||
}
|
||||
|
||||
checkPathLength(PROJECT_DIR, MAX_PATH_LENGTH);
|
||||
const hasError = checkPathLength(PROJECT_DIR, MAX_PATH_LENGTH);
|
||||
|
||||
if (hasError) {
|
||||
console.error('\n-----------------------------------');
|
||||
console.error(ERROR_COLOR, 'Path length check failed');
|
||||
console.error('-----------------------------------\n');
|
||||
if (IS_CI && processExitCode) {
|
||||
process.exit(processExitCode);
|
||||
}
|
||||
} else {
|
||||
console.log('\n-----------------------------------');
|
||||
console.log(SUCCESS_COLOR, 'Path length check passed');
|
||||
console.log('-----------------------------------\n');
|
||||
}
|
||||
|
||||
@@ -1923,9 +1923,17 @@ export default {
|
||||
noUserGroupsAdded: 'Ingen brugere er blevet tilføjet',
|
||||
'2faDisableText': 'Hvis du ønsker at slå denne totrinsbekræftelse fra, så skal du nu indtaste koden fra din enhed:',
|
||||
'2faProviderIsEnabled': 'Denne totrinsbekræftelse er slået til',
|
||||
'2faProviderIsDisabledMsg': 'Den valgte totrinsbekræftelse er nu slået fra',
|
||||
'2faProviderIsNotDisabledMsg': 'Der skete en ukendt fejl da denne totrinsbekræftelse skulles slåes fra',
|
||||
'2faDisableForUser': 'Er du sikker på, at du vil fjerne denne totrinsbekræftelse for denne bruger?',
|
||||
'2faProviderIsEnabledMsg': '{0} er nu slået til',
|
||||
'2faProviderIsNotEnabledMsg': 'Der skete en fejl da {0} skulles slåes til',
|
||||
'2faProviderIsDisabledMsg': '{0} er nu slået fra',
|
||||
'2faProviderIsNotDisabledMsg': 'Der skete en fejl da {0} skulles slåes fra',
|
||||
'2faDisableForUser': 'Er du sikker på, at du vil fjerne "{0}" for denne bruger?',
|
||||
'2faQrCodeAlt': 'QR kode for totrinsbekræftelse med {0}',
|
||||
'2faQrCodeTitle': 'QR kode for totrinsbekræftelse med {0}',
|
||||
'2faQrCodeDescription': 'Scan QR koden med din autentificeringsapp',
|
||||
'2faCodeInput': 'Indtast din verifikationskode',
|
||||
'2faCodeInputHelp': 'Indtast din verifikationskode fra din autentificeringsapp',
|
||||
'2faInvalidCode': 'Den indtastede kode er ugyldig',
|
||||
emailRequired: 'Required - enter an email address for this user',
|
||||
duplicateLogin: 'A user with this login already exists',
|
||||
nameRequired: 'Required - enter a name for this user',
|
||||
|
||||
@@ -1932,11 +1932,11 @@ export default {
|
||||
'2faDisableText':
|
||||
'If you wish to disable this two-factor provider, then you must enter the code shown on your authentication device:',
|
||||
'2faProviderIsEnabled': 'This two-factor provider is enabled',
|
||||
'2faProviderIsEnabledMsg': 'This two-factor provider is now enabled',
|
||||
'2faProviderIsNotEnabledMsg': 'Something went wrong with trying to enable this two-factor provider',
|
||||
'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?',
|
||||
'2faProviderIsEnabledMsg': '{0} is now enabled',
|
||||
'2faProviderIsNotEnabledMsg': 'Something went wrong with trying to enable {0}',
|
||||
'2faProviderIsDisabledMsg': '{0} is now disabled',
|
||||
'2faProviderIsNotDisabledMsg': 'Something went wrong with trying to disable {0}',
|
||||
'2faDisableForUser': 'Do you want to disable "{0}" on 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',
|
||||
|
||||
@@ -60,10 +60,10 @@ export const data: Array<UmbMockUserModel> = [
|
||||
updateDate: '2023-10-12T18:30:32.879Z',
|
||||
createDate: '2023-10-12T18:30:32.879Z',
|
||||
failedLoginAttempts: 0,
|
||||
userGroupIds: ['user-group-administrators-id'],
|
||||
userGroupIds: ['user-group-editors-id'],
|
||||
userName: '',
|
||||
avatarUrls: [],
|
||||
isAdmin: true,
|
||||
isAdmin: false,
|
||||
},
|
||||
{
|
||||
id: 'ff2f4a50-d3d4-4bc4-869d-c7948c160e54',
|
||||
@@ -79,10 +79,10 @@ export const data: Array<UmbMockUserModel> = [
|
||||
updateDate: '2023-10-12T18:30:32.879Z',
|
||||
createDate: '2023-10-12T18:30:32.879Z',
|
||||
failedLoginAttempts: 0,
|
||||
userGroupIds: ['user-group-administrators-id'],
|
||||
userGroupIds: ['user-group-editors-id'],
|
||||
userName: '',
|
||||
avatarUrls: [],
|
||||
isAdmin: true,
|
||||
isAdmin: false,
|
||||
},
|
||||
{
|
||||
id: 'c290c6d9-9f12-4838-8567-621b52a178de',
|
||||
@@ -98,13 +98,17 @@ export const data: Array<UmbMockUserModel> = [
|
||||
updateDate: '2023-10-12T18:30:32.879Z',
|
||||
createDate: '2023-10-12T18:30:32.879Z',
|
||||
failedLoginAttempts: 25,
|
||||
userGroupIds: ['user-group-administrators-id'],
|
||||
userGroupIds: ['user-group-editors-id', 'user-group-sensitive-data-id'],
|
||||
userName: '',
|
||||
avatarUrls: [],
|
||||
isAdmin: true,
|
||||
isAdmin: false,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Mock data for MFA login providers
|
||||
* This is usually linked to a user, but for the sake of the mock, we're just going to have a list of providers
|
||||
*/
|
||||
export const mfaLoginProviders: Array<UserTwoFactorProviderModel> = [
|
||||
{
|
||||
isEnabledOnUser: true,
|
||||
|
||||
@@ -204,7 +204,7 @@ const createMockMapper = (item: CreateUserRequestModel): UmbMockUserModel => {
|
||||
lastLoginDate: null,
|
||||
lastLockoutDate: null,
|
||||
lastPasswordChangeDate: null,
|
||||
isAdmin: false,
|
||||
isAdmin: item.userGroupIds.includes(umbUserGroupMockDb.getAll()[0].id),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -81,8 +81,14 @@ export const manifestDevelopmentHandler = rest.get(umbracoPath('/package/manifes
|
||||
extensions: [
|
||||
{
|
||||
type: 'mfaLoginProvider',
|
||||
alias: 'My.MfaLoginProvider.Custom',
|
||||
name: 'My Custom MFA Provider',
|
||||
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',
|
||||
|
||||
@@ -8,6 +8,7 @@ import { handlers as changePasswordHandlers } from './change-password.handlers.j
|
||||
import { handlers as unlockHandlers } from './unlock.handlers.js';
|
||||
import { handlers as inviteHandlers } from './invite.handlers.js';
|
||||
import { handlers as filterHandlers } from './filter.handlers.js';
|
||||
import { handlers as mfaHandlers } from './mfa.handlers.js';
|
||||
|
||||
export const handlers = [
|
||||
...itemHandlers,
|
||||
@@ -20,4 +21,5 @@ export const handlers = [
|
||||
...filterHandlers,
|
||||
...inviteHandlers,
|
||||
...detailHandlers,
|
||||
...mfaHandlers,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
const { rest } = window.MockServiceWorker;
|
||||
import { umbUserMockDb } from '../../data/user/user.db.js';
|
||||
import { UMB_SLUG } from './slug.js';
|
||||
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
|
||||
|
||||
export const handlers = [
|
||||
rest.get(umbracoPath(`${UMB_SLUG}/:id/2fa`), (_req, res, ctx) => {
|
||||
const mfaLoginProviders = umbUserMockDb.getMfaLoginProviders();
|
||||
return res(ctx.status(200), ctx.json(mfaLoginProviders));
|
||||
}),
|
||||
rest.delete(umbracoPath(`${UMB_SLUG}/:id/2fa/:providerName`), async (req, res, ctx) => {
|
||||
const mfaLoginProviders = umbUserMockDb.getMfaLoginProviders();
|
||||
const provider = mfaLoginProviders.find((p) => p.providerName === req.params.providerName);
|
||||
if (!provider) {
|
||||
return res(ctx.status(404));
|
||||
}
|
||||
|
||||
provider.isEnabledOnUser = false;
|
||||
return res(ctx.status(200));
|
||||
}),
|
||||
];
|
||||
@@ -6,7 +6,7 @@ export const workspaceViews: Array<ManifestWorkspaceView> = [
|
||||
type: 'workspaceView',
|
||||
alias: 'Umb.WorkspaceView.BlockGridAreaType.Settings',
|
||||
name: 'Block Grid Area Type Workspace Settings View',
|
||||
js: () => import('./block-grid-area-type-workspace-view-settings.element.js'),
|
||||
js: () => import('./settings.element.js'),
|
||||
weight: 1000,
|
||||
meta: {
|
||||
label: 'Settings',
|
||||
|
||||
@@ -65,6 +65,9 @@ export class UmbDebugElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
private _openDialog() {
|
||||
|
||||
this._update();
|
||||
|
||||
this._modalContext?.open(this, UMB_CONTEXT_DEBUGGER_MODAL, {
|
||||
data: {
|
||||
content: html`${this._renderContextAliases()}`,
|
||||
@@ -73,9 +76,10 @@ export class UmbDebugElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
private _renderDialog() {
|
||||
return html` <div id="container">
|
||||
return html`
|
||||
<div id="container">
|
||||
<uui-badge color="danger" look="primary" attention @click="${this._openDialog}">
|
||||
<uui-icon name="icon-bug"></uui-icon> Debug
|
||||
<uui-icon name="icon-bug"></uui-icon> Debug
|
||||
</uui-badge>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ export default class UmbContextDebuggerModalElement extends UmbModalBaseElement<
|
||||
return html`
|
||||
<uui-dialog-layout>
|
||||
<span slot="headline"> <uui-icon name="icon-bug"></uui-icon> Debug: Contexts </span>
|
||||
<uui-scroll-container id="field-settings"> ${this.data?.content} </uui-scroll-container>
|
||||
<uui-scroll-container id="field-settings">
|
||||
${this.data?.content}
|
||||
</uui-scroll-container>
|
||||
<uui-button slot="actions" look="primary" label="Close sidebar" @click="${this._handleClose}">Close</uui-button>
|
||||
</uui-dialog-layout>
|
||||
`;
|
||||
|
||||
@@ -16,24 +16,24 @@ This can help with the developer experience to quickly see what is available to
|
||||
|
||||
### Usage
|
||||
|
||||
The `<umb-debug>` component can be used in two different ways, either as a button or as a dialog. By default it is rendered as a button and the debug information about available contexts is dissplayed inline to where the element is placed.
|
||||
The `<umb-debug>` component can be used in two different ways, either as a button or as a dialog. By default it is rendered as a button and the debug information about available contexts is displayed inline to where the `umb-debug` element is placed.
|
||||
|
||||
<img src={DebugImage} width="100%" />
|
||||
|
||||
```typescript
|
||||
// This will add a Debug button to the UI and once clicked the information about avilable contextes will slide down
|
||||
<umb-debug enabled></umb-debug>
|
||||
<umb-debug visible></umb-debug>
|
||||
```
|
||||
|
||||
#### Dialog
|
||||
|
||||
This example uses an additional property/attribute `dialog` which adds a smaller badge to the UI as opposed to a button and will open the information in a small dialog/modal from the right hand side, this may be more useful to use when space is limited in the UI to add a button and pane of information directly to where the element is placed.
|
||||
This example uses an additional property/attribute `dialog` which adds a smaller badge to the UI as opposed to a button and will open the information in a dialog/modal on the right hand side, this may be more useful to use when space is limited in the UI to add a button and pane of information directly to where the element is placed.
|
||||
|
||||
<img src={DebugDialogImage} width="100%" />
|
||||
|
||||
```typescript
|
||||
// This will open the debug information in a small dialog/modal from the right hand side
|
||||
<umb-debug enabled dialog></umb-debug>
|
||||
<umb-debug visible dialog></umb-debug>
|
||||
```
|
||||
|
||||
#### Disable
|
||||
@@ -41,6 +41,8 @@ This example uses an additional property/attribute `dialog` which adds a smaller
|
||||
You may wish to temporarily hide or disable the debug information but return to it later on in the development process.
|
||||
|
||||
```typescript
|
||||
// To hide or remove the button ensure you remove the enabled attribute or set the enabled property to false
|
||||
// To hide or remove the button ensure you remove the `visible` attribute or set it to false
|
||||
<umb-debug></umb-debug>
|
||||
|
||||
<umb-debug ?visible=${false}></umb-debug>
|
||||
```
|
||||
|
||||
@@ -4,7 +4,7 @@ export const manifest: ManifestPropertyEditorUi = {
|
||||
type: 'propertyEditorUi',
|
||||
alias: 'Umb.PropertyEditorUi.CollectionView.BulkActionPermissions',
|
||||
name: 'Collection View Bulk Action Permissions Property Editor UI',
|
||||
element: () => import('./property-editor-ui-collection-view-bulk-action-permissions.element.js'),
|
||||
element: () => import('./permissions.element.js'),
|
||||
meta: {
|
||||
label: 'Collection View Bulk Action Permissions',
|
||||
icon: 'icon-autofill',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { UmbCollectionBulkActionPermissions } from '../../../../../../core/collection/types.js';
|
||||
import type { UmbCollectionBulkActionPermissions } from '../../../../../collection/types.js';
|
||||
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { html, customElement, property, css } from '@umbraco-cms/backoffice/external/lit';
|
||||
import type { UUIBooleanInputEvent } from '@umbraco-cms/backoffice/external/uui';
|
||||
@@ -15,10 +15,10 @@ type BulkActionPermissionType =
|
||||
| 'allowBulkUnpublish';
|
||||
|
||||
/**
|
||||
* @element umb-property-editor-ui-collection-view-bulk-action-permissions
|
||||
* @element umb-property-editor-ui-collection-view-permissions
|
||||
*/
|
||||
@customElement('umb-property-editor-ui-collection-view-bulk-action-permissions')
|
||||
export class UmbPropertyEditorUICollectionViewBulkActionPermissionsElement
|
||||
@customElement('umb-property-editor-ui-collection-view-permissions')
|
||||
export class UmbPropertyEditorUICollectionViewPermissionsElement
|
||||
extends UmbLitElement
|
||||
implements UmbPropertyEditorUiElement
|
||||
{
|
||||
@@ -98,10 +98,10 @@ export class UmbPropertyEditorUICollectionViewBulkActionPermissionsElement
|
||||
];
|
||||
}
|
||||
|
||||
export default UmbPropertyEditorUICollectionViewBulkActionPermissionsElement;
|
||||
export default UmbPropertyEditorUICollectionViewPermissionsElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-property-editor-ui-collection-view-bulk-action-permissions': UmbPropertyEditorUICollectionViewBulkActionPermissionsElement;
|
||||
'umb-property-editor-ui-collection-view-permissions': UmbPropertyEditorUICollectionViewPermissionsElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Meta, Story } from '@storybook/web-components';
|
||||
import type { UmbPropertyEditorUICollectionViewPermissionsElement } from './permissions.element.js';
|
||||
import { html } from '@umbraco-cms/backoffice/external/lit';
|
||||
|
||||
import './permissions.element.js';
|
||||
|
||||
export default {
|
||||
title: 'Property Editor UIs/Collection View Bulk Action Permissions',
|
||||
component: 'umb-property-editor-ui-collection-view-permissions',
|
||||
id: 'umb-property-editor-ui-collection-view-permissions',
|
||||
} as Meta;
|
||||
|
||||
export const AAAOverview: Story<UmbPropertyEditorUICollectionViewPermissionsElement> = () =>
|
||||
html`<umb-property-editor-ui-collection-view-permissions></umb-property-editor-ui-collection-view-permissions>`;
|
||||
AAAOverview.storyName = 'Overview';
|
||||
@@ -1,18 +1,18 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
import { UmbPropertyEditorUICollectionViewBulkActionPermissionsElement } from './property-editor-ui-collection-view-bulk-action-permissions.element.js';
|
||||
import { UmbPropertyEditorUICollectionViewPermissionsElement } from './permissions.element.js';
|
||||
import { type UmbTestRunnerWindow, defaultA11yConfig } from '@umbraco-cms/internal/test-utils';
|
||||
|
||||
describe('UmbPropertyEditorUICollectionViewBulkActionPermissionsElement', () => {
|
||||
let element: UmbPropertyEditorUICollectionViewBulkActionPermissionsElement;
|
||||
describe('UmbPropertyEditorUICollectionViewPermissionsElement', () => {
|
||||
let element: UmbPropertyEditorUICollectionViewPermissionsElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
element = await fixture(html`
|
||||
<umb-property-editor-ui-collection-view-bulk-action-permissions></umb-property-editor-ui-collection-view-bulk-action-permissions>
|
||||
<umb-property-editor-ui-collection-view-permissions></umb-property-editor-ui-collection-view-permissions>
|
||||
`);
|
||||
});
|
||||
|
||||
it('is defined with its own instance', () => {
|
||||
expect(element).to.be.instanceOf(UmbPropertyEditorUICollectionViewBulkActionPermissionsElement);
|
||||
expect(element).to.be.instanceOf(UmbPropertyEditorUICollectionViewPermissionsElement);
|
||||
});
|
||||
|
||||
if ((window as UmbTestRunnerWindow).__UMBRACO_TEST_RUN_A11Y_TEST) {
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { Meta, Story } from '@storybook/web-components';
|
||||
import type { UmbPropertyEditorUICollectionViewBulkActionPermissionsElement } from './property-editor-ui-collection-view-bulk-action-permissions.element.js';
|
||||
import { html } from '@umbraco-cms/backoffice/external/lit';
|
||||
|
||||
import './property-editor-ui-collection-view-bulk-action-permissions.element.js';
|
||||
|
||||
export default {
|
||||
title: 'Property Editor UIs/Collection View Bulk Action Permissions',
|
||||
component: 'umb-property-editor-ui-collection-view-bulk-action-permissions',
|
||||
id: 'umb-property-editor-ui-collection-view-bulk-action-permissions',
|
||||
} as Meta;
|
||||
|
||||
export const AAAOverview: Story<UmbPropertyEditorUICollectionViewBulkActionPermissionsElement> = () =>
|
||||
html`<umb-property-editor-ui-collection-view-bulk-action-permissions></umb-property-editor-ui-collection-view-bulk-action-permissions>`;
|
||||
AAAOverview.storyName = 'Overview';
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Meta, Story } from '@storybook/web-components';
|
||||
import type { UmbPropertyEditorUICollectionViewColumnConfigurationElement } from './property-editor-ui-collection-view-column-configuration.element.js';
|
||||
import type { UmbPropertyEditorUICollectionViewColumnConfigurationElement } from './column-configuration.element.js';
|
||||
import { html } from '@umbraco-cms/backoffice/external/lit';
|
||||
|
||||
import './property-editor-ui-collection-view-column-configuration.element.js';
|
||||
import './column-configuration.element.js';
|
||||
|
||||
export default {
|
||||
title: 'Property Editor UIs/Collection View Column Configuration',
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
import { UmbPropertyEditorUICollectionViewColumnConfigurationElement } from './property-editor-ui-collection-view-column-configuration.element.js';
|
||||
import { UmbPropertyEditorUICollectionViewColumnConfigurationElement } from './column-configuration.element.js';
|
||||
import { type UmbTestRunnerWindow, defaultA11yConfig } from '@umbraco-cms/internal/test-utils';
|
||||
|
||||
describe('UmbPropertyEditorUICollectionViewColumnConfigurationElement', () => {
|
||||
@@ -4,7 +4,7 @@ export const manifest: ManifestPropertyEditorUi = {
|
||||
type: 'propertyEditorUi',
|
||||
alias: 'Umb.PropertyEditorUi.CollectionView.ColumnConfiguration',
|
||||
name: 'Collection View Column Configuration Property Editor UI',
|
||||
element: () => import('./property-editor-ui-collection-view-column-configuration.element.js'),
|
||||
element: () => import('./column-configuration.element.js'),
|
||||
meta: {
|
||||
label: 'Collection View Column Configuration',
|
||||
icon: 'icon-autofill',
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Meta, Story } from '@storybook/web-components';
|
||||
import type { UmbPropertyEditorUICollectionViewLayoutConfigurationElement } from './property-editor-ui-collection-view-layout-configuration.element.js';
|
||||
import type { UmbPropertyEditorUICollectionViewLayoutConfigurationElement } from './layout-configuration.element.js';
|
||||
import { html } from '@umbraco-cms/backoffice/external/lit';
|
||||
|
||||
import './property-editor-ui-collection-view-layout-configuration.element.js';
|
||||
import './layout-configuration.element.js';
|
||||
|
||||
export default {
|
||||
title: 'Property Editor UIs/Collection View Layout Configuration',
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
import { UmbPropertyEditorUICollectionViewLayoutConfigurationElement } from './property-editor-ui-collection-view-layout-configuration.element.js';
|
||||
import { UmbPropertyEditorUICollectionViewLayoutConfigurationElement } from './layout-configuration.element.js';
|
||||
import { type UmbTestRunnerWindow, defaultA11yConfig } from '@umbraco-cms/internal/test-utils';
|
||||
|
||||
describe('UmbPropertyEditorUICollectionViewLayoutConfigurationElement', () => {
|
||||
@@ -4,7 +4,7 @@ export const manifest: ManifestPropertyEditorUi = {
|
||||
type: 'propertyEditorUi',
|
||||
alias: 'Umb.PropertyEditorUi.CollectionView.LayoutConfiguration',
|
||||
name: 'Collection View Column Configuration Property Editor UI',
|
||||
element: () => import('./property-editor-ui-collection-view-layout-configuration.element.js'),
|
||||
element: () => import('./layout-configuration.element.js'),
|
||||
meta: {
|
||||
label: 'Collection View Layout Configuration',
|
||||
icon: 'icon-autofill',
|
||||
@@ -4,7 +4,7 @@ export const manifest: ManifestPropertyEditorUi = {
|
||||
type: 'propertyEditorUi',
|
||||
alias: 'Umb.PropertyEditorUi.CollectionView.OrderBy',
|
||||
name: 'Collection View Column Configuration Property Editor UI',
|
||||
element: () => import('./property-editor-ui-collection-view-order-by.element.js'),
|
||||
element: () => import('./order-by.element.js'),
|
||||
meta: {
|
||||
label: 'Collection View Order By',
|
||||
icon: 'icon-autofill',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Meta, Story } from '@storybook/web-components';
|
||||
import type { UmbPropertyEditorUICollectionViewOrderByElement } from './property-editor-ui-collection-view-order-by.element.js';
|
||||
import type { UmbPropertyEditorUICollectionViewOrderByElement } from './order-by.element.js';
|
||||
import { html } from '@umbraco-cms/backoffice/external/lit';
|
||||
|
||||
import './property-editor-ui-collection-view-order-by.element.js';
|
||||
import './order-by.element.js';
|
||||
|
||||
export default {
|
||||
title: 'Property Editor UIs/Collection View Order By',
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
import { UmbPropertyEditorUICollectionViewOrderByElement } from './property-editor-ui-collection-view-order-by.element.js';
|
||||
import { UmbPropertyEditorUICollectionViewOrderByElement } from './order-by.element.js';
|
||||
import { type UmbTestRunnerWindow, defaultA11yConfig } from '@umbraco-cms/internal/test-utils';
|
||||
|
||||
describe('UmbPropertyEditorUICollectionViewOrderByElement', () => {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { manifest as bulkActionPermissions } from './config/bulk-action-permissions/manifests.js';
|
||||
import { manifest as columnConfiguration } from './config/column-configuration/manifests.js';
|
||||
import { manifest as layoutConfiguration } from './config/layout-configuration/manifests.js';
|
||||
import { manifest as columnConfiguration } from './config/column/manifests.js';
|
||||
import { manifest as layoutConfiguration } from './config/layout/manifests.js';
|
||||
import { manifest as orderBy } from './config/order-by/manifests.js';
|
||||
import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from './resource.controller.js';
|
||||
export * from './tryExecute.function.js';
|
||||
export * from './tryExecuteAndNotify.function.js';
|
||||
export * from './extractUmbColorVariable.function.js';
|
||||
export * from './apiTypeValidators.function.js';
|
||||
|
||||
@@ -91,7 +91,7 @@ export class UmbInputSectionElement extends FormControlMixin(UmbLitElement) {
|
||||
() => !!this.max && this.#pickerContext.getSelection().length > this.max,
|
||||
);
|
||||
|
||||
this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems));
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export class UmbDataTypeInputElement extends FormControlMixin(UmbLitElement) {
|
||||
() => !!this.max && this.#pickerContext.getSelection().length > this.max,
|
||||
);
|
||||
|
||||
this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems));
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ export class UmbInputDocumentTypeElement extends FormControlMixin(UmbLitElement)
|
||||
() => !!this.max && this.#pickerContext.getSelection().length > this.max,
|
||||
);
|
||||
|
||||
this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems));
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ export class UmbInputDocumentElement extends FormControlMixin(UmbLitElement) {
|
||||
this._editDocumentPath = routeBuilder({});
|
||||
});
|
||||
|
||||
this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems));
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ export class UmbInputLanguageElement extends FormControlMixin(UmbLitElement) {
|
||||
() => !!this.max && this.#pickerContext.getSelection().length > this.max,
|
||||
);
|
||||
|
||||
this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems));
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export class UmbInputMediaTypeElement extends FormControlMixin(UmbLitElement) {
|
||||
() => !!this.max && this.#pickerContext.getSelection().length > this.max,
|
||||
);
|
||||
|
||||
this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems));
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ export class UmbInputMediaElement extends FormControlMixin(UmbLitElement) {
|
||||
this._editMediaPath = routeBuilder({});
|
||||
});
|
||||
|
||||
this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems));
|
||||
|
||||
this.addValidator(
|
||||
|
||||
@@ -122,7 +122,7 @@ export class UmbInputMemberGroupElement extends FormControlMixin(UmbLitElement)
|
||||
this._editMemberGroupPath = routeBuilder({});
|
||||
});
|
||||
|
||||
this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selectedItems, (selectedItems) => {
|
||||
this._items = selectedItems;
|
||||
});
|
||||
|
||||
@@ -89,7 +89,7 @@ export class UmbInputMemberTypeElement extends FormControlMixin(UmbLitElement) {
|
||||
() => !!this.max && this.#pickerContext.getSelection().length > this.max,
|
||||
);
|
||||
|
||||
this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems));
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ export class UmbInputMemberElement extends FormControlMixin(UmbLitElement) {
|
||||
this._editMemberPath = routeBuilder({});
|
||||
});
|
||||
|
||||
this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selectedItems, (selectedItems) => {
|
||||
this._items = selectedItems;
|
||||
});
|
||||
|
||||
@@ -90,7 +90,7 @@ export class UmbInputStaticFileElement extends FormControlMixin(UmbLitElement) {
|
||||
() => !!this.max && this.#pickerContext.getSelection().length > this.max,
|
||||
);
|
||||
|
||||
this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems));
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export class UmbStylesheetInputElement extends FormControlMixin(UmbLitElement) {
|
||||
() => !!this.max && this.#pickerContext.getSelection().length > this.max,
|
||||
);
|
||||
|
||||
this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')));
|
||||
this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { UmbMfaProviderConfigurationElementProps } from '../types.js';
|
||||
import type { UmbMfaProviderConfigurationCallback, UmbMfaProviderConfigurationElementProps } from '../types.js';
|
||||
import { UserResource } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
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 { isApiError, tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
|
||||
import { UMB_NOTIFICATION_CONTEXT, type UmbNotificationColor } from '@umbraco-cms/backoffice/notification';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import type { UUIButtonState } from '@umbraco-cms/backoffice/external/uui';
|
||||
@@ -20,7 +20,8 @@ export class UmbMfaProviderDefaultElement extends UmbLitElement implements UmbMf
|
||||
displayName = '';
|
||||
|
||||
@property({ attribute: false })
|
||||
callback: (providerName: string, code: string, secret: string) => Promise<boolean> = async () => false;
|
||||
callback: (providerName: string, code: string, secret: string) => UmbMfaProviderConfigurationCallback =
|
||||
async () => ({});
|
||||
|
||||
@property({ attribute: false })
|
||||
close = () => {};
|
||||
@@ -174,16 +175,27 @@ export class UmbMfaProviderDefaultElement extends UmbLitElement implements UmbMf
|
||||
if (!code) return;
|
||||
|
||||
this._buttonState = 'waiting';
|
||||
const successful = await this.callback(this.providerName, code, this._secret);
|
||||
const { error } = await this.callback(this.providerName, code, this._secret);
|
||||
|
||||
if (successful) {
|
||||
this.peek(this.localize.term('user_2faProviderIsEnabled'));
|
||||
if (!error) {
|
||||
this.peek(this.localize.term('user_2faProviderIsEnabledMsg', this.displayName ?? this.providerName));
|
||||
this._buttonState = 'success';
|
||||
this.close();
|
||||
} else {
|
||||
this.codeField?.setCustomValidity(this.localize.term('user_2faInvalidCode'));
|
||||
this.codeField?.focus();
|
||||
this._buttonState = 'failed';
|
||||
if (isApiError(error)) {
|
||||
if (error.body.operationStatus === 'InvalidCode') {
|
||||
this.codeField?.setCustomValidity(this.localize.term('user_2faInvalidCode'));
|
||||
this.codeField?.focus();
|
||||
} else {
|
||||
this.peek(
|
||||
this.localize.term('user_2faProviderIsNotEnabledMsg', this.displayName ?? this.providerName),
|
||||
'warning',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.peek(error.message, 'warning');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,15 @@ export class UmbCurrentUserContext extends UmbContextBase<UmbCurrentUserContext>
|
||||
return currentUser?.unique === userUnique;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current user is an admin.
|
||||
* @returns True if the current user is an admin, otherwise false
|
||||
*/
|
||||
async isCurrentUserAdmin(): Promise<boolean> {
|
||||
const currentUser = await firstValueFrom(this.currentUser);
|
||||
return currentUser?.isAdmin ?? false;
|
||||
}
|
||||
|
||||
#observeIsAuthorized() {
|
||||
if (!this.#authContext) return;
|
||||
this.observe(this.#authContext.isAuthorized, (isAuthorized) => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { css, customElement, html, query, state } from '@umbraco-cms/backoffice/
|
||||
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
|
||||
import type { UUIButtonState } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { isApiError } from '@umbraco-cms/backoffice/resources';
|
||||
|
||||
@customElement('umb-current-user-mfa-disable-provider-modal')
|
||||
export class UmbCurrentUserMfaDisableProviderModalElement extends UmbModalBaseElement<
|
||||
@@ -99,16 +100,27 @@ export class UmbCurrentUserMfaDisableProviderModalElement extends UmbModalBaseEl
|
||||
if (!code) return;
|
||||
|
||||
this._buttonState = 'waiting';
|
||||
const success = await this.#currentUserRepository.disableMfaProvider(this.data.providerName, code);
|
||||
const { error } = await this.#currentUserRepository.disableMfaProvider(this.data.providerName, code);
|
||||
|
||||
if (success) {
|
||||
this.#peek(this.localize.term('user_2faProviderIsDisabledMsg'));
|
||||
if (!error) {
|
||||
this.#peek(this.localize.term('user_2faProviderIsDisabledMsg', this.data.displayName ?? this.data.providerName));
|
||||
this.modalContext?.submit();
|
||||
this._buttonState = 'success';
|
||||
} else {
|
||||
this._codeInput.setCustomValidity(this.localize.term('user_2faInvalidCode'));
|
||||
this._codeInput.focus();
|
||||
this._buttonState = 'failed';
|
||||
if (isApiError(error)) {
|
||||
if (error.body.operationStatus === 'InvalidCode') {
|
||||
this._codeInput.setCustomValidity(this.localize.term('user_2faInvalidCode'));
|
||||
this._codeInput.focus();
|
||||
} else {
|
||||
this.#peek(
|
||||
this.localize.term('user_2faProviderIsNotDisabledMsg', this.data.displayName ?? this.data.providerName),
|
||||
'warning',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.#peek(error.message, 'warning');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import type { UmbCurrentUserMfaProviderModel } from '../types.js';
|
||||
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
|
||||
@@ -13,15 +10,13 @@ import { UserResource } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
* @extends {UmbRepositoryBase}
|
||||
*/
|
||||
export class UmbCurrentUserRepository extends UmbRepositoryBase {
|
||||
#currentUserSource: UmbCurrentUserServerDataSource;
|
||||
#currentUserSource = new UmbCurrentUserServerDataSource(this._host);
|
||||
#currentUserStore?: typeof UMB_CURRENT_USER_STORE_CONTEXT.TYPE;
|
||||
#init: Promise<unknown>;
|
||||
|
||||
constructor(host: UmbControllerHost) {
|
||||
super(host);
|
||||
|
||||
this.#currentUserSource = new UmbCurrentUserServerDataSource(host);
|
||||
|
||||
this.#init = Promise.all([
|
||||
this.consumeContext(UMB_CURRENT_USER_STORE_CONTEXT, (instance) => {
|
||||
this.#currentUserStore = instance;
|
||||
@@ -67,19 +62,16 @@ export class UmbCurrentUserRepository extends UmbRepositoryBase {
|
||||
* @param code The activation code of the provider to enable
|
||||
* @memberof UmbCurrentUserRepository
|
||||
*/
|
||||
async enableMfaProvider(providerName: string, code: string, secret: string): Promise<boolean> {
|
||||
const { error } = await tryExecuteAndNotify(
|
||||
this._host,
|
||||
UserResource.postUserCurrent2FaByProviderName({ providerName, requestBody: { code, secret } }),
|
||||
);
|
||||
async enableMfaProvider(providerName: string, code: string, secret: string) {
|
||||
const { error } = await this.#currentUserSource.enableMfaProvider(providerName, code, secret);
|
||||
|
||||
if (error) {
|
||||
return false;
|
||||
return { error };
|
||||
}
|
||||
|
||||
this.#currentUserStore?.updateMfaProvider({ providerName, isEnabledOnUser: true });
|
||||
|
||||
return true;
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,19 +80,16 @@ export class UmbCurrentUserRepository extends UmbRepositoryBase {
|
||||
* @param code The activation code of the provider to disable
|
||||
* @memberof UmbCurrentUserRepository
|
||||
*/
|
||||
async disableMfaProvider(providerName: string, code: string): Promise<boolean> {
|
||||
const { error } = await tryExecuteAndNotify(
|
||||
this._host,
|
||||
UserResource.deleteUserCurrent2FaByProviderName({ providerName, code }),
|
||||
);
|
||||
async disableMfaProvider(providerName: string, code: string) {
|
||||
const { error } = await this.#currentUserSource.disableMfaProvider(providerName, code);
|
||||
|
||||
if (error) {
|
||||
return false;
|
||||
return { error };
|
||||
}
|
||||
|
||||
this.#currentUserStore?.updateMfaProvider({ providerName, isEnabledOnUser: false });
|
||||
|
||||
return true;
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { UmbCurrentUserModel } from '../types.js';
|
||||
import { SecurityResource, UserResource } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import { UserResource } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
|
||||
import { tryExecute, tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
|
||||
|
||||
/**
|
||||
* A data source for the current user that fetches data from the server
|
||||
@@ -43,6 +43,7 @@ export class UmbCurrentUserServerDataSource {
|
||||
fallbackPermissions: data.fallbackPermissions,
|
||||
permissions: data.permissions,
|
||||
allowedSections: data.allowedSections,
|
||||
isAdmin: data.isAdmin,
|
||||
};
|
||||
return { data: user };
|
||||
}
|
||||
@@ -63,4 +64,32 @@ export class UmbCurrentUserServerDataSource {
|
||||
|
||||
return { error };
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable an MFA provider
|
||||
*/
|
||||
async enableMfaProvider(providerName: string, code: string, secret: string) {
|
||||
const { error } = await tryExecute(
|
||||
UserResource.postUserCurrent2FaByProviderName({ providerName, requestBody: { code, secret } }),
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { error };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable an MFA provider
|
||||
*/
|
||||
async disableMfaProvider(providerName: string, code: string) {
|
||||
const { error } = await tryExecute(UserResource.deleteUserCurrent2FaByProviderName({ providerName, code }));
|
||||
|
||||
if (error) {
|
||||
return { error };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ export class UmbCurrentUserStore extends UmbContextBase<UmbCurrentUserStore> {
|
||||
documentStartNodeUniques: updatedCurrentUser.documentStartNodeUniques,
|
||||
mediaStartNodeUniques: updatedCurrentUser.mediaStartNodeUniques,
|
||||
avatarUrls: updatedCurrentUser.avatarUrls,
|
||||
isAdmin: updatedCurrentUser.isAdmin,
|
||||
};
|
||||
|
||||
this.update(mappedCurrentUser);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type {
|
||||
ApiError,
|
||||
CancelError,
|
||||
DocumentPermissionPresentationModel,
|
||||
UnknownTypePermissionPresentationModel,
|
||||
UserTwoFactorProviderModel,
|
||||
@@ -18,10 +20,13 @@ export interface UmbCurrentUserModel {
|
||||
allowedSections: Array<string>;
|
||||
fallbackPermissions: Array<string>;
|
||||
permissions: Array<DocumentPermissionPresentationModel | UnknownTypePermissionPresentationModel>;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
export type UmbCurrentUserMfaProviderModel = UserTwoFactorProviderModel;
|
||||
|
||||
export type UmbMfaProviderConfigurationCallback = Promise<{ error?: ApiError | CancelError }>;
|
||||
|
||||
export interface UmbMfaProviderConfigurationElementProps {
|
||||
/**
|
||||
* The name of the provider reflecting the provider name in the backend.
|
||||
@@ -38,9 +43,9 @@ export interface UmbMfaProviderConfigurationElementProps {
|
||||
* @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 action was executed successfully.
|
||||
* @returns A promise that resolves when the action is completed with an error if the action failed.
|
||||
*/
|
||||
callback: (providerName: string, code: string, secret: string) => Promise<boolean>;
|
||||
callback: (providerName: string, code: string, secret: string) => UmbMfaProviderConfigurationCallback;
|
||||
|
||||
/**
|
||||
* Call this function to close the modal.
|
||||
|
||||
@@ -2,18 +2,24 @@ import { UMB_CURRENT_USER_CONTEXT } from '../current-user.context.js';
|
||||
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
/**
|
||||
* Check if the current user is the user with the given unique id
|
||||
*/
|
||||
export const isCurrentUser = async (host: UmbControllerHost, userUnique: string) => {
|
||||
const ctrl = new UmbContextConsumerController(host, UMB_CURRENT_USER_CONTEXT);
|
||||
let currentUserContext = await ctrl.asPromise();
|
||||
const currentUserContext = await ctrl.asPromise();
|
||||
ctrl.destroy();
|
||||
|
||||
const controller = new UmbContextConsumerController(host, UMB_CURRENT_USER_CONTEXT, (context) => {
|
||||
currentUserContext = context;
|
||||
});
|
||||
|
||||
await controller.asPromise();
|
||||
|
||||
controller.destroy();
|
||||
|
||||
return await currentUserContext!.isUserCurrentUser(userUnique);
|
||||
return currentUserContext!.isUserCurrentUser(userUnique);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the current user is an admin
|
||||
*/
|
||||
export const isCurrentUserAnAdmin = async (host: UmbControllerHost) => {
|
||||
const ctrl = new UmbContextConsumerController(host, UMB_CURRENT_USER_CONTEXT);
|
||||
const currentUserContext = await ctrl.asPromise();
|
||||
ctrl.destroy();
|
||||
|
||||
return currentUserContext!.isCurrentUserAdmin();
|
||||
};
|
||||
|
||||
@@ -91,7 +91,7 @@ export class UmbUserGroupInputElement extends FormControlMixin(UmbLitElement) {
|
||||
|
||||
this.observe(
|
||||
this.#pickerContext.selection,
|
||||
(selection) => (super.value = selection.join(',')),
|
||||
(selection) => (this.value = selection.join(',')),
|
||||
'umbUserGroupInputSelectionObserver',
|
||||
);
|
||||
this.observe(
|
||||
|
||||
@@ -56,6 +56,7 @@ export class UmbUserCollectionServerDataSource implements UmbCollectionDataSourc
|
||||
lastLoginDate: item.lastLoginDate || null,
|
||||
lastLockoutDate: item.lastLockoutDate || null,
|
||||
lastPasswordChangeDate: item.lastPasswordChangeDate || null,
|
||||
isAdmin: item.isAdmin,
|
||||
};
|
||||
|
||||
return userDetail;
|
||||
|
||||
@@ -93,7 +93,7 @@ export class UmbUserInputElement extends FormControlMixin(UmbLitElement) {
|
||||
|
||||
this.observe(
|
||||
this.#pickerContext.selection,
|
||||
(selection) => (super.value = selection.join(',')),
|
||||
(selection) => (this.value = selection.join(',')),
|
||||
'umbUserInputSelectionObserver',
|
||||
);
|
||||
this.observe(
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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 userAllowMfaActionManifest } from './user-allow-mfa-action.condition.js';
|
||||
import { manifest as userAllowDeleteActionManifest } from './user-allow-delete-action.condition.js';
|
||||
|
||||
export const manifests = [
|
||||
userAllowDisableActionManifest,
|
||||
userAllowEnableActionManifest,
|
||||
userAllowUnlockActionManifest,
|
||||
userAllowMfaActionManifest,
|
||||
userAllowDeleteActionManifest,
|
||||
];
|
||||
|
||||
@@ -8,12 +8,20 @@ import type {
|
||||
UmbExtensionCondition,
|
||||
} from '@umbraco-cms/backoffice/extension-api';
|
||||
import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { observeMultiple } from '@umbraco-cms/backoffice/observable-api';
|
||||
|
||||
export abstract class UmbUserActionConditionBase
|
||||
extends UmbConditionBase<UmbConditionConfigBase>
|
||||
implements UmbExtensionCondition
|
||||
{
|
||||
/**
|
||||
* The unique identifier of the user being edited
|
||||
*/
|
||||
protected userUnique?: string;
|
||||
|
||||
/**
|
||||
* The state of the user being edited
|
||||
*/
|
||||
protected userState?: UmbUserStateEnum | null;
|
||||
|
||||
constructor(host: UmbControllerHost, args: UmbConditionControllerArguments<UmbConditionConfigBase>) {
|
||||
@@ -21,23 +29,13 @@ export abstract class UmbUserActionConditionBase
|
||||
|
||||
this.consumeContext(UMB_USER_WORKSPACE_CONTEXT, (context) => {
|
||||
this.observe(
|
||||
context.unique,
|
||||
(unique) => {
|
||||
observeMultiple([context.unique, context.state]),
|
||||
([unique, state]) => {
|
||||
this.userUnique = unique;
|
||||
this._onUserDataChange();
|
||||
},
|
||||
'umbUserUnique',
|
||||
);
|
||||
this.observe(
|
||||
context.state,
|
||||
(state) => {
|
||||
this.userState = state;
|
||||
// TODO: Investigate if we can remove this observation and just use the unique change to trigger the state change. [NL]
|
||||
// Can user state change over time? if not then this observation is not needed and then we just need to retrieve the state when the unique has changed. [NL]
|
||||
// These two could also be combined via the observeMultiple method, that could prevent triggering onUserDataChanged twice. [NL]
|
||||
this._onUserDataChange();
|
||||
},
|
||||
'umbUserState',
|
||||
'_umbActiveUser',
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -51,6 +49,7 @@ export abstract class UmbUserActionConditionBase
|
||||
protected async isCurrentUser() {
|
||||
return this.userUnique ? isCurrentUser(this._host, this.userUnique) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user data changes
|
||||
* @protected
|
||||
|
||||
@@ -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 UmbUserAllowMfaActionCondition extends UmbConditionBase<never> {
|
||||
// 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('mfaLoginProvider'),
|
||||
(exts) => (this.permitted = exts.length > 0),
|
||||
'_userAllowMfaActionConditionProviders',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const manifest: ManifestCondition = {
|
||||
type: 'condition',
|
||||
name: 'User Allow Mfa Action Condition',
|
||||
alias: 'Umb.Condition.User.AllowMfaAction',
|
||||
api: UmbUserAllowMfaActionCondition,
|
||||
};
|
||||
@@ -16,18 +16,20 @@ export class UmbDisableUserEntityAction extends UmbEntityActionBase<never> {
|
||||
const itemRepository = new UmbUserItemRepository(this);
|
||||
const { data } = await itemRepository.requestItems([this.args.unique]);
|
||||
|
||||
if (data) {
|
||||
const item = data[0];
|
||||
|
||||
await umbConfirmModal(this._host, {
|
||||
headline: `Disable ${item.name}`,
|
||||
content: 'Are you sure you want to disable this user?',
|
||||
color: 'danger',
|
||||
confirmLabel: 'Disable',
|
||||
});
|
||||
|
||||
const disableUserRepository = new UmbDisableUserRepository(this);
|
||||
await disableUserRepository.disable([this.args.unique]);
|
||||
if (!data?.length) {
|
||||
throw new Error('Item not found.');
|
||||
}
|
||||
|
||||
const item = data[0];
|
||||
|
||||
await umbConfirmModal(this._host, {
|
||||
headline: `Disable ${item.name}`,
|
||||
content: 'Are you sure you want to disable this user?',
|
||||
color: 'danger',
|
||||
confirmLabel: 'Disable',
|
||||
});
|
||||
|
||||
const disableUserRepository = new UmbDisableUserRepository(this);
|
||||
await disableUserRepository.disable([this.args.unique]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,17 +16,19 @@ export class UmbEnableUserEntityAction extends UmbEntityActionBase<never> {
|
||||
const itemRepository = new UmbUserItemRepository(this);
|
||||
const { data } = await itemRepository.requestItems([this.args.unique]);
|
||||
|
||||
if (data) {
|
||||
const item = data[0];
|
||||
|
||||
await umbConfirmModal(this._host, {
|
||||
headline: `Enable ${item.name}`,
|
||||
content: 'Are you sure you want to enable this user?',
|
||||
confirmLabel: 'Enable',
|
||||
});
|
||||
|
||||
const enableRepository = new UmbEnableUserRepository(this);
|
||||
await enableRepository.enable([this.args.unique]);
|
||||
if (!data?.length) {
|
||||
throw new Error('Item not found.');
|
||||
}
|
||||
|
||||
const item = data[0];
|
||||
|
||||
await umbConfirmModal(this._host, {
|
||||
headline: `Enable ${item.name}`,
|
||||
content: 'Are you sure you want to enable this user?',
|
||||
confirmLabel: 'Enable',
|
||||
});
|
||||
|
||||
const enableRepository = new UmbEnableUserRepository(this);
|
||||
await enableRepository.enable([this.args.unique]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { UmbDisableUserEntityAction } from './disable/disable-user.action.js';
|
||||
import { UmbEnableUserEntityAction } from './enable/enable-user.action.js';
|
||||
import { UmbChangeUserPasswordEntityAction } from './change-password/change-user-password.action.js';
|
||||
import { UmbUnlockUserEntityAction } from './unlock/unlock-user.action.js';
|
||||
import { UmbMfaUserEntityAction } from './mfa/mfa-user.action.js';
|
||||
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
const entityActions: Array<ManifestTypes> = [
|
||||
@@ -90,6 +91,24 @@ const entityActions: Array<ManifestTypes> = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'entityAction',
|
||||
kind: 'default',
|
||||
alias: 'Umb.EntityAction.User.ConfigureMfa',
|
||||
name: 'Configure MFA Entity Action',
|
||||
weight: 500,
|
||||
api: UmbMfaUserEntityAction,
|
||||
forEntityTypes: [UMB_USER_ENTITY_TYPE],
|
||||
meta: {
|
||||
icon: 'icon-settings',
|
||||
label: 'Configure Two-Factor',
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
alias: 'Umb.Condition.User.AllowMfaAction',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const manifests = [...entityActions];
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { UMB_USER_MFA_MODAL } from '../../modals/user-mfa/user-mfa-modal.token.js';
|
||||
import { UMB_CURRENT_USER_MFA_MODAL } from '../../../current-user/modals/current-user-mfa/current-user-mfa-modal.token.js';
|
||||
import type { UmbEntityActionArgs } from '@umbraco-cms/backoffice/entity-action';
|
||||
import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user';
|
||||
import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs';
|
||||
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
|
||||
|
||||
export class UmbMfaUserEntityAction extends UmbEntityActionBase<never> {
|
||||
constructor(host: UmbControllerHost, args: UmbEntityActionArgs<never>) {
|
||||
super(host, args);
|
||||
}
|
||||
|
||||
async execute() {
|
||||
const { unique } = this.args;
|
||||
if (!unique) throw new Error('Unique is not available');
|
||||
|
||||
const currentUserContext = await this.getContext(UMB_CURRENT_USER_CONTEXT);
|
||||
const currentUserModel = await firstValueFrom(currentUserContext.currentUser);
|
||||
|
||||
if (!currentUserModel) throw new Error('Current user is not available');
|
||||
|
||||
// If you clicked on yourself, we can just use the current user modal
|
||||
const modalManagerContext = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
|
||||
if (currentUserModel.unique === unique) {
|
||||
await modalManagerContext
|
||||
.open(this, UMB_CURRENT_USER_MFA_MODAL)
|
||||
.onSubmit()
|
||||
.catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise we will show the generic mfa modal
|
||||
await modalManagerContext
|
||||
.open(this, UMB_USER_MFA_MODAL, {
|
||||
data: { unique },
|
||||
})
|
||||
.onSubmit()
|
||||
.catch(() => undefined);
|
||||
}
|
||||
}
|
||||
@@ -16,17 +16,19 @@ export class UmbUnlockUserEntityAction extends UmbEntityActionBase<never> {
|
||||
const itemRepository = new UmbUserItemRepository(this);
|
||||
const { data } = await itemRepository.requestItems([this.args.unique]);
|
||||
|
||||
if (data) {
|
||||
const item = data[0];
|
||||
|
||||
await umbConfirmModal(this._host, {
|
||||
headline: `Unlock ${item.name}`,
|
||||
content: 'Are you sure you want to unlock this user?',
|
||||
confirmLabel: 'Unlock',
|
||||
});
|
||||
|
||||
const unlockUserRepository = new UmbUnlockUserRepository(this);
|
||||
await unlockUserRepository?.unlock([this.args.unique]);
|
||||
if (!data?.length) {
|
||||
throw new Error('Item not found.');
|
||||
}
|
||||
|
||||
const item = data[0];
|
||||
|
||||
await umbConfirmModal(this._host, {
|
||||
headline: `Unlock ${item.name}`,
|
||||
content: 'Are you sure you want to unlock this user?',
|
||||
confirmLabel: 'Unlock',
|
||||
});
|
||||
|
||||
const unlockUserRepository = new UmbUnlockUserRepository(this);
|
||||
await unlockUserRepository?.unlock([this.args.unique]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,5 @@ export * from './collection/index.js';
|
||||
export * from './components/index.js';
|
||||
export * from './invite/index.js';
|
||||
export * from './repository/index.js';
|
||||
export * from './types.js';
|
||||
export type * from './types.js';
|
||||
export * from './utils/index.js';
|
||||
|
||||
@@ -19,6 +19,12 @@ const modals: Array<ManifestModal> = [
|
||||
name: 'User Picker Modal',
|
||||
js: () => import('./user-picker/user-picker-modal.element.js'),
|
||||
},
|
||||
{
|
||||
type: 'modal',
|
||||
alias: 'Umb.Modal.User.Mfa',
|
||||
name: 'User Mfa Modal',
|
||||
js: () => import('./user-mfa/user-mfa-modal.element.js'),
|
||||
},
|
||||
];
|
||||
|
||||
export const manifests = [...modals];
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
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 { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
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';
|
||||
|
||||
type UmbMfaLoginProviderOption = UmbUserMfaProviderModel & {
|
||||
displayName: string;
|
||||
};
|
||||
|
||||
@customElement('umb-user-mfa-modal')
|
||||
export class UmbUserMfaModalElement extends UmbLitElement {
|
||||
@property({ attribute: false })
|
||||
modalContext?: UmbModalContext<UmbUserMfaModalConfiguration, never>;
|
||||
|
||||
@state()
|
||||
_items: Array<UmbMfaLoginProviderOption> = [];
|
||||
|
||||
#unique = '';
|
||||
#userRepository = new UmbUserRepository(this);
|
||||
|
||||
firstUpdated() {
|
||||
this.#unique = this.modalContext?.data.unique ?? '';
|
||||
this.#loadProviders();
|
||||
}
|
||||
|
||||
async #loadProviders() {
|
||||
const serverLoginProviders$ = (await this.#userRepository.requestMfaProviders(this.#unique)).asObservable();
|
||||
const manifestLoginProviders$ = umbExtensionsRegistry.byType('mfaLoginProvider');
|
||||
|
||||
// Merge the server and manifest providers to get the final list of providers
|
||||
const mfaLoginProviders$ = mergeObservables(
|
||||
[serverLoginProviders$, manifestLoginProviders$],
|
||||
([serverLoginProviders, manifestLoginProviders]) => {
|
||||
return manifestLoginProviders.map((manifestLoginProvider) => {
|
||||
const serverLoginProvider = serverLoginProviders.find(
|
||||
(serverLoginProvider) => serverLoginProvider.providerName === manifestLoginProvider.forProviderName,
|
||||
);
|
||||
return {
|
||||
isEnabledOnUser: serverLoginProvider?.isEnabledOnUser ?? false,
|
||||
providerName: serverLoginProvider?.providerName ?? manifestLoginProvider.forProviderName,
|
||||
displayName:
|
||||
manifestLoginProvider.meta?.label ?? serverLoginProvider?.providerName ?? manifestLoginProvider.name,
|
||||
} satisfies UmbMfaLoginProviderOption;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
this.observe(
|
||||
mfaLoginProviders$,
|
||||
(providers) => {
|
||||
this._items = providers;
|
||||
},
|
||||
'_mfaLoginProviders',
|
||||
);
|
||||
}
|
||||
|
||||
#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: UmbMfaLoginProviderOption) {
|
||||
return html`
|
||||
<uui-box headline=${item.displayName}>
|
||||
${when(
|
||||
item.isEnabledOnUser,
|
||||
() => html`
|
||||
<p style="margin-top:0">
|
||||
<umb-localize key="user_2faProviderIsEnabled">This two-factor provider is enabled</umb-localize>
|
||||
<uui-icon icon="check"></uui-icon>
|
||||
</p>
|
||||
<uui-button
|
||||
type="button"
|
||||
look="secondary"
|
||||
color="danger"
|
||||
.label=${this.localize.term('actions_disable')}
|
||||
@click=${() => this.#onProviderDisable(item)}></uui-button>
|
||||
`,
|
||||
() => html`
|
||||
<uui-button
|
||||
disabled
|
||||
type="button"
|
||||
look="secondary"
|
||||
.label=${this.localize.term('actions_enable')}></uui-button>
|
||||
`,
|
||||
)}
|
||||
</uui-box>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called when the user clicks the disable button on a provider.
|
||||
* It will show a confirmation dialog and then disable the provider if the user confirms.
|
||||
* NB! The user must have administrative rights before doing so.
|
||||
*/
|
||||
async #onProviderDisable(item: UmbMfaLoginProviderOption) {
|
||||
await umbConfirmModal(this, {
|
||||
headline: this.localize.term('actions_disable'),
|
||||
content: this.localize.term('user_2faDisableForUser', item.displayName),
|
||||
confirmLabel: this.localize.term('actions_disable'),
|
||||
color: 'danger',
|
||||
});
|
||||
|
||||
await this.#userRepository.disableMfaProvider(this.#unique, item.providerName, item.displayName);
|
||||
this.#loadProviders();
|
||||
}
|
||||
|
||||
static styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
uui-box {
|
||||
margin-bottom: var(--uui-size-space-3);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
export default UmbUserMfaModalElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-user-mfa-modal': UmbUserMfaModalElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { Meta, StoryObj } from '@storybook/web-components';
|
||||
import type { UmbUserMfaModalElement } from './user-mfa-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 './user-mfa-modal.element.js';
|
||||
|
||||
class UmbServerExtensionsHostElement extends UmbLitElement {
|
||||
constructor() {
|
||||
super();
|
||||
new UmbServerExtensionRegistrator(this, umbExtensionsRegistry).registerAllExtensions();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<slot></slot>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (window.customElements.get('umb-server-extensions-host') === undefined) {
|
||||
customElements.define('umb-server-extensions-host', UmbServerExtensionsHostElement);
|
||||
}
|
||||
|
||||
const meta: Meta<UmbUserMfaModalElement> = {
|
||||
title: 'User/MFA/Configure MFA Providers',
|
||||
component: 'umb-user-mfa-modal',
|
||||
decorators: [
|
||||
(Story) =>
|
||||
html`<umb-server-extensions-host style="display: block; width: 500px; height: 500px;">
|
||||
${Story()}
|
||||
</umb-server-extensions-host>`,
|
||||
],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
actions: {
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<UmbUserMfaModalElement>;
|
||||
|
||||
export const Overview: Story = {};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
|
||||
|
||||
export type UmbUserMfaModalConfiguration = {
|
||||
unique: string;
|
||||
};
|
||||
|
||||
export const UMB_USER_MFA_MODAL = new UmbModalToken<UmbUserMfaModalConfiguration, never>('Umb.Modal.User.Mfa', {
|
||||
modal: {
|
||||
type: 'sidebar',
|
||||
size: 'small',
|
||||
},
|
||||
});
|
||||
@@ -1,11 +1,9 @@
|
||||
import { UmbUserRepositoryBase } from '../user-repository-base.js';
|
||||
import { UmbChangeUserPasswordServerDataSource } from './change-user-password.server.data-source.js';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import type { UmbNotificationContext } from '@umbraco-cms/backoffice/notification';
|
||||
|
||||
export class UmbChangeUserPasswordRepository extends UmbUserRepositoryBase {
|
||||
#changePasswordSource: UmbChangeUserPasswordServerDataSource;
|
||||
#notificationContext?: UmbNotificationContext;
|
||||
|
||||
constructor(host: UmbControllerHost) {
|
||||
super(host);
|
||||
@@ -21,7 +19,7 @@ export class UmbChangeUserPasswordRepository extends UmbUserRepositoryBase {
|
||||
|
||||
if (!error) {
|
||||
const notification = { data: { message: `Password changed` } };
|
||||
this.#notificationContext?.peek('positive', notification);
|
||||
this.notificationContext?.peek('positive', notification);
|
||||
}
|
||||
|
||||
return { data, error };
|
||||
|
||||
@@ -50,6 +50,7 @@ export class UmbUserServerDataSource implements UmbDetailDataSource<UmbUserDetai
|
||||
updateDate: null,
|
||||
userGroupUniques: [],
|
||||
userName: '',
|
||||
isAdmin: false,
|
||||
};
|
||||
|
||||
return { data };
|
||||
@@ -89,6 +90,7 @@ export class UmbUserServerDataSource implements UmbDetailDataSource<UmbUserDetai
|
||||
updateDate: data.updateDate,
|
||||
userGroupUniques: data.userGroupIds,
|
||||
userName: data.userName,
|
||||
isAdmin: data.isAdmin,
|
||||
};
|
||||
|
||||
return { data: dataType };
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { UserResource } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { tryExecute, tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
|
||||
|
||||
/**
|
||||
* A data source for User MFA items that fetches data from the server
|
||||
* @export
|
||||
* @class UmbMfaServerDataSource
|
||||
*/
|
||||
export class UmbUserMfaServerDataSource {
|
||||
#host: UmbControllerHost;
|
||||
|
||||
/**
|
||||
* Creates an instance of UmbMfaServerDataSource.
|
||||
* @param {UmbControllerHost} host
|
||||
* @memberof UmbMfaServerDataSource
|
||||
*/
|
||||
constructor(host: UmbControllerHost) {
|
||||
this.#host = host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the MFA providers for a user
|
||||
* @param unique The unique id of the user
|
||||
* @memberof UmbMfaServerDataSource
|
||||
*/
|
||||
requestMfaProviders(unique: string) {
|
||||
if (!unique) throw new Error('User id is missing');
|
||||
|
||||
return tryExecuteAndNotify(
|
||||
this.#host,
|
||||
UserResource.getUserById2Fa({
|
||||
id: unique,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables a MFA provider for a user
|
||||
* @param unique The unique id of the user
|
||||
* @param providerName The name of the provider
|
||||
* @memberof UmbMfaServerDataSource
|
||||
*/
|
||||
disableMfaProvider(unique: string, providerName: string) {
|
||||
if (!unique) throw new Error('User id is missing');
|
||||
if (!providerName) throw new Error('Provider is missing');
|
||||
|
||||
return tryExecute(
|
||||
UserResource.deleteUserById2FaByProviderName({
|
||||
id: unique,
|
||||
providerName,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,12 @@
|
||||
import { UmbUserMfaServerDataSource } from './sources/user-mfa.server.data-source.js';
|
||||
import { UmbUserSetGroupsServerDataSource } from './sources/user-set-group.server.data-source.js';
|
||||
import { UmbUserRepositoryBase } from './user-repository-base.js';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { of } from '@umbraco-cms/backoffice/external/rxjs';
|
||||
import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
|
||||
|
||||
export class UmbUserRepository extends UmbUserRepositoryBase {
|
||||
#setUserGroupsSource: UmbUserSetGroupsServerDataSource;
|
||||
|
||||
constructor(host: UmbControllerHost) {
|
||||
super(host);
|
||||
|
||||
this.#setUserGroupsSource = new UmbUserSetGroupsServerDataSource(host);
|
||||
}
|
||||
#setUserGroupsSource = new UmbUserSetGroupsServerDataSource(this._host);
|
||||
#userMfaSource = new UmbUserMfaServerDataSource(this._host);
|
||||
|
||||
async setUserGroups(userIds: Array<string>, userGroupIds: Array<string>) {
|
||||
if (userGroupIds.length === 0) throw new Error('User group ids are missing');
|
||||
@@ -23,4 +20,42 @@ export class UmbUserRepository extends UmbUserRepositoryBase {
|
||||
|
||||
return { error };
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the MFA providers for a user
|
||||
* @param unique The unique id of the user
|
||||
* @memberof UmbUserRepository
|
||||
*/
|
||||
async requestMfaProviders(unique: string) {
|
||||
const { data, error } = await this.#userMfaSource.requestMfaProviders(unique);
|
||||
return { data, error, asObservable: () => of(data ?? []) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables a MFA provider for a user
|
||||
* @param unique The unique id of the user
|
||||
* @param providerName The name of the provider
|
||||
* @param displayName The display name of the provider to show in the notification (optional)
|
||||
* @memberof UmbUserRepository
|
||||
*/
|
||||
async disableMfaProvider(unique: string, providerName: string, displayName?: string) {
|
||||
const { data, error } = await this.#userMfaSource.disableMfaProvider(unique, providerName);
|
||||
|
||||
const localize = new UmbLocalizationController(this._host);
|
||||
|
||||
if (!error) {
|
||||
const notification = {
|
||||
data: { message: localize.term('user_2faProviderIsDisabledMsg', displayName ?? providerName) },
|
||||
};
|
||||
this.notificationContext?.peek('positive', notification);
|
||||
} else {
|
||||
console.error('Failed to disable MFA provider', error);
|
||||
const notification = {
|
||||
data: { message: localize.term('user_2faProviderIsNotDisabledMsg', displayName ?? providerName) },
|
||||
};
|
||||
this.notificationContext?.peek('warning', notification);
|
||||
}
|
||||
|
||||
return { data, error };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { UmbUserEntityType } from './entity.js';
|
||||
import { UserStateModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import { UserStateModel, type UserTwoFactorProviderModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
|
||||
export type UmbUserStateEnum = UserStateModel;
|
||||
export const UmbUserStateEnum = UserStateModel;
|
||||
@@ -22,4 +22,7 @@ export interface UmbUserDetailModel {
|
||||
lastLoginDate: string | null;
|
||||
lastLockoutDate: string | null;
|
||||
lastPasswordChangeDate: string | null;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
export type UmbUserMfaProviderModel = UserTwoFactorProviderModel;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './is-user.function.js';
|
||||
@@ -0,0 +1,12 @@
|
||||
import { UmbUserDetailRepository } from '../repository/index.js';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
/**
|
||||
* Check if the user is an admin
|
||||
*/
|
||||
export const isUserAdmin = async (host: UmbControllerHost, userUnique: string) => {
|
||||
const repository = new UmbUserDetailRepository(host);
|
||||
const { data: user } = await repository.requestByUnique(userUnique);
|
||||
|
||||
return user?.isAdmin ?? false;
|
||||
};
|
||||
Reference in New Issue
Block a user