diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts index 07abe034016d..a7984841b670 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts @@ -342,6 +342,24 @@ export default { blueprintDescription: 'En indholdskabelon er foruddefineret indhold, som en redaktør kan vælge at bruge\n som grundlag for at oprette nyt indhold\n ', }, + entityDetail: { + notFoundTitle: (entityType: string) => { + const entityName = entityType ?? 'Elementet'; + return `${entityName} blev ikke fundet`; + }, + notFoundDescription: (entityType: string) => { + const entityName = entityType ?? 'element'; + return `Den/det ønskede ${entityName} kunne ikke findes. Dette kan skyldes, at den/det er blevet slettet, eller at du ikke har adgang. Kontakt din administrator for hjælp.`; + }, + forbiddenTitle: (entityType: string) => { + const entityName = entityType ?? 'Elementet'; + return `${entityName} er ikke tilgængelig`; + }, + forbiddenDescription: (entityType: string) => { + const entityName = entityType ?? 'element'; + return `Du har ikke adgang til den/det ønskede ${entityName}. Kontakt din administrator for hjælp.`; + }, + }, media: { clickToUpload: 'Klik for at uploade', orClickHereToUpload: 'eller klik her for at vælge filer', @@ -2605,6 +2623,9 @@ export default { routing: { routeNotFoundTitle: 'Ikke fundet', routeNotFoundDescription: 'Den side du leder efter kunne ikke findes. Kontroller adressen og prøv igen.', + routeForbiddenTitle: 'Adgang nægtet', + routeForbiddenDescription: + 'Du har ikke tilladelse til at få adgang til denne ressource. Kontakt venligst din administrator for hjælp.', }, codeEditor: { label: 'Code editor', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/de.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/de.ts index b33b1c09a667..799967daeaf8 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/de.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/de.ts @@ -344,6 +344,24 @@ export default { blueprintDescription: 'Eine Inhaltsvorlage ist vordefinierter Inhalt,\n den ein Redakteur als Basis für neuen Inhalt verwenden kann\n ', }, + entityDetail: { + notFoundTitle: (entityType: string) => { + const entityName = entityType ?? 'Element'; + return `${entityName} nicht gefunden`; + }, + notFoundDescription: (entityType: string) => { + const entityName = entityType ?? 'element'; + return `Der angeforderte ${entityName} konnte nicht gefunden werden. Bitte überprüfen Sie die URL und versuchen Sie es erneut.`; + }, + forbiddenTitle: (entityType: string) => { + const entityName = entityType ?? 'Element'; + return `${entityName} nicht verfügbar`; + }, + forbiddenDescription: (entityType: string) => { + const entityName = entityType ?? 'dieses Element'; + return `Sie haben keine Berechtigung, auf ${entityName} zuzugreifen. Bitte wenden Sie sich an Ihren Administrator, um Unterstützung zu erhalten.`; + }, + }, media: { clickToUpload: 'Für Upload klicken', orClickHereToUpload: 'oder klicken Sie hier um eine Datei zu wählen', @@ -2007,4 +2025,12 @@ export default { searchResult: 'Element zurückgegeben', searchResults: 'Elemente zurückgegeben', }, + routing: { + routeNotFoundTitle: 'Seite wurde nicht gefunden', + routeNotFoundDescription: + 'Die angeforderte Seite konnte nicht gefunden werden. Bitte überprüfen Sie die URL und versuchen Sie es erneut.', + routeForbiddenTitle: 'Zugriff verweigert', + routeForbiddenDescription: + 'Sie haben keine Berechtigung, auf diese Seite zuzugreifen. Bitte wenden Sie sich an Ihren Administrator, um Unterstützung zu erhalten.', + }, } as UmbLocalizationDictionary; diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index f7a62dc34638..ffc2bb9abf35 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -373,6 +373,14 @@ export default { const entityName = entityType ?? 'item'; return `The requested ${entityName} could not be found. Please check the URL and try again.`; }, + forbiddenTitle: (entityType: string) => { + const entityName = entityType ?? 'item'; + return `Access denied to this ${entityName}`; + }, + forbiddenDescription: (entityType: string) => { + const entityName = entityType ?? 'item'; + return `You do not have permission to access this ${entityName}. Please contact your administrator for assistance.`; + }, }, media: { clickToUpload: 'Click to upload', @@ -2735,6 +2743,9 @@ export default { routing: { routeNotFoundTitle: 'Not found', routeNotFoundDescription: 'The requested route could not be found. Please check the URL and try again.', + routeForbiddenTitle: 'Access denied', + routeForbiddenDescription: + 'You do not have permission to access this resource. Please contact your administrator for assistance.', }, codeEditor: { label: 'Code editor', diff --git a/src/Umbraco.Web.UI.Client/src/mocks/browser-handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/browser-handlers.ts index 7ce13e1c6443..6003f11ff521 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/browser-handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/browser-handlers.ts @@ -39,6 +39,7 @@ import * as manifestsHandlers from './handlers/manifests.handlers.js'; import * as serverHandlers from './handlers/server.handlers.js'; import { handlers as documentBlueprintHandlers } from './handlers/document-blueprint/index.js'; import { handlers as temporaryFileHandlers } from './handlers/temporary-file/index.js'; +import { handlers as segmentHandlers } from './handlers/segment.handlers.js'; const handlers = [ ...backofficeHandlers, @@ -80,6 +81,7 @@ const handlers = [ ...userHandlers, ...documentBlueprintHandlers, ...temporaryFileHandlers, + ...segmentHandlers, ...serverHandlers.serverInformationHandlers, serverHandlers.serverRunningHandler, ...manifestsHandlers.manifestEmptyHandlers, diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts index c317402c4c7c..571074884ee4 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts @@ -31,6 +31,18 @@ export const data: Array = [ isDeletable: true, canIgnoreStartNodes: false, }, + { + id: 'forbidden', + parent: null, + name: 'Forbidden Data Type', + editorAlias: 'Umbraco.TextBox', + editorUiAlias: 'Umb.PropertyEditorUi.TextBox', + values: [], + hasChildren: false, + isFolder: false, + isDeletable: true, + canIgnoreStartNodes: false, + }, { id: '0cc0eba1-9960-42c9-bf9b-60e150b429ae', parent: null, diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/dictionary/dictionary.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/dictionary/dictionary.data.ts index 3f68fa97f598..ba7e0e18bf84 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/dictionary/dictionary.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/dictionary/dictionary.data.ts @@ -11,6 +11,23 @@ export type UmbMockDictionaryModel = DictionaryItemResponseModel & DictionaryOverviewResponseModel; export const data: Array = [ + { + name: 'Forbidden', + id: 'forbidden', + parent: null, + hasChildren: false, + translatedIsoCodes: ['en-us'], + translations: [ + { + isoCode: 'en-us', + translation: 'This is a forbidden dictionary item', + }, + { + isoCode: 'da', + translation: 'Dette er et forbudt ordbogsobjekt', + }, + ], + }, { name: 'Hello', id: 'aae7d0ab-53ba-485d-b8bd-12537f9925cb', diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/document-blueprint/document-blueprint.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/document-blueprint/document-blueprint.data.ts index 8588c17bb2b4..6be07dc36992 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/document-blueprint/document-blueprint.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/document-blueprint/document-blueprint.data.ts @@ -18,6 +18,7 @@ export const data: Array = [ }, hasChildren: false, isFolder: false, + parent: null, name: 'The Simplest Document Blueprint', variants: [ { @@ -40,4 +41,35 @@ export const data: Array = [ }, ], }, + { + id: 'forbidden', + documentType: { + id: 'the-simplest-document-type-id', + icon: 'icon-document', + }, + hasChildren: false, + isFolder: false, + parent: null, + name: 'A Forbidden Document Blueprint', + variants: [ + { + state: DocumentVariantStateModel.DRAFT, + publishDate: '2023-02-06T15:32:24.957009', + culture: 'en-US', + segment: null, + name: 'A Forbidden Document Blueprint', + createDate: '2023-02-06T15:32:05.350038', + updateDate: '2023-02-06T15:32:24.957009', + }, + ], + values: [ + { + editorAlias: 'Umbraco.TextBox', + alias: 'prop1', + culture: null, + segment: null, + value: 'my blueprint value', + }, + ], + }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts index 0c1815eaf7e5..7314cc77d3c9 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts @@ -1870,4 +1870,58 @@ export const data: Array = [ keepLatestVersionPerDayForDays: null, }, }, + { + allowedTemplates: [], + defaultTemplate: { id: 'the-simplest-document-type-id' }, + id: 'forbidden', + alias: 'forbidden', + name: 'A forbidden document type', + description: null, + icon: 'icon-document', + allowedAsRoot: true, + variesByCulture: false, + variesBySegment: false, + isElement: false, + hasChildren: false, + parent: null, + isFolder: false, + properties: [ + { + id: '1680d4d2-cda8-4ac2-affd-a69fc10382b1', + container: { id: 'the-simplest-document-type-id-container' }, + alias: 'prop1', + name: 'Prop 1', + description: null, + dataType: { id: '0cc0eba1-9960-42c9-bf9b-60e150b429ae' }, + variesByCulture: false, + variesBySegment: false, + sortOrder: 0, + validation: { + mandatory: false, + mandatoryMessage: null, + regEx: null, + regExMessage: null, + }, + appearance: { + labelOnTop: false, + }, + }, + ], + containers: [ + { + id: 'the-simplest-document-type-id-container', + parent: null, + name: 'Content', + type: 'Group', + sortOrder: 0, + }, + ], + allowedDocumentTypes: [], + compositions: [], + cleanup: { + preventCleanup: false, + keepAllVersionsNewerThanDays: null, + keepLatestVersionPerDayForDays: null, + }, + }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts index ec53e44a1c55..ca181f29ab48 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts @@ -28,7 +28,7 @@ export const data: Array = [ { state: DocumentVariantStateModel.DRAFT, publishDate: '2023-02-06T15:32:24.957009', - culture: 'en-us', + culture: 'en-US', segment: null, name: 'The Simplest Document', createDate: '2023-02-06T15:32:05.350038', @@ -1227,5 +1227,50 @@ export const data: Array = [ }, ], }, + { + ancestors: [], + urls: [], + template: null, + id: 'forbidden', + createDate: '2023-02-06T15:32:05.350038', + parent: null, + documentType: { + id: 'the-simplest-document-type-id', + icon: 'icon-document', + }, + hasChildren: false, + noAccess: false, + isProtected: false, + isTrashed: false, + variants: [ + { + state: DocumentVariantStateModel.PUBLISHED, + publishDate: '2023-02-06T15:32:24.957009', + culture: 'en-US', + segment: null, + name: 'A forbidden document', + createDate: '2023-02-06T15:32:05.350038', + updateDate: '2023-02-06T15:32:24.957009', + }, + { + state: DocumentVariantStateModel.PUBLISHED, + publishDate: '2023-02-06T15:32:24.957009', + culture: 'da-dk', + segment: null, + name: 'Et utilgængeligt dokument', + createDate: '2023-02-06T15:32:05.350038', + updateDate: '2023-02-06T15:32:24.957009', + }, + ], + values: [ + { + editorAlias: 'Umbraco.TextBox', + alias: 'prop1', + culture: null, + segment: null, + value: 'default value here', + }, + ], + }, ...permissionsTestData, ]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/language/language.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/language/language.data.ts index 10c0771e8bff..1f3136ab67fa 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/language/language.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/language/language.data.ts @@ -16,4 +16,10 @@ export const data: Array = [ isMandatory: false, fallbackIsoCode: 'en-US', }, + { + name: 'Forbidden', + isoCode: 'forbidden', + isDefault: false, + isMandatory: false, + }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/media-type/media-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/media-type/media-type.data.ts index 1614c94fc1ca..4e01037aebcc 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/media-type/media-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/media-type/media-type.data.ts @@ -304,4 +304,54 @@ export const data: Array = [ isDeletable: false, aliasCanBeChanged: false, }, + { + name: 'A Forbidden Media Type', + id: 'forbidden', + parent: null, + description: 'Clicking on this results in a 403 Forbidden error', + alias: 'forbidden', + icon: 'icon-document', + properties: [ + { + id: '19', + container: { id: 'c3cd2f12-b7c4-4206-8d8b-27c061589f75' }, + alias: 'umbracoFile', + name: 'File', + description: '', + dataType: { id: 'dt-uploadFieldFiles' }, + variesByCulture: false, + variesBySegment: false, + sortOrder: 0, + validation: { + mandatory: true, + mandatoryMessage: null, + regEx: null, + regExMessage: null, + }, + appearance: { + labelOnTop: false, + }, + }, + ], + containers: [ + { + id: 'c3cd2f12-b7c4-4206-8d8b-27c061589f75', + parent: null, + name: 'Content', + type: 'Group', + sortOrder: 0, + }, + ], + allowedAsRoot: true, + variesByCulture: false, + variesBySegment: false, + isElement: false, + allowedMediaTypes: [{ mediaType: { id: 'forbidden' }, sortOrder: 0 }], + compositions: [], + isFolder: false, + hasChildren: false, + collection: { id: 'dt-collectionView' }, + isDeletable: true, + aliasCanBeChanged: false, + }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/media/media.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/media/media.data.ts index dc4e3507dd1c..33cc29780051 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/media/media.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/media/media.data.ts @@ -244,4 +244,36 @@ export const data: Array = [ ], urls: [], }, + { + hasChildren: false, + id: 'forbidden', + createDate: '2023-02-06T15:32:05.350038', + parent: null, + noAccess: false, + isTrashed: false, + mediaType: { + id: 'media-type-1-id', + icon: 'icon-picture', + }, + values: [ + { + editorAlias: 'Umbraco.UploadField', + alias: 'mediaPicker', + value: { + src: '/umbraco/backoffice/assets/installer-illustration.svg', + }, + }, + ], + variants: [ + { + publishDate: '2023-02-06T15:31:51.354764', + culture: null, + segment: null, + name: 'Forbidden Media', + createDate: '2023-02-06T15:31:46.876902', + updateDate: '2023-02-06T15:31:51.354764', + }, + ], + urls: [], + }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/member-group/member-group.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/member-group/member-group.data.ts index 7ac00b079acc..d0ffd87cf4b1 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/member-group/member-group.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/member-group/member-group.data.ts @@ -11,4 +11,8 @@ export const data: Array = [ name: 'Member Group 2', id: 'member-group-2-id', }, + { + name: 'Forbidden Member Group', + id: 'forbidden', + }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/member-type/member-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/member-type/member-type.data.ts index a2466459f413..918ba7f61d84 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/member-type/member-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/member-type/member-type.data.ts @@ -59,4 +59,52 @@ export const data: Array = [ hasChildren: false, hasListView: false, }, + { + name: 'A Forbidden Member Type', + id: 'forbidden', + description: 'Clicking on this results in a 403 Forbidden error.', + alias: 'forbidden', + icon: 'icon-bug', + properties: [ + { + id: '1680d4d2-cda8-4ac2-affd-a69fc10382b1', + container: { id: 'the-simplest-document-type-id-container' }, + alias: 'prop1', + name: 'Prop 1', + description: null, + isSensitive: false, + visibility: { memberCanEdit: true, memberCanView: true }, + dataType: { id: '0cc0eba1-9960-42c9-bf9b-60e150b429ae' }, + variesByCulture: false, + variesBySegment: false, + sortOrder: 0, + validation: { + mandatory: false, + mandatoryMessage: null, + regEx: null, + regExMessage: null, + }, + appearance: { + labelOnTop: false, + }, + }, + ], + containers: [ + { + id: 'the-simplest-document-type-id-container', + parent: null, + name: 'Content', + type: 'Group', + sortOrder: 0, + }, + ], + allowedAsRoot: false, + variesByCulture: false, + variesBySegment: false, + isElement: false, + compositions: [], + parent: null, + hasChildren: false, + hasListView: false, + }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/member/member.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/member/member.data.ts index ee2d5c0e1d06..55597474c2e0 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/member/member.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/member/member.data.ts @@ -82,4 +82,29 @@ export const data: Array = [ ], kind: MemberKindModel.DEFAULT, }, + { + email: 'forbidden@example.com', + failedPasswordAttempts: 0, + groups: [], + id: 'forbidden', + isApproved: false, + isLockedOut: false, + isTwoFactorEnabled: false, + lastLockoutDate: null, + lastLoginDate: null, + lastPasswordChangeDate: null, + memberType: { id: 'member-type-1-id', icon: '' }, + username: 'forbidden', + values: [], + variants: [ + { + name: 'A Forbidden Member', + culture: 'en-us', + segment: null, + createDate: '2023-02-06T15:31:46.876902', + updateDate: '2023-02-06T15:31:51.354764', + }, + ], + kind: MemberKindModel.DEFAULT, + }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/member/member.db.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/member/member.db.ts index 77f41e2cde76..076b7234c8c5 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/member/member.db.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/member/member.db.ts @@ -3,6 +3,7 @@ import { UmbMockEntityItemManager } from '../utils/entity/entity-item.manager.js import { UmbMockEntityDetailManager } from '../utils/entity/entity-detail.manager.js'; import { umbMemberTypeMockDb } from '../member-type/member-type.db.js'; import { UmbMockContentCollectionManager } from '../utils/content/content-collection.manager.js'; +import { objectArrayFilter, queryFilter } from '../utils.js'; import type { UmbMockMemberModel } from './member.data.js'; import { data } from './member.data.js'; import { UmbId } from '@umbraco-cms/backoffice/id'; @@ -12,8 +13,26 @@ import { type MemberItemResponseModel, type MemberResponseModel, type MemberValueResponseModel, + type PagedMemberResponseModel, } from '@umbraco-cms/backoffice/external/backend-api'; +interface MemberFilterOptions { + skip: number; + take: number; + orderBy: string; + orderDirection: string; + memberGroupIds: Array<{ id: string }>; + memberTypeId: string; + filter: string; +} + +const memberGroupFilter = (filterOptions: MemberFilterOptions, item: UmbMockMemberModel) => + objectArrayFilter(filterOptions.memberGroupIds, item.groups, 'id'); +const memberTypeFilter = (filterOptions: MemberFilterOptions, item: UmbMockMemberModel) => + queryFilter(filterOptions.memberTypeId, item.memberType.id); +const memberQueryFilter = (filterOptions: MemberFilterOptions, item: UmbMockMemberModel) => + queryFilter(filterOptions.filter, item.username); + class UmbMemberMockDB extends UmbEntityMockDbBase { item = new UmbMockEntityItemManager(this, itemResponseMapper); detail = new UmbMockEntityDetailManager(this, createDetailMockMapper, detailResponseMapper); @@ -22,6 +41,32 @@ class UmbMemberMockDB extends UmbEntityMockDbBase { constructor(data: Array) { super(data); } + + filter(options: MemberFilterOptions): PagedMemberResponseModel { + const allItems = this.getAll(); + + const filterOptions: MemberFilterOptions = { + skip: options.skip || 0, + take: options.take || 25, + orderBy: options.orderBy || 'name', + orderDirection: options.orderDirection || 'asc', + memberGroupIds: options.memberGroupIds, + memberTypeId: options.memberTypeId || '', + filter: options.filter, + }; + + const filteredItems = allItems.filter( + (item) => + memberGroupFilter(filterOptions, item) && + memberTypeFilter(filterOptions, item) && + memberQueryFilter(filterOptions, item), + ); + const totalItems = filteredItems.length; + + const paginatedItems = filteredItems.slice(filterOptions.skip, filterOptions.skip + filterOptions.take); + + return { total: totalItems, items: paginatedItems }; + } } const createDetailMockMapper = (request: CreateMemberRequestModel): UmbMockMemberModel => { diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/partial-view/partial-view.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/partial-view/partial-view.data.ts index 0fa1bf78c7e7..0bc254e70360 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/partial-view/partial-view.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/partial-view/partial-view.data.ts @@ -10,6 +10,14 @@ export type UmbMockPartialViewModel = PartialViewResponseModel & PartialViewItemResponseModel; export const data: Array = [ + { + name: 'Forbidden', + path: '/forbidden', + parent: null, + isFolder: false, + hasChildren: false, + content: '', + }, { name: 'blockgrid', path: '/blockgrid', @@ -44,7 +52,7 @@ export const data: Array = [ hasChildren: false, content: `@using Umbraco.Extensions @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage - +
= [ @{ if (Model?.Any() != true) { return; } } - +
@@ -99,7 +107,7 @@ export const data: Array = [ { string embedValue = Convert.ToString(Model.value); embedValue = embedValue.DetectIsJson() ? Model.value.preview : Model.value; - +
@Html.Raw(embedValue)
@@ -130,18 +138,18 @@ export const snippets: Array = [ content: `@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @using Umbraco.Cms.Core.Routing @using Umbraco.Extensions - + @inject IPublishedUrlProvider PublishedUrlProvider @* This snippet makes a breadcrumb of parents using an unordered HTML list. - + How it works: - It uses the Ancestors() method to get all parents and then generates links so the visitor can go back - Finally it outputs the name of the current page (without a link) *@ - + @{ var selection = Model.Ancestors().ToArray(); } - + @if (selection?.Length > 0) { @@ -170,7 +178,7 @@ export const snippets: Array = [ @inject IMemberExternalLoginProviders memberExternalLoginProviders @inject IExternalLoginWithKeyService externalLoginWithKeyService @{ - + // Build a profile model to edit var profileModel = await memberModelBuilderFactory .CreateProfileModel() @@ -179,21 +187,21 @@ export const snippets: Array = [ // Include editable custom properties on the form .WithCustomProperties(true) .BuildForCurrentMemberAsync(); - + var success = TempData["FormSuccess"] != null; - + var loginProviders = await memberExternalLoginProviders.GetMemberProvidersAsync(); var externalSignInError = ViewData.GetExternalSignInProviderErrors(); - + var currentExternalLogin = profileModel is null ? new Dictionary() : externalLoginWithKeyService.GetExternalLogins(profileModel.Key).ToDictionary(x=>x.LoginProvider, x=>x.ProviderKey); } - + - + @if (profileModel != null) { if (success) @@ -201,7 +209,7 @@ export const snippets: Array = [ @* This message will show if profileModel.RedirectUrl is not defined (default) *@

Profile updated

} - + using (Html.BeginUmbracoForm("HandleUpdateProfile", new { RedirectUrl = profileModel.RedirectUrl })) {

Update your account.

@@ -217,7 +225,7 @@ export const snippets: Array = [
- + @if (!string.IsNullOrWhiteSpace(profileModel.UserName)) {
@@ -226,7 +234,7 @@ export const snippets: Array = [
} - + @if (profileModel.MemberProperties != null) { for (var i = 0; i < profileModel.MemberProperties.Count; i++) @@ -239,19 +247,19 @@ export const snippets: Array = [
} } - + - + if (loginProviders.Any()) {

Link external accounts

- + if (externalSignInError?.AuthenticationType is null && externalSignInError?.Errors.Any() == true) { @Html.DisplayFor(x => externalSignInError.Errors); } - + @foreach (var login in loginProviders) { if (currentExternalLogin.TryGetValue(login.ExternalLoginProvider.AuthenticationType, out var providerKey)) @@ -262,7 +270,7 @@ export const snippets: Array = [ - + if (externalSignInError?.AuthenticationType == login.ExternalLoginProvider.AuthenticationType) { @Html.DisplayFor(x => externalSignInError.Errors); @@ -276,14 +284,14 @@ export const snippets: Array = [ - + if (externalSignInError?.AuthenticationType == login.ExternalLoginProvider.AuthenticationType) { @Html.DisplayFor(x => externalSignInError.Errors); } } } - + } } } diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/script/script.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/script/script.data.ts index 24b7fb53e0fb..6d3dd6662812 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/script/script.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/script/script.data.ts @@ -129,4 +129,12 @@ export const data: Array = [ hasChildren: false, content: `alert('hello file with dash');`, }, + { + name: 'Forbidden', + path: '/forbidden', + parent: null, + isFolder: false, + hasChildren: false, + content: `console.log('You are not allowed to see this script!');`, + }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/stylesheet/stylesheet.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/stylesheet/stylesheet.data.ts index e2ce26ce2969..f8ccec957790 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/stylesheet/stylesheet.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/stylesheet/stylesheet.data.ts @@ -89,4 +89,12 @@ body { } `, }, + { + name: 'Forbidden', + path: '/forbidden', + parent: null, + isFolder: false, + hasChildren: false, + content: `console.log('You are not allowed to see this stylesheet!');`, + }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/template/template.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/template/template.data.ts index 4aadfd758e50..36c308dfd3fd 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/template/template.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/template/template.data.ts @@ -59,6 +59,14 @@ export const data: Array = [ content: '@using Umbraco.Cms.Web.Common.PublishedModels;\n@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage\r\n@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;\r\n@{\r\n\tLayout = "Test.cshtml";\r\n}', }, + { + id: 'forbidden', + parent: null, + name: 'Forbidden', + hasChildren: false, + alias: 'Forbidden', + content: `console.log('You are not allowed to see this template!');`, + }, ]; export const createTemplateScaffold = (masterTemplateAlias: string) => { diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/user-group/user-group.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/user-group/user-group.data.ts index ab62454c9ed7..11f7db6c3aa5 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/user-group/user-group.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/user-group/user-group.data.ts @@ -169,4 +169,20 @@ export const data: Array = [ aliasCanBeChanged: true, isDeletable: true, }, + { + id: 'forbidden', + name: 'Forbidden', + alias: 'forbidden', + icon: 'icon-lock', + documentStartNode: { id: 'forbidden-document-id' }, + fallbackPermissions: [], + permissions: [], + sections: [], + languages: [], + hasAccessToAllLanguages: true, + documentRootAccess: true, + mediaRootAccess: true, + aliasCanBeChanged: false, + isDeletable: false, + }, ]; 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 2043e9d2163a..97f60264fe41 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 @@ -118,6 +118,28 @@ export const data: Array = [ userGroupIds: [{ id: 'user-group-editors-id' }, { id: 'user-group-sensitive-data-id' }], userName: '', }, + { + avatarUrls: [], + createDate: '2023-10-12T18:30:32.879Z', + documentStartNodeIds: [], + email: 'forbidden@example.com', + failedLoginAttempts: 0, + hasDocumentRootAccess: true, + hasMediaRootAccess: true, + id: 'forbidden', + isAdmin: false, + kind: UserKindModel.DEFAULT, + languageIsoCode: 'en-us', + lastLockoutDate: null, + lastLoginDate: '2023-10-12T18:30:32.879Z', + lastPasswordChangeDate: null, + mediaStartNodeIds: [], + name: 'Forbidden', + state: UserStateModel.ACTIVE, + updateDate: '2023-10-12T18:30:32.879Z', + userGroupIds: [{ id: 'user-group-editors-id' }, { id: 'user-group-sensitive-data-id' }], + userName: '', + }, ]; /** 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 7a4934357eec..8e1d46acba21 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 @@ -7,11 +7,13 @@ import type { UmbMockUserModel } from './user.data.js'; import { data, mfaLoginProviders } from './user.data.js'; import { UmbId } from '@umbraco-cms/backoffice/id'; import type { + CalculatedUserStartNodesResponseModel, CreateUserRequestModel, CurrentUserResponseModel, InviteUserRequestModel, PagedUserResponseModel, UpdateUserGroupsOnUserRequestModel, + UserConfigurationResponseModel, UserItemResponseModel, UserResponseModel, } from '@umbraco-cms/backoffice/external/backend-api'; @@ -43,6 +45,47 @@ class UmbUserMockDB extends UmbEntityMockDbBase { super(data); } + calculateStartNodes(id: string): CalculatedUserStartNodesResponseModel { + const user = this.data.find((user) => user.id === id); + if (!user) { + throw new Error(`User with id ${id} not found`); + } + + return { + id: user.id, + documentStartNodeIds: user.documentStartNodeIds, + mediaStartNodeIds: user.mediaStartNodeIds, + hasDocumentRootAccess: user.hasDocumentRootAccess, + hasMediaRootAccess: user.hasMediaRootAccess, + }; + } + + clientCredentials(id: string): Array { + const user = this.data.find((user) => user.id === id); + if (!user) { + throw new Error(`User with id ${id} not found`); + } + + // TODO: Implement logic to return client credentials for the user + return []; + } + + getConfiguration(): UserConfigurationResponseModel { + return { + allowChangePassword: true, + allowTwoFactor: true, + canInviteUsers: true, + passwordConfiguration: { + minimumPasswordLength: 8, + requireDigit: true, + requireLowercase: true, + requireUppercase: true, + requireNonLetterOrDigit: true, + }, + usernameIsEmail: true, + }; + } + /** * Set user groups * @param {UpdateUserGroupsOnUserRequestModel} data diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/utils/entity/entity-tree.manager.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/utils/entity/entity-tree.manager.ts index 61ebfea7a5bc..4d1fd70e7c38 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/utils/entity/entity-tree.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/utils/entity/entity-tree.manager.ts @@ -12,7 +12,7 @@ export class UmbMockEntityTreeManager item.parent === null); + const items = this.#db.getAll().filter((item) => item.parent === null || item.parent === undefined); return this.#pagedTreeResult({ items, skip, take }); } diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/data-type/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/data-type/detail.handlers.ts index 025e33248c99..75804751b80d 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/data-type/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/data-type/detail.handlers.ts @@ -26,6 +26,10 @@ export const detailHandlers = [ rest.get(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const response = umbDataTypeMockDb.detail.read(id); return res(ctx.status(200), ctx.json(response)); }), @@ -33,6 +37,10 @@ export const detailHandlers = [ rest.put(umbracoPath(`${UMB_SLUG}/:id`), async (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const requestBody = (await req.json()) as UpdateDataTypeRequestModel; if (!requestBody) return res(ctx.status(400, 'no body found')); umbDataTypeMockDb.detail.update(id, requestBody); @@ -42,6 +50,10 @@ export const detailHandlers = [ rest.delete(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } umbDataTypeMockDb.detail.delete(id); return res(ctx.status(200)); }), diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/dictionary/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/dictionary/detail.handlers.ts index fcff5fefd735..55e44dd1fc09 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/dictionary/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/dictionary/detail.handlers.ts @@ -32,6 +32,10 @@ export const detailHandlers = [ rest.get(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const response = umbDictionaryMockDb.detail.read(id); return res(ctx.status(200), ctx.json(response)); }), @@ -39,6 +43,10 @@ export const detailHandlers = [ rest.put(umbracoPath(`${UMB_SLUG}/:id`), async (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const requestBody = (await req.json()) as UpdateDictionaryItemRequestModel; if (!requestBody) return res(ctx.status(400, 'no body found')); umbDictionaryMockDb.detail.update(id, requestBody); @@ -48,6 +56,10 @@ export const detailHandlers = [ rest.delete(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } umbDictionaryMockDb.detail.delete(id); return res(ctx.status(200)); }), diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/dictionary/tree.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/dictionary/tree.handlers.ts index 6d79c28104c5..620e0f272303 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/dictionary/tree.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/dictionary/tree.handlers.ts @@ -19,4 +19,11 @@ export const treeHandlers = [ const response = umbDictionaryMockDb.tree.getChildrenOf({ parentId, skip, take }); return res(ctx.status(200), ctx.json(response)); }), + + rest.get(umbracoPath(`/tree${UMB_SLUG}/ancestors`), (req, res, ctx) => { + const descendantId = req.url.searchParams.get('descendantId'); + if (!descendantId) return; + const response = umbDictionaryMockDb.tree.getAncestorsOf({ descendantId }); + return res(ctx.status(200), ctx.json(response)); + }), ]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/document-blueprint/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/document-blueprint/detail.handlers.ts index 80722ba2a866..f9ed905395d3 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/document-blueprint/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/document-blueprint/detail.handlers.ts @@ -26,6 +26,10 @@ export const detailHandlers = [ rest.get(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const response = umbDocumentBlueprintMockDb.detail.read(id); return res(ctx.status(200), ctx.json(response)); }), @@ -33,6 +37,10 @@ export const detailHandlers = [ rest.put(umbracoPath(`${UMB_SLUG}/:id`), async (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const requestBody = (await req.json()) as UpdateDocumentRequestModel; if (!requestBody) return res(ctx.status(400, 'no body found')); umbDocumentBlueprintMockDb.detail.update(id, requestBody); @@ -42,6 +50,10 @@ export const detailHandlers = [ rest.delete(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } umbDocumentBlueprintMockDb.detail.delete(id); return res(ctx.status(200)); }), diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/document-type/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/document-type/detail.handlers.ts index 2c9237cebd48..6ecfb5ffac85 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/document-type/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/document-type/detail.handlers.ts @@ -1,8 +1,10 @@ const { rest } = window.MockServiceWorker; +import { umbDocumentBlueprintMockDb } from '../../data/document-blueprint/document-blueprint.db.js'; import { umbDocumentTypeMockDb } from '../../data/document-type/document-type.db.js'; import { UMB_SLUG } from './slug.js'; import type { CreateMediaTypeRequestModel, + PagedDocumentTypeBlueprintItemResponseModel, UpdateMediaTypeRequestModel, } from '@umbraco-cms/backoffice/external/backend-api'; import { umbracoPath } from '@umbraco-cms/backoffice/utils'; @@ -26,13 +28,28 @@ export const detailHandlers = [ rest.get(umbracoPath(`${UMB_SLUG}/:id/blueprint`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } - return res(ctx.status(404)); + const relevantBlueprints = umbDocumentBlueprintMockDb + .getAll() + .filter((blueprint) => blueprint.documentType.id === id); + const response: PagedDocumentTypeBlueprintItemResponseModel = { + total: relevantBlueprints.length, + items: relevantBlueprints, + }; + return res(ctx.status(200), ctx.json(response)); }), rest.get(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const response = umbDocumentTypeMockDb.detail.read(id); return res(ctx.status(200), ctx.json(response)); }), @@ -40,6 +57,10 @@ export const detailHandlers = [ rest.put(umbracoPath(`${UMB_SLUG}/:id`), async (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const requestBody = (await req.json()) as UpdateMediaTypeRequestModel; if (!requestBody) return res(ctx.status(400, 'no body found')); umbDocumentTypeMockDb.detail.update(id, requestBody); @@ -49,6 +70,10 @@ export const detailHandlers = [ rest.delete(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } umbDocumentTypeMockDb.detail.delete(id); return res(ctx.status(200)); }), diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/document-type/tree.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/document-type/tree.handlers.ts index 9f8a2297ced8..80a905469de3 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/document-type/tree.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/document-type/tree.handlers.ts @@ -19,4 +19,11 @@ export const treeHandlers = [ const response = umbDocumentTypeMockDb.tree.getChildrenOf({ parentId, skip, take }); return res(ctx.status(200), ctx.json(response)); }), + + rest.get(umbracoPath(`/tree${UMB_SLUG}/ancestors`), (req, res, ctx) => { + const descendantId = req.url.searchParams.get('descendantId'); + if (!descendantId) return; + const response = umbDocumentTypeMockDb.tree.getAncestorsOf({ descendantId }); + return res(ctx.status(200), ctx.json(response)); + }), ]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/document/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/document/detail.handlers.ts index 2831e3c2e0e8..c1a3563664b3 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/document/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/document/detail.handlers.ts @@ -34,6 +34,10 @@ export const detailHandlers = [ rest.get(umbracoPath(`${UMB_SLUG}/:id/referenced-by`), (_req, res, ctx) => { const id = _req.params.id as string; if (!id) return; + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const query = _req.url.searchParams; const skip = query.get('skip') ? parseInt(query.get('skip') as string, 10) : 0; @@ -56,6 +60,10 @@ export const detailHandlers = [ rest.get(umbracoPath(`${UMB_SLUG}/:id/referenced-descendants`), (_req, res, ctx) => { const id = _req.params.id as string; if (!id) return; + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const ReferencedDescendantsResponse: GetDocumentByIdReferencedDescendantsResponse = { total: 0, @@ -68,6 +76,10 @@ export const detailHandlers = [ rest.put(umbracoPath(`${UMB_SLUG}/:id/validate`, 'v1.1'), (_req, res, ctx) => { const id = _req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } return res(ctx.status(200)); }), @@ -75,6 +87,10 @@ export const detailHandlers = [ rest.get(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const response = umbDocumentMockDb.detail.read(id); return res(ctx.status(200), ctx.json(response)); }), @@ -82,6 +98,10 @@ export const detailHandlers = [ rest.put(umbracoPath(`${UMB_SLUG}/:id`), async (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const requestBody = (await req.json()) as UpdateDocumentRequestModel; if (!requestBody) return res(ctx.status(400, 'no body found')); umbDocumentMockDb.detail.update(id, requestBody); @@ -91,6 +111,10 @@ export const detailHandlers = [ rest.delete(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } umbDocumentMockDb.detail.delete(id); return res(ctx.status(200)); }), diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/language/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/language/detail.handlers.ts index 667081e36cb5..bd5d474db46b 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/language/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/language/detail.handlers.ts @@ -37,6 +37,10 @@ export const detailHandlers = [ rest.get(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const response = umbLanguageMockDb.detail.read(id); return res(ctx.status(200), ctx.json(response)); }), @@ -44,6 +48,10 @@ export const detailHandlers = [ rest.put(umbracoPath(`${UMB_SLUG}/:id`), async (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const requestBody = (await req.json()) as UpdateLanguageRequestModel; if (!requestBody) return res(ctx.status(400, 'no body found')); umbLanguageMockDb.detail.update(id, requestBody); @@ -53,6 +61,10 @@ export const detailHandlers = [ rest.delete(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } umbLanguageMockDb.detail.delete(id); return res(ctx.status(200)); }), diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/media-type/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/media-type/detail.handlers.ts index ab4653454912..2e2de688e397 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/media-type/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/media-type/detail.handlers.ts @@ -26,6 +26,10 @@ export const detailHandlers = [ rest.get(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const response = umbMediaTypeMockDb.detail.read(id); return res(ctx.status(200), ctx.json(response)); }), @@ -33,6 +37,10 @@ export const detailHandlers = [ rest.put(umbracoPath(`${UMB_SLUG}/:id`), async (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const requestBody = (await req.json()) as UpdateMediaTypeRequestModel; if (!requestBody) return res(ctx.status(400, 'no body found')); umbMediaTypeMockDb.detail.update(id, requestBody); @@ -42,6 +50,10 @@ export const detailHandlers = [ rest.delete(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } umbMediaTypeMockDb.detail.delete(id); return res(ctx.status(200)); }), diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/detail.handlers.ts index fe52ac9a25fe..20a53376fe0d 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/detail.handlers.ts @@ -29,6 +29,10 @@ export const detailHandlers = [ rest.get(umbracoPath(`${UMB_SLUG}/:id/referenced-by`), (_req, res, ctx) => { const id = _req.params.id as string; if (!id) return; + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const PagedTrackedReference = { total: referenceData.length, @@ -41,6 +45,10 @@ export const detailHandlers = [ rest.get(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const response = umbMediaMockDb.detail.read(id); return res(ctx.status(200), ctx.json(response)); }), @@ -48,6 +56,10 @@ export const detailHandlers = [ rest.put(umbracoPath(`${UMB_SLUG}/:id/validate`), async (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const model = await req.json(); if (!model) return res(ctx.status(400)); @@ -65,6 +77,10 @@ export const detailHandlers = [ rest.put(umbracoPath(`${UMB_SLUG}/:id`), async (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const requestBody = (await req.json()) as UpdateMediaRequestModel; if (!requestBody) return res(ctx.status(400, 'no body found')); umbMediaMockDb.detail.update(id, requestBody); @@ -74,6 +90,10 @@ export const detailHandlers = [ rest.delete(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } umbMediaMockDb.detail.delete(id); return res(ctx.status(200)); }), diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/member-group/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/member-group/detail.handlers.ts index f59ed732e39f..8bf3d1e4f257 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/member-group/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/member-group/detail.handlers.ts @@ -19,9 +19,24 @@ export const detailHandlers = [ ); }), + rest.get(umbracoPath(`${UMB_SLUG}`), (req, res, ctx) => { + const skipParam = req.url.searchParams.get('skip'); + const skip = skipParam ? Number.parseInt(skipParam) : undefined; + const takeParam = req.url.searchParams.get('take'); + const take = takeParam ? Number.parseInt(takeParam) : undefined; + + const response = umbMemberGroupMockDb.get({ skip, take }); + + return res(ctx.status(200), ctx.json(response)); + }), + rest.get(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const response = umbMemberGroupMockDb.detail.read(id); return res(ctx.status(200), ctx.json(response)); }), @@ -29,6 +44,10 @@ export const detailHandlers = [ rest.put(umbracoPath(`${UMB_SLUG}/:id`), async (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const requestBody = (await req.json()) as any; if (!requestBody) return res(ctx.status(400, 'no body found')); umbMemberGroupMockDb.detail.update(id, requestBody); @@ -38,6 +57,10 @@ export const detailHandlers = [ rest.delete(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } umbMemberGroupMockDb.detail.delete(id); return res(ctx.status(200)); }), diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/member-type/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/member-type/detail.handlers.ts index 01a23c551940..4e1978f99aa8 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/member-type/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/member-type/detail.handlers.ts @@ -26,6 +26,10 @@ export const detailHandlers = [ rest.get(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const response = umbMemberTypeMockDb.detail.read(id); return res(ctx.status(200), ctx.json(response)); }), @@ -33,6 +37,10 @@ export const detailHandlers = [ rest.put(umbracoPath(`${UMB_SLUG}/:id`), async (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const requestBody = (await req.json()) as UpdateMemberTypeRequestModel; if (!requestBody) return res(ctx.status(400, 'no body found')); umbMemberTypeMockDb.detail.update(id, requestBody); @@ -42,6 +50,10 @@ export const detailHandlers = [ rest.delete(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } umbMemberTypeMockDb.detail.delete(id); return res(ctx.status(200)); }), diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/member-type/tree.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/member-type/tree.handlers.ts index 0aef1a0996f6..ab08a7bde265 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/member-type/tree.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/member-type/tree.handlers.ts @@ -5,8 +5,8 @@ import { umbracoPath } from '@umbraco-cms/backoffice/utils'; export const treeHandlers = [ rest.get(umbracoPath(`/tree${UMB_SLUG}/root`), (req, res, ctx) => { - const skip = Number(req.url.searchParams.get('skip')); - const take = Number(req.url.searchParams.get('take')); + const skip = Number(req.url.searchParams.get('skip') ?? '0'); + const take = Number(req.url.searchParams.get('take') ?? '100'); const response = umbMemberTypeMockDb.tree.getRoot({ skip, take }); return res(ctx.status(200), ctx.json(response)); }), @@ -14,8 +14,8 @@ export const treeHandlers = [ rest.get(umbracoPath(`/tree${UMB_SLUG}/children`), (req, res, ctx) => { const parentId = req.url.searchParams.get('parentId'); if (!parentId) return; - const skip = Number(req.url.searchParams.get('skip')); - const take = Number(req.url.searchParams.get('take')); + const skip = Number(req.url.searchParams.get('skip') ?? '0'); + const take = Number(req.url.searchParams.get('take') ?? '100'); const response = umbMemberTypeMockDb.tree.getChildrenOf({ parentId, skip, take }); return res(ctx.status(200), ctx.json(response)); }), diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/member/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/member/detail.handlers.ts index 1321d13d38cf..c25a194ad1ce 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/member/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/member/detail.handlers.ts @@ -23,6 +23,10 @@ export const detailHandlers = [ rest.get(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const response = umbMemberMockDb.detail.read(id); return res(ctx.status(200), ctx.json(response)); }), @@ -30,6 +34,10 @@ export const detailHandlers = [ rest.put(umbracoPath(`${UMB_SLUG}/:id`), async (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const requestBody = (await req.json()) as UpdateMemberRequestModel; if (!requestBody) return res(ctx.status(400, 'no body found')); umbMemberMockDb.detail.update(id, requestBody); @@ -39,6 +47,10 @@ export const detailHandlers = [ rest.delete(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } umbMemberMockDb.detail.delete(id); return res(ctx.status(200)); }), diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/member/filter.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/member/filter.handlers.ts new file mode 100644 index 000000000000..3047ca203fb4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/member/filter.handlers.ts @@ -0,0 +1,29 @@ +const { rest } = window.MockServiceWorker; +import { umbMemberMockDb } from '../../data/member/member.db.js'; +import { UMB_SLUG } from './slug.js'; +import { umbracoPath } from '@umbraco-cms/backoffice/utils'; + +export const handlers = [ + rest.get(umbracoPath(`/filter${UMB_SLUG}`), (req, res, ctx) => { + const skip = Number(req.url.searchParams.get('skip')); + const take = Number(req.url.searchParams.get('take')); + const orderBy = req.url.searchParams.get('orderBy'); + const orderDirection = req.url.searchParams.get('orderDirection'); + const memberGroupIds = req.url.searchParams.getAll('memberGroupIds'); + const memberTypeId = req.url.searchParams.get('memberTypeId'); + const filter = req.url.searchParams.get('filter'); + + const options: any = { + skip: skip || undefined, + take: take || undefined, + orderBy: orderBy || undefined, + orderDirection: orderDirection || undefined, + memberGroupIds: memberGroupIds.length > 0 ? memberGroupIds : undefined, + memberTypeId: memberTypeId || undefined, + filter: filter || undefined, + }; + + const response = umbMemberMockDb.filter(options); + return res(ctx.status(200), ctx.json(response)); + }), +]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/member/index.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/member/index.ts index 1326eb078e71..032773439b31 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/member/index.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/member/index.ts @@ -1,5 +1,6 @@ import { detailHandlers } from './detail.handlers.js'; import { itemHandlers } from './item.handlers.js'; import { collectionHandlers } from './collection.handlers.js'; +import { handlers as filterHandlers } from './filter.handlers.js'; -export const handlers = [...itemHandlers, ...collectionHandlers, ...detailHandlers]; +export const handlers = [...itemHandlers, ...collectionHandlers, ...detailHandlers, ...filterHandlers]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/partial-view/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/partial-view/detail.handlers.ts index 52fe83cd43ae..4d06d535b56d 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/partial-view/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/partial-view/detail.handlers.ts @@ -35,6 +35,10 @@ export const detailHandlers = [ rest.get(umbracoPath(`${UMB_SLUG}/:path`), (req, res, ctx) => { const path = req.params.path as string; if (!path) return res(ctx.status(400)); + if (path.endsWith('forbidden')) { + // Simulate a forbidden response + return res(ctx.status(403)); + } const response = umbPartialViewMockDB.file.read(decodeURIComponent(path)); return res(ctx.status(200), ctx.json(response)); }), @@ -42,6 +46,10 @@ export const detailHandlers = [ rest.delete(umbracoPath(`${UMB_SLUG}/:path`), (req, res, ctx) => { const path = req.params.path as string; if (!path) return res(ctx.status(400)); + if (path.endsWith('forbidden')) { + // Simulate a forbidden response + return res(ctx.status(403)); + } umbPartialViewMockDB.file.delete(decodeURIComponent(path)); return res(ctx.status(200)); }), @@ -49,6 +57,10 @@ export const detailHandlers = [ rest.put(umbracoPath(`${UMB_SLUG}/:path`), async (req, res, ctx) => { const path = req.params.path as string; if (!path) return res(ctx.status(400)); + if (path.endsWith('forbidden')) { + // Simulate a forbidden response + return res(ctx.status(403)); + } const requestBody = (await req.json()) as UpdatePartialViewRequestModel; if (!requestBody) return res(ctx.status(400, 'no body found')); umbPartialViewMockDB.file.update(decodeURIComponent(path), requestBody); diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/relation-type/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/relation-type/detail.handlers.ts index 118d391ed065..279b973f5807 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/relation-type/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/relation-type/detail.handlers.ts @@ -22,6 +22,10 @@ export const detailHandlers = [ rest.get(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const response = umbRelationTypeMockDb.detail.read(id); return res(ctx.status(200), ctx.json(response)); }), diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/script/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/script/detail.handlers.ts index 03e6ab6c0e95..f85e6c08000c 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/script/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/script/detail.handlers.ts @@ -32,6 +32,10 @@ export const detailHandlers = [ rest.get(umbracoPath(`${UMB_SLUG}/:path`), (req, res, ctx) => { const path = req.params.path as string; if (!path) return res(ctx.status(400)); + if (path.endsWith('forbidden')) { + // Simulate a forbidden response + return res(ctx.status(403)); + } const response = umbScriptMockDb.file.read(decodeURIComponent(path)); return res(ctx.status(200), ctx.json(response)); }), @@ -39,6 +43,10 @@ export const detailHandlers = [ rest.delete(umbracoPath(`${UMB_SLUG}/:path`), (req, res, ctx) => { const path = req.params.path as string; if (!path) return res(ctx.status(400)); + if (path.endsWith('forbidden')) { + // Simulate a forbidden response + return res(ctx.status(403)); + } umbScriptMockDb.file.delete(decodeURIComponent(path)); return res(ctx.status(200)); }), @@ -46,6 +54,10 @@ export const detailHandlers = [ rest.put(umbracoPath(`${UMB_SLUG}/:path`), async (req, res, ctx) => { const path = req.params.path as string; if (!path) return res(ctx.status(400)); + if (path.endsWith('forbidden')) { + // Simulate a forbidden response + return res(ctx.status(403)); + } const requestBody = (await req.json()) as UpdateScriptRequestModel; if (!requestBody) return res(ctx.status(400, 'no body found')); umbScriptMockDb.file.update(decodeURIComponent(path), requestBody); diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/segment.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/segment.handlers.ts new file mode 100644 index 000000000000..2eeca2a961df --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/segment.handlers.ts @@ -0,0 +1,17 @@ +const { rest } = window.MockServiceWorker; + +import { umbracoPath } from '@umbraco-cms/backoffice/utils'; +import type { PagedSegmentResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; + +export const handlers = [ + rest.get(umbracoPath('/segment'), (_req, res, ctx) => { + return res( + // Respond with a 200 status code + ctx.status(200), + ctx.json({ + total: 0, + items: [], + }), + ); + }), +]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/stylesheet/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/stylesheet/detail.handlers.ts index 4d59dffd1b9a..efcb3d5a28c1 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/stylesheet/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/stylesheet/detail.handlers.ts @@ -35,6 +35,10 @@ export const detailHandlers = [ rest.get(umbracoPath(`${UMB_SLUG}/:path`), (req, res, ctx) => { const path = req.params.path as string; if (!path) return res(ctx.status(400)); + if (path.endsWith('forbidden')) { + // Simulate a forbidden response + return res(ctx.status(403)); + } const response = umbStylesheetMockDb.file.read(decodeURIComponent(path)); return res(ctx.status(200), ctx.json(response)); }), @@ -42,6 +46,10 @@ export const detailHandlers = [ rest.delete(umbracoPath(`${UMB_SLUG}/:path`), (req, res, ctx) => { const path = req.params.path as string; if (!path) return res(ctx.status(400)); + if (path.endsWith('forbidden')) { + // Simulate a forbidden response + return res(ctx.status(403)); + } umbStylesheetMockDb.file.delete(decodeURIComponent(path)); return res(ctx.status(200)); }), @@ -49,6 +57,10 @@ export const detailHandlers = [ rest.put(umbracoPath(`${UMB_SLUG}/:path`), async (req, res, ctx) => { const path = req.params.path as string; if (!path) return res(ctx.status(400)); + if (path.endsWith('forbidden')) { + // Simulate a forbidden response + return res(ctx.status(403)); + } const requestBody = (await req.json()) as UpdateStylesheetRequestModel; if (!requestBody) return res(ctx.status(400, 'no body found')); umbStylesheetMockDb.file.update(decodeURIComponent(path), requestBody); diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/template/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/template/detail.handlers.ts index 732271025f7e..134b4fbf9924 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/template/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/template/detail.handlers.ts @@ -40,6 +40,10 @@ export const detailHandlers = [ rest.get(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const response = umbTemplateMockDb.detail.read(id); return res(ctx.status(200), ctx.json(response)); }), @@ -47,6 +51,10 @@ export const detailHandlers = [ rest.put(umbracoPath(`${UMB_SLUG}/:id`), async (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const requestBody = (await req.json()) as UpdateTemplateRequestModel; if (!requestBody) return res(ctx.status(400, 'no body found')); @@ -65,6 +73,10 @@ export const detailHandlers = [ rest.delete(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } umbTemplateMockDb.detail.delete(id); return res(ctx.status(200)); }), diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/user-group/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/user-group/detail.handlers.ts index 181fd2cbfc01..64e2db87a5c0 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/user-group/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/user-group/detail.handlers.ts @@ -36,6 +36,10 @@ export const detailHandlers = [ rest.get(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const response = umbUserGroupMockDb.detail.read(id); return res(ctx.status(200), ctx.json(response)); }), @@ -43,6 +47,10 @@ export const detailHandlers = [ rest.put(umbracoPath(`${UMB_SLUG}/:id`), async (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const requestBody = (await req.json()) as UpdateUserGroupRequestModel; if (!requestBody) return res(ctx.status(400, 'no body found')); umbUserGroupMockDb.detail.update(id, requestBody); @@ -52,6 +60,10 @@ export const detailHandlers = [ rest.delete(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } umbUserGroupMockDb.detail.delete(id); return res(ctx.status(200)); }), diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/detail.handlers.ts index 675f37a0b3b7..c9f4bfa4a6ad 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/detail.handlers.ts @@ -20,9 +20,37 @@ export const detailHandlers = [ ); }), + rest.get(umbracoPath(`${UMB_SLUG}/configuration`), (_req, res, ctx) => { + return res(ctx.status(200), ctx.json(umbUserMockDb.getConfiguration())); + }), + + rest.get(umbracoPath(`${UMB_SLUG}/:id/calculate-start-nodes`), (req, res, ctx) => { + const id = req.params.id as string; + if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } + return res(ctx.status(200), ctx.json(umbUserMockDb.calculateStartNodes(id))); + }), + + rest.get(umbracoPath(`${UMB_SLUG}/:id/client-credentials`), (req, res, ctx) => { + const id = req.params.id as string; + if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } + return res(ctx.status(200), ctx.json(umbUserMockDb.clientCredentials(id))); + }), + rest.get(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const response = umbUserMockDb.detail.read(id); return res(ctx.status(200), ctx.json(response)); }), @@ -30,6 +58,10 @@ export const detailHandlers = [ rest.put(umbracoPath(`${UMB_SLUG}/:id`), async (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } const requestBody = (await req.json()) as UpdateUserRequestModel; if (!requestBody) return res(ctx.status(400, 'no body found')); umbUserMockDb.detail.update(id, requestBody); @@ -39,6 +71,10 @@ export const detailHandlers = [ rest.delete(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); + if (id === 'forbidden') { + // Simulate a forbidden response + return res(ctx.status(403)); + } umbUserMockDb.detail.delete(id); return res(ctx.status(200)); }), diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/try-execute.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/try-execute.controller.ts index 2c5a356fe37b..0ccf210fdca8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/try-execute.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/try-execute.controller.ts @@ -49,15 +49,9 @@ export class UmbTryExecuteController extends UmbResourceController { // Check if we can extract problem details from the error if (apiError.problemDetails) { - if (apiError.problemDetails.status === 401) { - // Unauthorized error, show no notification - // the user will see a login screen instead - return; - } - - if (apiError.problemDetails.status === 404) { - // Not found error, show no notification - // the user will see a 404 page instead, or otherwise the UI will handle it + if ([400, 401, 403, 404].includes(apiError.problemDetails.status)) { + // Non-fatal errors that the UI can handle gracefully + // so we avoid showing a notification return; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/router/route/forbidden/route-forbidden.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/router/route/forbidden/route-forbidden.element.ts new file mode 100644 index 000000000000..bd1c461f25d0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/router/route/forbidden/route-forbidden.element.ts @@ -0,0 +1,59 @@ +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +/** + * A component that displays a "Forbidden" message when a user tries to access a route they do not have permission for. + * This is typically used in routing scenarios where access control is enforced. + * It informs the user that they do not have the necessary permissions to view the requested resource. + * @element umb-route-forbidden + */ +@customElement('umb-route-forbidden') +export class UmbRouteForbiddenElement extends UmbLitElement { + override render() { + return html` +
+

Access denied

+ + You do not have permission to access this resource. Please contact your administrator for assistance. + +
+ `; + } + + static override styles = [ + UmbTextStyles, + css` + :host { + display: block; + width: 100%; + height: 100%; + min-width: 0; + } + + :host > div { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100%; + opacity: 0; + animation: fadeIn 2s 2s forwards; + } + + @keyframes fadeIn { + 100% { + opacity: 100%; + } + } + `, + ]; +} + +export default UmbRouteForbiddenElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-route-forbidden': UmbRouteForbiddenElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/router/route/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/router/route/index.ts index 1c30607927f8..4dd43b97488d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/router/route/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/router/route/index.ts @@ -1,3 +1,4 @@ +export * from './forbidden/route-forbidden.element.js'; export * from './not-found/route-not-found.element.js'; export * from './route.context.js'; export * from './router-slot-change.event.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace-base.ts index a6a78e8f4abd..30fbf2019ecc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace-base.ts @@ -22,8 +22,10 @@ import type { import { UmbDeprecation, UmbStateManager } from '@umbraco-cms/backoffice/utils'; import { UmbValidationContext } from '@umbraco-cms/backoffice/validation'; import { UmbId } from '@umbraco-cms/backoffice/id'; +import { UmbApiError } from '@umbraco-cms/backoffice/resources'; const LOADING_STATE_UNIQUE = 'umbLoadingEntityDetail'; +const FORBIDDEN_STATE_UNIQUE = 'umbForbiddenEntityDetail'; export abstract class UmbEntityDetailWorkspaceContextBase< DetailModelType extends UmbEntityModel = UmbEntityModel, @@ -51,6 +53,7 @@ export abstract class UmbEntityDetailWorkspaceContextBase< public readonly data = this._data.current; public readonly persistedData = this._data.persisted; public readonly loading = new UmbStateManager(this); + public readonly forbidden = new UmbStateManager(this); protected _getDataPromise?: Promise< UmbRepositoryResponse | UmbRepositoryResponseWithAsObservable @@ -234,18 +237,21 @@ export abstract class UmbEntityDetailWorkspaceContextBase< this.loading.addState({ unique: LOADING_STATE_UNIQUE, message: `Loading ${this.getEntityType()} Details` }); await this.#init; this._getDataPromise = this._detailRepository!.requestByUnique(unique); - const response = await this._getDataPromise; - const data = response.data; - - if (data) { + const response = (await this._getDataPromise) as UmbRepositoryResponseWithAsObservable; + const { data, error, asObservable } = response; + + if (error) { + this.removeUmbControllerByAlias('umbEntityDetailTypeStoreObserver'); + if (UmbApiError.isUmbApiError(error)) { + if (error.status === 401 || error.status === 403) { + this.forbidden.addState({ unique: FORBIDDEN_STATE_UNIQUE, message: error.message }); + } + } + } else if (data) { this._data.setPersisted(data); this._data.setCurrent(data); - this.observe( - (response as UmbRepositoryResponseWithAsObservable).asObservable?.(), - (entity) => this.#onDetailStoreChange(entity), - 'umbEntityDetailTypeStoreObserver', - ); + this.observe(asObservable?.(), (entity) => this.#onDetailStoreChange(entity), 'umbEntityDetailTypeStoreObserver'); } this.loading.removeState(LOADING_STATE_UNIQUE); @@ -461,6 +467,7 @@ export abstract class UmbEntityDetailWorkspaceContextBase< override resetState() { super.resetState(); this.loading.clear(); + this.forbidden.clear(); this._data.clear(); this.#allowNavigateAway = false; this._getDataPromise = undefined; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/global-components/entity-detail-forbidden.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/global-components/entity-detail-forbidden.element.ts new file mode 100644 index 000000000000..841fd6b22dfa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/global-components/entity-detail-forbidden.element.ts @@ -0,0 +1,50 @@ +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +@customElement('umb-entity-detail-forbidden') +export class UmbEntityDetailForbiddenElement extends UmbLitElement { + @property({ type: String, attribute: 'entity-type' }) + entityType = ''; + + override render() { + return html` +
+

${this.localize.term('entityDetail_forbiddenTitle', this.entityType)}

+ ${this.localize.term('entityDetail_forbiddenDescription', this.entityType)} +
+ `; + } + + static override styles = [ + UmbTextStyles, + css` + :host { + display: block; + width: 100%; + height: 100%; + min-width: 0; + } + + :host > div { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100%; + } + + @keyframes fadeIn { + 100% { + opacity: 100%; + } + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-entity-detail-forbidden': UmbEntityDetailForbiddenElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/global-components/entity-detail-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/global-components/entity-detail-workspace-editor.element.ts index e98b9f46b4d7..482bfd8e581a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/global-components/entity-detail-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/global-components/entity-detail-workspace-editor.element.ts @@ -13,6 +13,9 @@ export class UmbEntityDetailWorkspaceEditorElement extends UmbLitElement { @state() private _isLoading = false; + @state() + private _isForbidden = false; + @state() private _exists = false; @@ -28,15 +31,30 @@ export class UmbEntityDetailWorkspaceEditorElement extends UmbLitElement { this.#context = context; this.observe(this.#context?.entityType, (entityType) => (this._entityType = entityType)); this.observe(this.#context?.loading.isOn, (isLoading) => (this._isLoading = isLoading ?? false)); + this.observe(this.#context?.forbidden.isOn, (isForbidden) => (this._isForbidden = isForbidden ?? false)); this.observe(this.#context?.data, (data) => (this._exists = !!data)); this.observe(this.#context?.isNew, (isNew) => (this._isNew = isNew)); }); } + #renderForbidden() { + if (!this._isLoading && this._isForbidden) { + return html``; + } + return nothing; + } + + #renderNotFound() { + if (!this._isLoading && !this._exists) { + return html``; + } + return nothing; + } + protected override render() { - return html` ${!this._exists && !this._isLoading - ? html`` - : nothing} + return html` ${this.#renderForbidden()} ${this.#renderNotFound()}