diff --git a/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs index 0b7867a812..99206da2ac 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs @@ -216,7 +216,8 @@ public class UserPresentationFactory : IUserPresentationFactory HasAccessToAllLanguages = hasAccessToAllLanguages, HasAccessToSensitiveData = user.HasAccessToSensitiveData(), AllowedSections = allowedSections, - IsAdmin = user.IsAdmin() + IsAdmin = user.IsAdmin(), + UserGroupIds = presentationUser.UserGroupIds, }); } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/CurrentUserResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/CurrentUserResponseModel.cs index fbf8dde97c..332e1768d5 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/CurrentUserResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/CurrentUserResponseModel.cs @@ -2,16 +2,10 @@ using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions; namespace Umbraco.Cms.Api.Management.ViewModels.User.Current; -public class CurrentUserResponseModel +public class CurrentUserResponseModel : UserPresentationBase { public required Guid Id { get; init; } - public required string Email { get; init; } = string.Empty; - - public required string UserName { get; init; } = string.Empty; - - public required string Name { get; init; } = string.Empty; - public required string? LanguageIsoCode { get; init; } public required ISet DocumentStartNodeIds { get; init; } = new HashSet(); @@ -35,5 +29,6 @@ public class CurrentUserResponseModel public required ISet Permissions { get; init; } public required ISet AllowedSections { get; init; } + public bool IsAdmin { get; set; } } diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts index 59bd48e112..2f2dfa87e7 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts @@ -443,10 +443,11 @@ export type CultureReponseModel = { }; export type CurrentUserResponseModel = { - id: string; email: string; userName: string; name: string; + userGroupIds: Array<(ReferenceByIdModel)>; + id: string; languageIsoCode: (string) | null; documentStartNodeIds: Array<(ReferenceByIdModel)>; hasDocumentRootAccess: boolean; @@ -867,6 +868,8 @@ export type DocumentVariantResponseModel = { updateDate: string; state: DocumentVariantStateModel; publishDate?: (string) | null; + scheduledPublishDate?: (string) | null; + scheduledUnpublishDate?: (string) | null; }; export enum DocumentVariantStateModel { 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 2416aeb899..231cb07ac7 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 @@ -86,6 +86,7 @@ class UmbUserMockDB extends UmbEntityMockDbBase { permissions, allowedSections, isAdmin: firstUser.isAdmin, + userGroupIds: firstUser.userGroupIds, }; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/string/string-or-string-array-contains/string-or-string-array-contains.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/string/string-or-string-array-contains/string-or-string-array-contains.function.ts index cc61e13bf9..612a3f93b0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/string/string-or-string-array-contains/string-or-string-array-contains.function.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/string/string-or-string-array-contains/string-or-string-array-contains.function.ts @@ -7,3 +7,17 @@ export function stringOrStringArrayContains(value: string | Array, search: string): boolean { return Array.isArray(value) ? value.indexOf(search) !== -1 : value === search; } + +/** + * Check if a string or array of strings intersects with another array of strings + * @param value The string or array of strings to search in + * @param search The array of strings to search for + * @returns {boolean} Whether the string or array of strings intersects with the search array + */ +export function stringOrStringArrayIntersects(value: string | Array, search: Array): boolean { + if (Array.isArray(value)) { + return value.some((v) => search.indexOf(v) !== -1); + } else { + return search.indexOf(value) !== -1; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/constants.ts index 8cef9ed55b..88a4f1bcbd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/constants.ts @@ -1 +1,2 @@ +export * from './group-id/constants.js'; export * from './is-admin/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/group-id/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/group-id/constants.ts new file mode 100644 index 0000000000..74ba339c6a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/group-id/constants.ts @@ -0,0 +1 @@ +export const UMB_CURRENT_USER_GROUP_ID_CONDITION_ALIAS = 'Umb.Condition.CurrentUser.GroupId'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/group-id/group-id.condition-config.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/group-id/group-id.condition-config.ts new file mode 100644 index 0000000000..18c646f1cf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/group-id/group-id.condition-config.ts @@ -0,0 +1,35 @@ +import type { UMB_CURRENT_USER_GROUP_ID_CONDITION_ALIAS } from './constants.js'; +import type { UmbConditionConfigBase } from '@umbraco-cms/backoffice/extension-api'; + +export interface UmbCurrentUserGroupIdConditionConfig + extends UmbConditionConfigBase { + /** + * The user group that the current user must be a member of to pass the condition. + * @examples ['guid1'] + */ + match?: string; + + /** + * The user group(s) that the current user must be a member of to pass the condition. + * @examples [['guid1', 'guid2']] + */ + oneOf?: Array; + + /** + * The user groups that the current user must be a member of to pass the condition. + * @examples [['guid1', 'guid2']] + */ + allOf?: Array; + + /** + * The user group(s) that the current user must not be a member of to pass the condition. + * @examples [['guid1', 'guid2']] + */ + noneOf?: Array; +} + +declare global { + interface UmbExtensionConditionConfigMap { + UmbCurrentUserGroupIdConditionConfig: UmbCurrentUserGroupIdConditionConfig; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/group-id/group-id.condition.manifest.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/group-id/group-id.condition.manifest.ts new file mode 100644 index 0000000000..9755685912 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/group-id/group-id.condition.manifest.ts @@ -0,0 +1,9 @@ +import { UMB_CURRENT_USER_GROUP_ID_CONDITION_ALIAS } from './constants.js'; +import type { ManifestCondition } from '@umbraco-cms/backoffice/extension-api'; + +export const manifest: ManifestCondition = { + type: 'condition', + name: 'Current user group id Condition', + alias: UMB_CURRENT_USER_GROUP_ID_CONDITION_ALIAS, + api: () => import('./group-id.condition.js'), +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/group-id/group-id.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/group-id/group-id.condition.ts new file mode 100644 index 0000000000..6dff9d7446 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/group-id/group-id.condition.ts @@ -0,0 +1,51 @@ +import { UMB_CURRENT_USER_CONTEXT } from '../../current-user.context.token.js'; +import type { UmbCurrentUserModel } from '../../types.js'; +import type { UmbCurrentUserGroupIdConditionConfig } from './types.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbConditionControllerArguments, UmbExtensionCondition } from '@umbraco-cms/backoffice/extension-api'; +import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry'; + +export class UmbCurrentUserGroupCondition + extends UmbConditionBase + implements UmbExtensionCondition +{ + constructor(host: UmbControllerHost, args: UmbConditionControllerArguments) { + super(host, args); + + this.consumeContext(UMB_CURRENT_USER_CONTEXT, (context) => { + this.observe(context.currentUser, this.observeCurrentUser, 'umbCurrentUserGroupConditionObserver'); + }); + } + + private observeCurrentUser = async (currentUser: UmbCurrentUserModel) => { + // Idea: This part could be refactored to become a shared util, to align these matching feature across conditions. [NL] + // Notice doing so it would be interesting to invistigate if it makes sense to combine some of these properties, to enable more specific matching. (But maybe it is only relevant for the combination of match + oneOf) [NL] + const { match, oneOf, allOf, noneOf } = this.config; + + if (match) { + this.permitted = currentUser.userGroupUniques.includes(match); + return; + } + + if (oneOf) { + this.permitted = oneOf.some((v) => currentUser.userGroupUniques.includes(v)); + return; + } + + if (allOf) { + this.permitted = allOf.every((v) => currentUser.userGroupUniques.includes(v)); + return; + } + + if (noneOf) { + if (noneOf.some((v) => currentUser.userGroupUniques.includes(v))) { + this.permitted = false; + return; + } + } + + this.permitted = true; + }; +} + +export { UmbCurrentUserGroupCondition as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/group-id/types.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/group-id/types.ts new file mode 100644 index 0000000000..8c14e66d97 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/group-id/types.ts @@ -0,0 +1 @@ +export type * from './group-id.condition-config.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/manifests.ts index 63918c839e..6a16e25d4e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/manifests.ts @@ -1,3 +1,4 @@ +import { manifest as groupManifest } from './group-id/group-id.condition.manifest.js'; import { manifest as isAdminManifests } from './is-admin/is-admin.condition.manifest.js'; -export const manifests = [isAdminManifests]; +export const manifests = [groupManifest, isAdminManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/types.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/types.ts index fbd158e6d0..f49d805a7a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/conditions/types.ts @@ -1 +1,2 @@ +export type * from './group-id/types.js'; export type * from './is-admin/types.js'; 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 436960989c..55a12a7824 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 @@ -54,6 +54,7 @@ export class UmbCurrentUserServerDataSource { permissions: data.permissions, unique: data.id, userName: data.userName, + userGroupUniques: data.userGroupIds.map((group) => group.id), }; return { data: user }; } 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 33286f098b..e199d68780 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 @@ -30,6 +30,7 @@ export interface UmbCurrentUserModel { permissions: Array; unique: string; userName: string; + userGroupUniques: string[]; } export type UmbCurrentUserExternalLoginProviderModel = UserExternalLoginProviderModel; diff --git a/src/Umbraco.Web.UI.Client/utils/all-umb-consts/index.ts b/src/Umbraco.Web.UI.Client/utils/all-umb-consts/index.ts index e8d76f2d65..35157ef14c 100644 --- a/src/Umbraco.Web.UI.Client/utils/all-umb-consts/index.ts +++ b/src/Umbraco.Web.UI.Client/utils/all-umb-consts/index.ts @@ -112,7 +112,7 @@ export const foundConsts = [{ }, { path: '@umbraco-cms/backoffice/current-user', - consts: ["UMB_CURRENT_USER_IS_ADMIN_CONDITION_ALIAS","UMB_CURRENT_USER_CONTEXT","UMB_CURRENT_USER_EXTERNAL_LOGIN_MODAL","UMB_CURRENT_USER_HISTORY_STORE_CONTEXT","UMB_CURRENT_USER_MODAL","UMB_CURRENT_USER_MFA_MODAL","UMB_CURRENT_USER_MFA_DISABLE_PROVIDER_MODAL","UMB_CURRENT_USER_MFA_ENABLE_PROVIDER_MODAL","UMB_CURRENT_USER_REPOSITORY_ALIAS","UMB_CURRENT_USER_STORE_CONTEXT"] + consts: ["UMB_CURRENT_USER_GROUP_ID_CONDITION_ALIAS","UMB_CURRENT_USER_IS_ADMIN_CONDITION_ALIAS","UMB_CURRENT_USER_CONTEXT","UMB_CURRENT_USER_EXTERNAL_LOGIN_MODAL","UMB_CURRENT_USER_HISTORY_STORE_CONTEXT","UMB_CURRENT_USER_MODAL","UMB_CURRENT_USER_MFA_MODAL","UMB_CURRENT_USER_MFA_DISABLE_PROVIDER_MODAL","UMB_CURRENT_USER_MFA_ENABLE_PROVIDER_MODAL","UMB_CURRENT_USER_REPOSITORY_ALIAS","UMB_CURRENT_USER_STORE_CONTEXT"] }, { path: '@umbraco-cms/backoffice/dashboard',