diff --git a/src/Umbraco.Web.UI.Client/devops/build/check-path-length.js b/src/Umbraco.Web.UI.Client/devops/build/check-path-length.js index d21097639b..e195c7e7c0 100644 --- a/src/Umbraco.Web.UI.Client/devops/build/check-path-length.js +++ b/src/Umbraco.Web.UI.Client/devops/build/check-path-length.js @@ -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'); +} diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts index c038966225..17bd03e419 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts @@ -1927,9 +1927,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', 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 682a1bbb9a..7dc7c62b8d 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 @@ -1936,11 +1936,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', diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/user/user.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/user/user.data.ts index e736fc264e..b2e34eae17 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/user/user.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/user/user.data.ts @@ -60,10 +60,10 @@ export const data: Array = [ 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 = [ 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 = [ 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 = [ { isEnabledOnUser: true, diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/user/user.db.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/user/user.db.ts index 046f00bc53..2c5787ac29 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/user/user.db.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/user/user.db.ts @@ -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), }; }; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts index d0ee368388..fbfc33ed1a 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts @@ -63,6 +63,26 @@ export const manifestDevelopmentHandlers = [ }, ], }, + { + name: 'My MFA Package', + extensions: [ + { + type: 'mfaLoginProvider', + 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', + }, + }, + ], + }, { name: 'Package with a view', extensions: [ diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/index.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/index.ts index 43a356f3bb..bcd348f3f4 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/index.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/index.ts @@ -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, ]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/mfa.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/mfa.handlers.ts new file mode 100644 index 0000000000..e3316bf1b0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/mfa.handlers.ts @@ -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)); + }), +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/views/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/views/manifests.ts index 10a379a924..c92f2b2d77 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/views/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/views/manifests.ts @@ -6,7 +6,7 @@ export const workspaceViews: Array = [ 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', diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/views/block-grid-area-type-workspace-view-settings.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/views/settings.element.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/views/block-grid-area-type-workspace-view-settings.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/views/settings.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/debug/debug.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/debug/debug.element.ts index 4cf6debdd6..1059ecd908 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/debug/debug.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/debug/debug.element.ts @@ -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`
+ return html` +
- Debug +  Debug
`; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/debug/modals/debug/debug-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/debug/modals/debug/debug-modal.element.ts index d782460c12..255ce97a88 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/debug/modals/debug/debug-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/debug/modals/debug/debug-modal.element.ts @@ -13,7 +13,9 @@ export default class UmbContextDebuggerModalElement extends UmbModalBaseElement< return html` Debug: Contexts - ${this.data?.content} + + ${this.data?.content} + Close `; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/debug/stories/debug.mdx b/src/Umbraco.Web.UI.Client/src/packages/core/debug/stories/debug.mdx index bc1d5399d0..188a7a3c28 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/debug/stories/debug.mdx +++ b/src/Umbraco.Web.UI.Client/src/packages/core/debug/stories/debug.mdx @@ -16,24 +16,24 @@ This can help with the developer experience to quickly see what is available to ### Usage -The `` 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 `` 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. ```typescript // This will add a Debug button to the UI and once clicked the information about avilable contextes will slide down - + ``` #### 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. ```typescript // This will open the debug information in a small dialog/modal from the right hand side - + ``` #### 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 + + ``` diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/bulk-action-permissions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/bulk-action-permissions/manifests.ts index f0fda71aaa..1a9b24ee5f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/bulk-action-permissions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/bulk-action-permissions/manifests.ts @@ -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', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/bulk-action-permissions/property-editor-ui-collection-view-bulk-action-permissions.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/bulk-action-permissions/permissions.element.ts similarity index 86% rename from src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/bulk-action-permissions/property-editor-ui-collection-view-bulk-action-permissions.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/bulk-action-permissions/permissions.element.ts index 4915a0d8ca..9233e7b7d4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/bulk-action-permissions/property-editor-ui-collection-view-bulk-action-permissions.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/bulk-action-permissions/permissions.element.ts @@ -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; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/bulk-action-permissions/permissions.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/bulk-action-permissions/permissions.stories.ts new file mode 100644 index 0000000000..f9d86ec016 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/bulk-action-permissions/permissions.stories.ts @@ -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 = () => + html``; +AAAOverview.storyName = 'Overview'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/bulk-action-permissions/property-editor-ui-collection-view-bulk-action-permissions.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/bulk-action-permissions/permissions.test.ts similarity index 53% rename from src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/bulk-action-permissions/property-editor-ui-collection-view-bulk-action-permissions.test.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/bulk-action-permissions/permissions.test.ts index 019979333e..94d53fe617 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/bulk-action-permissions/property-editor-ui-collection-view-bulk-action-permissions.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/bulk-action-permissions/permissions.test.ts @@ -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` - + `); }); 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) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/bulk-action-permissions/property-editor-ui-collection-view-bulk-action-permissions.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/bulk-action-permissions/property-editor-ui-collection-view-bulk-action-permissions.stories.ts deleted file mode 100644 index 1fdeb9e17a..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/bulk-action-permissions/property-editor-ui-collection-view-bulk-action-permissions.stories.ts +++ /dev/null @@ -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 = () => - html``; -AAAOverview.storyName = 'Overview'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/column-configuration/property-editor-ui-collection-view-column-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/column/column-configuration.element.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/column-configuration/property-editor-ui-collection-view-column-configuration.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/column/column-configuration.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/column-configuration/property-editor-ui-collection-view-column-configuration.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/column/column-configuration.stories.ts similarity index 80% rename from src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/column-configuration/property-editor-ui-collection-view-column-configuration.stories.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/column/column-configuration.stories.ts index 961a2454cf..21ef92946c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/column-configuration/property-editor-ui-collection-view-column-configuration.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/column/column-configuration.stories.ts @@ -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', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/column-configuration/property-editor-ui-collection-view-column-configuration.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/column/column-configuration.test.ts similarity index 91% rename from src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/column-configuration/property-editor-ui-collection-view-column-configuration.test.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/column/column-configuration.test.ts index 9bc2a6e955..bb0b17070f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/column-configuration/property-editor-ui-collection-view-column-configuration.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/column/column-configuration.test.ts @@ -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', () => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/column-configuration/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/column/manifests.ts similarity index 81% rename from src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/column-configuration/manifests.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/column/manifests.ts index d06c7f1ade..7f1500ee96 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/column-configuration/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/column/manifests.ts @@ -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', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/layout-configuration/property-editor-ui-collection-view-layout-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/layout/layout-configuration.element.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/layout-configuration/property-editor-ui-collection-view-layout-configuration.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/layout/layout-configuration.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/layout-configuration/property-editor-ui-collection-view-layout-configuration.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/layout/layout-configuration.stories.ts similarity index 80% rename from src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/layout-configuration/property-editor-ui-collection-view-layout-configuration.stories.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/layout/layout-configuration.stories.ts index c1e3cd0f17..e9da7ae5df 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/layout-configuration/property-editor-ui-collection-view-layout-configuration.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/layout/layout-configuration.stories.ts @@ -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', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/layout-configuration/property-editor-ui-collection-view-layout-configuration.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/layout/layout-configuration.test.ts similarity index 91% rename from src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/layout-configuration/property-editor-ui-collection-view-layout-configuration.test.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/layout/layout-configuration.test.ts index 602083f78a..bb009fe9ea 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/layout-configuration/property-editor-ui-collection-view-layout-configuration.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/layout/layout-configuration.test.ts @@ -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', () => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/layout-configuration/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/layout/manifests.ts similarity index 81% rename from src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/layout-configuration/manifests.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/layout/manifests.ts index 411d187f3d..310c0ce22b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/layout-configuration/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/layout/manifests.ts @@ -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', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/order-by/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/order-by/manifests.ts index d8871bb956..4ce645e9fb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/order-by/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/order-by/manifests.ts @@ -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', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/order-by/property-editor-ui-collection-view-order-by.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/order-by/order-by.element.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/order-by/property-editor-ui-collection-view-order-by.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/order-by/order-by.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/order-by/property-editor-ui-collection-view-order-by.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/order-by/order-by.stories.ts similarity index 82% rename from src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/order-by/property-editor-ui-collection-view-order-by.stories.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/order-by/order-by.stories.ts index 611de8458b..d16e448f1e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/order-by/property-editor-ui-collection-view-order-by.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/order-by/order-by.stories.ts @@ -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', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/order-by/property-editor-ui-collection-view-order-by.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/order-by/order-by.test.ts similarity index 93% rename from src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/order-by/property-editor-ui-collection-view-order-by.test.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/order-by/order-by.test.ts index 15614d6781..a3c53b41db 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/order-by/property-editor-ui-collection-view-order-by.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/config/order-by/order-by.test.ts @@ -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', () => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/manifests.ts index 33e003f94a..a327ffc009 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/manifests.ts @@ -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'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/resources/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/resources/index.ts index 6b2ae359d4..7b0f05fef4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/resources/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/resources/index.ts @@ -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'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/section/components/input-section/input-section.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/components/input-section/input-section.element.ts index f06139c632..40040eca48 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/section/components/input-section/input-section.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/components/input-section/input-section.element.ts @@ -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)); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/components/data-type-input/data-type-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/components/data-type-input/data-type-input.element.ts index f9cfb17624..0e39df71c7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/components/data-type-input/data-type-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/components/data-type-input/data-type-input.element.ts @@ -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)); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.element.ts index befb7d5a4e..c022d1000d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.element.ts @@ -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)); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts index 74711e310c..f643a477a6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts @@ -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)); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/components/input-language/input-language.element.ts b/src/Umbraco.Web.UI.Client/src/packages/language/components/input-language/input-language.element.ts index fea8574cc8..1f99dde40e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/components/input-language/input-language.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/components/input-language/input-language.element.ts @@ -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)); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts index a443132a7e..111e7e7fca 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts @@ -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)); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts index 55e79a1541..1367987c5f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts @@ -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( diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.element.ts index daca53081b..c0fc8a2ae8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.element.ts @@ -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; }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/components/input-member-type/input-member-type.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/components/input-member-type/input-member-type.element.ts index fbf120f6f9..c42c94ae53 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/components/input-member-type/input-member-type.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/components/input-member-type/input-member-type.element.ts @@ -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)); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts index 3a0caf44b3..f4dee267b0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts @@ -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; }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/static-file/components/input-static-file/input-static-file.element.ts b/src/Umbraco.Web.UI.Client/src/packages/static-file/components/input-static-file/input-static-file.element.ts index c06d66dfba..941e03254b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/static-file/components/input-static-file/input-static-file.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/static-file/components/input-static-file/input-static-file.element.ts @@ -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)); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/components/stylesheet-input/stylesheet-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/components/stylesheet-input/stylesheet-input.element.ts index 3d3784132c..9cd434ba75 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/components/stylesheet-input/stylesheet-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/components/stylesheet-input/stylesheet-input.element.ts @@ -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)); } 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 3bbe517784..9c735beda3 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,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 = 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'); + } } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/current-user.context.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/current-user.context.ts index 7c7e759f0f..5de00944d5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/current-user.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/current-user.context.ts @@ -55,6 +55,15 @@ export class UmbCurrentUserContext extends UmbContextBase 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 { + const currentUser = await firstValueFrom(this.currentUser); + return currentUser?.isAdmin ?? false; + } + #observeIsAuthorized() { if (!this.#authContext) return; this.observe(this.#authContext.isAuthorized, (isAuthorized) => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa-disable-provider/current-user-mfa-disable-provider-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa-disable-provider/current-user-mfa-disable-provider-modal.element.ts index c80bd47e77..709a336d2a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa-disable-provider/current-user-mfa-disable-provider-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa-disable-provider/current-user-mfa-disable-provider-modal.element.ts @@ -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'); + } } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.repository.ts index ae069e9f2b..40026ffe1a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.repository.ts @@ -1,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; 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 { - 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 { - 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 {}; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.server.data-source.ts index 69a1bb8b58..1d0fcb88aa 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.server.data-source.ts @@ -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 {}; + } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.store.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.store.ts index 9d30056316..1e6a9ffa1f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.store.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.store.ts @@ -72,6 +72,7 @@ export class UmbCurrentUserStore extends UmbContextBase { documentStartNodeUniques: updatedCurrentUser.documentStartNodeUniques, mediaStartNodeUniques: updatedCurrentUser.mediaStartNodeUniques, avatarUrls: updatedCurrentUser.avatarUrls, + isAdmin: updatedCurrentUser.isAdmin, }; this.update(mappedCurrentUser); 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 f6c67bf129..ba8898ade5 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 @@ -1,4 +1,6 @@ import type { + ApiError, + CancelError, DocumentPermissionPresentationModel, UnknownTypePermissionPresentationModel, UserTwoFactorProviderModel, @@ -18,10 +20,13 @@ export interface UmbCurrentUserModel { allowedSections: Array; fallbackPermissions: Array; permissions: Array; + 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; + callback: (providerName: string, code: string, secret: string) => UmbMfaProviderConfigurationCallback; /** * Call this function to close the modal. diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/utils/is-current-user.function.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/utils/is-current-user.function.ts index 820146e9a3..5fb077125e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/utils/is-current-user.function.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/utils/is-current-user.function.ts @@ -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(); }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/components/input-user-group/user-group-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/components/input-user-group/user-group-input.element.ts index 8457ae2134..e0342b556b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/components/input-user-group/user-group-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/components/input-user-group/user-group-input.element.ts @@ -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( diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/repository/user-collection.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/repository/user-collection.server.data-source.ts index aeeda6459f..a6aeed6726 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/repository/user-collection.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/repository/user-collection.server.data-source.ts @@ -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; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.element.ts index 6b4f8537e7..dd6846ac76 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.element.ts @@ -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( diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/manifests.ts index 15d506c0c8..e085874d5e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/manifests.ts @@ -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, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/user-allow-action-base.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/user-allow-action-base.condition.ts index 5ffcee3d46..813640f348 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/user-allow-action-base.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/user-allow-action-base.condition.ts @@ -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 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) { @@ -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 diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/user-allow-mfa-action.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/user-allow-mfa-action.condition.ts new file mode 100644 index 0000000000..af54e401b8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/user-allow-mfa-action.condition.ts @@ -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 { + // 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, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/disable/disable-user.action.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/disable/disable-user.action.ts index ff604d6691..79bc7b187b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/disable/disable-user.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/disable/disable-user.action.ts @@ -16,18 +16,20 @@ export class UmbDisableUserEntityAction extends UmbEntityActionBase { 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]); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/enable/enable-user.action.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/enable/enable-user.action.ts index a8b5b428eb..6e19458210 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/enable/enable-user.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/enable/enable-user.action.ts @@ -16,17 +16,19 @@ export class UmbEnableUserEntityAction extends UmbEntityActionBase { 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]); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/manifests.ts index 879e196811..a8200fddfc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/manifests.ts @@ -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 = [ @@ -90,6 +91,24 @@ const entityActions: Array = [ }, ], }, + { + 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]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/mfa/mfa-user.action.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/mfa/mfa-user.action.ts new file mode 100644 index 0000000000..cd96ab140e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/mfa/mfa-user.action.ts @@ -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 { + constructor(host: UmbControllerHost, args: UmbEntityActionArgs) { + 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); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/unlock/unlock-user.action.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/unlock/unlock-user.action.ts index 5fabd55505..00df1dbb47 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/unlock/unlock-user.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/unlock/unlock-user.action.ts @@ -16,17 +16,19 @@ export class UmbUnlockUserEntityAction extends UmbEntityActionBase { 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]); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/index.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/index.ts index 516c4d7ade..0eb2746d9c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/index.ts @@ -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'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/manifests.ts index e01a970060..1489398aeb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/manifests.ts @@ -19,6 +19,12 @@ const modals: Array = [ 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]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-mfa/user-mfa-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-mfa/user-mfa-modal.element.ts new file mode 100644 index 0000000000..faa4bdfded --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-mfa/user-mfa-modal.element.ts @@ -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; + + @state() + _items: Array = []; + + #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` + +
+ ${when( + this._items.length > 0, + () => html` + ${repeat( + this._items, + (item) => item.providerName, + (item) => this.#renderProvider(item), + )} + `, + )} +
+
+ + ${this.localize.term('general_close')} + +
+
+ `; + } + + /** + * Render a provider with a toggle to enable/disable it + */ + #renderProvider(item: UmbMfaLoginProviderOption) { + return html` + + ${when( + item.isEnabledOnUser, + () => html` +

+ This two-factor provider is enabled + +

+ this.#onProviderDisable(item)}> + `, + () => html` + + `, + )} +
+ `; + } + + /** + * 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; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-mfa/user-mfa-modal.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-mfa/user-mfa-modal.stories.ts new file mode 100644 index 0000000000..1f5bac4788 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-mfa/user-mfa-modal.stories.ts @@ -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``; + } +} + +if (window.customElements.get('umb-server-extensions-host') === undefined) { + customElements.define('umb-server-extensions-host', UmbServerExtensionsHostElement); +} + +const meta: Meta = { + title: 'User/MFA/Configure MFA Providers', + component: 'umb-user-mfa-modal', + decorators: [ + (Story) => + html` + ${Story()} + `, + ], + parameters: { + layout: 'centered', + actions: { + disabled: true, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Overview: Story = {}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-mfa/user-mfa-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-mfa/user-mfa-modal.token.ts new file mode 100644 index 0000000000..aec11cb24e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-mfa/user-mfa-modal.token.ts @@ -0,0 +1,12 @@ +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export type UmbUserMfaModalConfiguration = { + unique: string; +}; + +export const UMB_USER_MFA_MODAL = new UmbModalToken('Umb.Modal.User.Mfa', { + modal: { + type: 'sidebar', + size: 'small', + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/change-password/change-user-password.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/change-password/change-user-password.repository.ts index 6c6792d6e4..404958ae52 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/change-password/change-user-password.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/change-password/change-user-password.repository.ts @@ -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 }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/detail/user-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/detail/user-detail.server.data-source.ts index c1d2a6f98f..419c410264 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/detail/user-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/detail/user-detail.server.data-source.ts @@ -50,6 +50,7 @@ export class UmbUserServerDataSource implements UmbDetailDataSource, userGroupIds: Array) { 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 }; + } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/types.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/types.ts index 3cb879b380..f52a0979ef 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/types.ts @@ -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; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/utils/index.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/utils/index.ts new file mode 100644 index 0000000000..5d1478f0fa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/utils/index.ts @@ -0,0 +1 @@ +export * from './is-user.function.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/utils/is-user.function.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/utils/is-user.function.ts new file mode 100644 index 0000000000..42ee46012f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/utils/is-user.function.ts @@ -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; +};