diff --git a/.gitignore b/.gitignore index 6a0c21d66f..f43d093009 100644 --- a/.gitignore +++ b/.gitignore @@ -68,7 +68,11 @@ preserve.belle /build/docs.zip /build/ui-docs.zip /build/csharp-docs.zip -/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/ +/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/backoffice +/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets +/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/js +/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/lib +/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/views # Environment specific data /src/Umbraco.Web.UI.Client/[Bb]uild/ diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index ff52d3c2aa..deccda508c 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -39,6 +39,7 @@ stages: ## Build ############################################### - stage: Build + variables: npm_config_cache: $(Pipeline.Workspace)/.npm_client jobs: @@ -47,6 +48,8 @@ stages: pool: vmImage: 'ubuntu-latest' steps: + - checkout: self + submodules: true - task: NodeTool@0 displayName: Use Node.js $(nodeVersion) inputs: @@ -110,7 +113,7 @@ stages: } } - foreach($csproj in Get-ChildItem –Path "src/" -Recurse -Filter *.csproj) + foreach($csproj in Get-ChildItem -Recurse -Filter *.csproj) { dotnet pack $csproj --configuration $(buildConfiguration) --no-build --output $(Build.ArtifactStagingDirectory)/nupkg } diff --git a/src/Umbraco.Cms.Api.Common/Builders/ProblemDetailsBuilder.cs b/src/Umbraco.Cms.Api.Common/Builders/ProblemDetailsBuilder.cs index bc49851911..d3897d5377 100644 --- a/src/Umbraco.Cms.Api.Common/Builders/ProblemDetailsBuilder.cs +++ b/src/Umbraco.Cms.Api.Common/Builders/ProblemDetailsBuilder.cs @@ -7,7 +7,6 @@ public class ProblemDetailsBuilder { private string? _title; private string? _detail; - private int _status = StatusCodes.Status400BadRequest; private string? _type; public ProblemDetailsBuilder WithTitle(string title) @@ -22,12 +21,6 @@ public class ProblemDetailsBuilder return this; } - public ProblemDetailsBuilder WithStatus(int status) - { - _status = status; - return this; - } - public ProblemDetailsBuilder WithType(string type) { _type = type; @@ -39,7 +32,6 @@ public class ProblemDetailsBuilder { Title = _title, Detail = _detail, - Status = _status, Type = _type ?? "Error", }; } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/UserGroups/ByKeyUserGroupController.cs b/src/Umbraco.Cms.Api.Management/Controllers/UserGroups/ByKeyUserGroupController.cs new file mode 100644 index 0000000000..c335926e74 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/UserGroups/ByKeyUserGroupController.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.UserGroups; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.UserGroups; + + +public class ByKeyUserGroupController : UserGroupsControllerBase +{ + private readonly IUserGroupService _userGroupService; + private readonly IUserGroupViewModelFactory _userGroupViewModelFactory; + + public ByKeyUserGroupController( + IUserGroupService userGroupService, + IUserGroupViewModelFactory userGroupViewModelFactory) + { + _userGroupService = userGroupService; + _userGroupViewModelFactory = userGroupViewModelFactory; + } + + [HttpGet("{key:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(UserGroupViewModel), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> ByKey(Guid key) + { + IUserGroup? userGroup = await _userGroupService.GetAsync(key); + + if (userGroup is null) + { + return NotFound(); + } + + return await _userGroupViewModelFactory.CreateAsync(userGroup); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/UserGroups/CreateUserGroupController.cs b/src/Umbraco.Cms.Api.Management/Controllers/UserGroups/CreateUserGroupController.cs new file mode 100644 index 0000000000..755cb1dd4b --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/UserGroups/CreateUserGroupController.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.UserGroups; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.UserGroups; + +public class CreateUserGroupController : UserGroupsControllerBase +{ + private readonly IUserGroupService _userGroupService; + private readonly IUserGroupViewModelFactory _userGroupViewModelFactory; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public CreateUserGroupController( + IUserGroupService userGroupService, + IUserGroupViewModelFactory userGroupViewModelFactory, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _userGroupService = userGroupService; + _userGroupViewModelFactory = userGroupViewModelFactory; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + [HttpPost] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task Create(UserGroupSaveModel userGroupSaveModel) + { + // FIXME: Comment this in when auth is in place and we can get a currently logged in user. + // IUser? currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser; + // if (currentUser is null) + // { + // return UserGroupOperationStatusResult(UserGroupOperationStatus.MissingUser); + // } + + Attempt userGroupCreationAttempt = await _userGroupViewModelFactory.CreateAsync(userGroupSaveModel); + if (userGroupCreationAttempt.Success is false) + { + return UserGroupOperationStatusResult(userGroupCreationAttempt.Status); + } + + IUserGroup group = userGroupCreationAttempt.Result; + + Attempt result = await _userGroupService.CreateAsync(group, /*currentUser.Id*/ -1); + return result.Success + ? CreatedAtAction(controller => nameof(controller.ByKey), group.Key) + : UserGroupOperationStatusResult(result.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/UserGroups/DeleteUserGroupController.cs b/src/Umbraco.Cms.Api.Management/Controllers/UserGroups/DeleteUserGroupController.cs new file mode 100644 index 0000000000..ad26e28cc2 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/UserGroups/DeleteUserGroupController.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.UserGroups; + +public class DeleteUserGroupController : UserGroupsControllerBase +{ + private readonly IUserGroupService _userGroupService; + + public DeleteUserGroupController(IUserGroupService userGroupService) + { + _userGroupService = userGroupService; + } + + [HttpDelete("{key:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Delete(Guid key) + { + Attempt result = await _userGroupService.DeleteAsync(key); + + return result.Success + ? Ok() + : UserGroupOperationStatusResult(result.Result); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/UserGroups/GetAllUserGroupController.cs b/src/Umbraco.Cms.Api.Management/Controllers/UserGroups/GetAllUserGroupController.cs new file mode 100644 index 0000000000..86b61019fc --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/UserGroups/GetAllUserGroupController.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.UserGroups; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services; +using Umbraco.New.Cms.Core.Models; + +namespace Umbraco.Cms.Api.Management.Controllers.UserGroups; + +public class GetAllUserGroupController : UserGroupsControllerBase +{ + private readonly IUserGroupService _userGroupService; + private readonly IUserGroupViewModelFactory _userViewModelFactory; + + public GetAllUserGroupController( + IUserGroupService userGroupService, + IUserGroupViewModelFactory userViewModelFactory) + { + _userGroupService = userGroupService; + _userViewModelFactory = userViewModelFactory; + } + + [HttpGet] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> GetAll(int skip = 0, int take = 100) + { + // FIXME: In the old controller this endpoint had a switch "onlyCurrentUserGroup" + // If this was enabled we'd only return the groups the current user was in + // and even if it was set to false we'd still remove the admin group. + // We still need to have this functionality, however, it does not belong here. + // Instead we should implement this functionality on the CurrentUserController + PagedModel userGroups = await _userGroupService.GetAllAsync(skip, take); + + var viewModels = (await _userViewModelFactory.CreateMultipleAsync(userGroups.Items)).ToList(); + return new PagedViewModel { Total = userGroups.Total, Items = viewModels }; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/UserGroups/UpdateUserGroupController.cs b/src/Umbraco.Cms.Api.Management/Controllers/UserGroups/UpdateUserGroupController.cs new file mode 100644 index 0000000000..dc3f5bac70 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/UserGroups/UpdateUserGroupController.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.UserGroups; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.UserGroups; + +public class UpdateUserGroupController : UserGroupsControllerBase +{ + private readonly IUserGroupService _userGroupService; + private readonly IUserGroupViewModelFactory _userGroupViewModelFactory; + + public UpdateUserGroupController( + IUserGroupService userGroupService, + IUserGroupViewModelFactory userGroupViewModelFactory) + { + _userGroupService = userGroupService; + _userGroupViewModelFactory = userGroupViewModelFactory; + } + + [HttpPut("{key:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Update(Guid key, UserGroupUpdateModel dataTypeUpdateModel) + { + IUserGroup? existingUserGroup = await _userGroupService.GetAsync(key); + + if (existingUserGroup is null) + { + return UserGroupOperationStatusResult(UserGroupOperationStatus.NotFound); + } + + Attempt userGroupUpdateAttempt = await _userGroupViewModelFactory.UpdateAsync(existingUserGroup, dataTypeUpdateModel); + if (userGroupUpdateAttempt.Success is false) + { + return UserGroupOperationStatusResult(userGroupUpdateAttempt.Status); + } + + IUserGroup userGroup = userGroupUpdateAttempt.Result; + Attempt result = await _userGroupService.UpdateAsync(userGroup, -1); + + return result.Success + ? Ok() + : UserGroupOperationStatusResult(result.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/UserGroups/UserGroupsControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/UserGroups/UserGroupsControllerBase.cs new file mode 100644 index 0000000000..a24edfd973 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/UserGroups/UserGroupsControllerBase.cs @@ -0,0 +1,83 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.Builders; +using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.UserGroups; + +// TODO: This needs to be an authorized controller. + +[ApiController] +[VersionedApiBackOfficeRoute("user-groups")] +[ApiExplorerSettings(GroupName = "User Groups")] +[ApiVersion("1.0")] +public class UserGroupsControllerBase : ManagementApiControllerBase +{ + protected IActionResult UserGroupOperationStatusResult(UserGroupOperationStatus status) => + status switch + { + UserGroupOperationStatus.NotFound => NotFound("The user group could not be found"), + UserGroupOperationStatus.AlreadyExists => Conflict(new ProblemDetailsBuilder() + .WithTitle("User group already exists") + .WithDetail("The user group exists already.") + .Build()), + UserGroupOperationStatus.DuplicateAlias => Conflict(new ProblemDetailsBuilder() + .WithTitle("Duplicate alias") + .WithDetail("A user group already exists with the attempted alias.") + .Build()), + UserGroupOperationStatus.MissingUser => Unauthorized(new ProblemDetailsBuilder() + .WithTitle("Missing user") + .WithDetail("A performing user was not found when attempting to create the user group.") + .Build()), + UserGroupOperationStatus.IsSystemUserGroup => BadRequest(new ProblemDetailsBuilder() + .WithTitle("System user group") + .WithDetail("The operation is not allowed on a system user group.") + .Build()), + UserGroupOperationStatus.UnauthorizedMissingUserSection => Unauthorized(new ProblemDetailsBuilder() + .WithTitle("Unauthorized") + .WithDetail("The performing user does not have access to the required section") + .Build()), + UserGroupOperationStatus.UnauthorizedMissingSections => Unauthorized(new ProblemDetailsBuilder() + .WithTitle("Unauthorized section") + .WithDetail("The specified allowed section contained a section the performing user doesn't have access to.") + .Build()), + UserGroupOperationStatus.UnauthorizedStartNodes => Unauthorized(new ProblemDetailsBuilder() + .WithTitle("Unauthorized start node") + .WithDetail("The specified start nodes contained a start node the performing user doesn't have access to.") + .Build()), + UserGroupOperationStatus.UnauthorizedMissingUserGroup => Unauthorized(new ProblemDetailsBuilder() + .WithTitle("User not in user group") + .WithDetail("The current user is not in the user group") + .Build()), + UserGroupOperationStatus.CancelledByNotification => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Cancelled by notification") + .WithDetail("A notification handler prevented the language operation.") + .Build()), + UserGroupOperationStatus.DocumentStartNodeKeyNotFound => NotFound(new ProblemDetailsBuilder() + .WithTitle("Document start node key not found") + .WithDetail("The assigned document start node does not exists.") + .Build()), + UserGroupOperationStatus.MediaStartNodeKeyNotFound => NotFound(new ProblemDetailsBuilder() + .WithTitle("Media start node key not found") + .WithDetail("The assigned media start node does not exists.") + .Build()), + UserGroupOperationStatus.LanguageNotFound => NotFound(new ProblemDetailsBuilder() + .WithTitle("Language not found") + .WithDetail("The specified language cannot be found.") + .Build()), + UserGroupOperationStatus.NameTooLong => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Name too long") + .WithDetail("User Group name is too long.") + .Build()), + UserGroupOperationStatus.AliasTooLong => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Alias too long") + .WithDetail("The user group alias is too long.") + .Build()), + UserGroupOperationStatus.MissingName => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Missing user group name.") + .WithDetail("The user group name is required, and cannot be an empty string.") + .Build()), + _ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown user group operation status."), + }; +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/UserGroupsBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/UserGroupsBuilderExtensions.cs new file mode 100644 index 0000000000..0cb4cf7595 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/UserGroupsBuilderExtensions.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Core.DependencyInjection; + +namespace Umbraco.Cms.Api.Management.DependencyInjection; + +internal static class UserGroupsBuilderExtensions +{ + internal static IUmbracoBuilder AddUserGroups(this IUmbracoBuilder builder) + { + builder.Services.AddTransient(); + return builder; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/IUserGroupViewModelFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IUserGroupViewModelFactory.cs new file mode 100644 index 0000000000..b79fd7b9b3 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/IUserGroupViewModelFactory.cs @@ -0,0 +1,41 @@ +using Umbraco.Cms.Api.Management.ViewModels.UserGroups; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Factories; + +/// +/// A factory for creating +/// +public interface IUserGroupViewModelFactory +{ + /// + /// Creates a based on a + /// + /// + /// + Task CreateAsync(IUserGroup userGroup); + + /// + /// Creates multiple base on multiple + /// + /// + /// + Task> CreateMultipleAsync(IEnumerable userGroups); + + /// + /// Creates an based on a + /// + /// + /// An attempt indicating if the operation was a success as well as a more detailed . + Task> CreateAsync(UserGroupSaveModel saveModel); + + /// + /// Converts the values of an update model to fit with the existing backoffice implementations, and maps it to an existing user group. + /// + /// Existing user group to map to. + /// Update model containing the new values. + /// An attempt indicating if the operation was a success as well as a more detailed . + Task> UpdateAsync(IUserGroup current, UserGroupUpdateModel update); +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/UserGroupViewModelFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/UserGroupViewModelFactory.cs new file mode 100644 index 0000000000..e732497f3a --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/UserGroupViewModelFactory.cs @@ -0,0 +1,226 @@ +using Umbraco.Cms.Api.Management.Mapping; +using Umbraco.Cms.Api.Management.ViewModels.UserGroups; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Core.Strings; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Factories; + +/// +public class UserGroupViewModelFactory : IUserGroupViewModelFactory +{ + private readonly IEntityService _entityService; + private readonly IShortStringHelper _shortStringHelper; + private readonly ILanguageService _languageService; + + public UserGroupViewModelFactory( + IEntityService entityService, + IShortStringHelper shortStringHelper, + ILanguageService languageService) + { + _entityService = entityService; + _shortStringHelper = shortStringHelper; + _languageService = languageService; + } + + /// + public async Task CreateAsync(IUserGroup userGroup) + { + Guid? contentStartNodeKey = GetKeyFromId(userGroup.StartContentId, UmbracoObjectTypes.Document); + Guid? mediaStartNodeKey = GetKeyFromId(userGroup.StartMediaId, UmbracoObjectTypes.Media); + Attempt, UserGroupOperationStatus> languageIsoCodesMappingAttempt = await MapLanguageIdsToIsoCodeAsync(userGroup.AllowedLanguages); + + // We've gotten this data from the database, so the mapping should not fail + if (languageIsoCodesMappingAttempt.Success is false) + { + throw new InvalidOperationException($"Unknown language ID in User Group: {userGroup.Name}"); + } + + return new UserGroupViewModel + { + Name = userGroup.Name ?? string.Empty, + Key = userGroup.Key, + DocumentStartNodeKey = contentStartNodeKey, + MediaStartNodeKey = mediaStartNodeKey, + Icon = userGroup.Icon, + Languages = languageIsoCodesMappingAttempt.Result, + HasAccessToAllLanguages = userGroup.HasAccessToAllLanguages, + Permissions = userGroup.PermissionNames, + Sections = userGroup.AllowedSections.Select(SectionMapper.GetName), + }; + } + + /// + public async Task> CreateMultipleAsync(IEnumerable userGroups) + { + var userGroupViewModels = new List(); + foreach (IUserGroup userGroup in userGroups) + { + userGroupViewModels.Add(await CreateAsync(userGroup)); + } + + return userGroupViewModels; + } + + /// + public async Task> CreateAsync(UserGroupSaveModel saveModel) + { + var cleanedName = saveModel.Name.CleanForXss('[', ']', '(', ')', ':'); + + var group = new UserGroup(_shortStringHelper) + { + Name = cleanedName, + Alias = cleanedName, + Icon = saveModel.Icon, + HasAccessToAllLanguages = saveModel.HasAccessToAllLanguages, + PermissionNames = saveModel.Permissions, + }; + + Attempt assignmentAttempt = AssignStartNodesToUserGroup(saveModel, group); + if (assignmentAttempt.Success is false) + { + return Attempt.FailWithStatus(assignmentAttempt.Result, group); + } + + foreach (var section in saveModel.Sections) + { + group.AddAllowedSection(SectionMapper.GetAlias(section)); + } + + Attempt, UserGroupOperationStatus> languageIsoCodeMappingAttempt = await MapLanguageIsoCodesToIdsAsync(saveModel.Languages); + if (languageIsoCodeMappingAttempt.Success is false) + { + return Attempt.FailWithStatus(languageIsoCodeMappingAttempt.Status, group); + } + + foreach (var languageId in languageIsoCodeMappingAttempt.Result) + { + group.AddAllowedLanguage(languageId); + } + + return Attempt.SucceedWithStatus(UserGroupOperationStatus.Success, group); + } + + /// + public async Task> UpdateAsync(IUserGroup current, UserGroupUpdateModel update) + { + Attempt assignmentAttempt = AssignStartNodesToUserGroup(update, current); + if (assignmentAttempt.Success is false) + { + return Attempt.FailWithStatus(assignmentAttempt.Result, current); + } + + current.ClearAllowedLanguages(); + Attempt, UserGroupOperationStatus> languageIdsMappingAttempt = await MapLanguageIsoCodesToIdsAsync(update.Languages); + if (languageIdsMappingAttempt.Success is false) + { + return Attempt.FailWithStatus(languageIdsMappingAttempt.Status, current); + } + + foreach (var languageId in languageIdsMappingAttempt.Result) + { + current.AddAllowedLanguage(languageId); + } + + current.ClearAllowedSections(); + foreach (var sectionName in update.Sections) + { + current.AddAllowedSection(SectionMapper.GetAlias(sectionName)); + } + + current.Name = update.Name.CleanForXss('[', ']', '(', ')', ':'); + current.Icon = update.Icon; + current.HasAccessToAllLanguages = update.HasAccessToAllLanguages; + current.PermissionNames = update.Permissions; + + + return Attempt.SucceedWithStatus(UserGroupOperationStatus.Success, current); + } + + private async Task, UserGroupOperationStatus>> MapLanguageIdsToIsoCodeAsync(IEnumerable ids) + { + IEnumerable languages = await _languageService.GetAllAsync(); + string[] isoCodes = languages + .Where(x => ids.Contains(x.Id)) + .Select(x => x.IsoCode) + .ToArray(); + + return isoCodes.Length == ids.Count() + ? Attempt.SucceedWithStatus, UserGroupOperationStatus>(UserGroupOperationStatus.Success, isoCodes) + : Attempt.FailWithStatus, UserGroupOperationStatus>(UserGroupOperationStatus.LanguageNotFound, isoCodes); + } + + private async Task, UserGroupOperationStatus>> MapLanguageIsoCodesToIdsAsync(IEnumerable isoCodes) + { + IEnumerable languages = await _languageService.GetAllAsync(); + int[] languageIds = languages + .Where(x => isoCodes.Contains(x.IsoCode)) + .Select(x => x.Id) + .ToArray(); + + return languageIds.Length == isoCodes.Count() + ? Attempt.SucceedWithStatus, UserGroupOperationStatus>(UserGroupOperationStatus.Success, languageIds) + : Attempt.FailWithStatus, UserGroupOperationStatus>(UserGroupOperationStatus.LanguageNotFound, languageIds); + } + + private Attempt AssignStartNodesToUserGroup(UserGroupBase source, IUserGroup target) + { + if (source.DocumentStartNodeKey is not null) + { + var contentId = GetIdFromKey(source.DocumentStartNodeKey.Value, UmbracoObjectTypes.Document); + + if (contentId is null) + { + return Attempt.Fail(UserGroupOperationStatus.DocumentStartNodeKeyNotFound); + } + + target.StartContentId = contentId; + } + + if (source.MediaStartNodeKey is not null) + { + var mediaId = GetIdFromKey(source.MediaStartNodeKey.Value, UmbracoObjectTypes.Media); + + if (mediaId is null) + { + return Attempt.Fail(UserGroupOperationStatus.MediaStartNodeKeyNotFound); + } + + target.StartMediaId = mediaId; + } + + return Attempt.Succeed(UserGroupOperationStatus.Success); + } + + private Guid? GetKeyFromId(int? id, UmbracoObjectTypes objectType) + { + if (id is null) + { + return null; + } + + Attempt attempt = _entityService.GetKey(id.Value, objectType); + if (attempt.Success is false) + { + return null; + } + + return attempt.Result; + } + + private int? GetIdFromKey(Guid key, UmbracoObjectTypes objectType) + { + Attempt attempt = _entityService.GetId(key, objectType); + + if (attempt.Success is false) + { + return null; + } + + return attempt.Result; + } +} diff --git a/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs b/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs index 819e0d9f14..1bb5f1ff4c 100644 --- a/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs +++ b/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs @@ -37,6 +37,7 @@ public class ManagementApiComposer : IComposer .AddDataTypes() .AddTemplates() .AddLogViewer() + .AddUserGroups() .AddBackOfficeAuthentication() .AddApiVersioning() .AddSwaggerGen(); diff --git a/src/Umbraco.Cms.Api.Management/Mapping/SectionMapper.cs b/src/Umbraco.Cms.Api.Management/Mapping/SectionMapper.cs new file mode 100644 index 0000000000..41db2123e3 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Mapping/SectionMapper.cs @@ -0,0 +1,54 @@ +namespace Umbraco.Cms.Api.Management.Mapping; + +/// +/// Maps from the old section aliases to the new section names. +/// This is static since it's expected to be removed, so might as well make the clean up work as easy as possible. +/// FIXME: This is a temporary thing until permissions is fleshed out and section is either migrated to some form of permission +/// +public static class SectionMapper +{ + private static readonly List _sectionMappings = new() + { + new SectionMapping { Alias = "content", Name = "Umb.Section.Content" }, + new SectionMapping { Alias = "media", Name = "Umb.Section.Media" }, + new SectionMapping { Alias = "member", Name = "Umb.Section.Members" }, + new SectionMapping { Alias = "settings", Name = "Umb.Section.Settings" }, + new SectionMapping { Alias = "packages", Name = "Umb.Section.Packages" }, + new SectionMapping { Alias = "translation", Name = "Umb.Section.Translation" }, + new SectionMapping { Alias = "users", Name = "Umb.Section.Users" }, + new SectionMapping { Alias = "forms", Name = "Umb.Section.Forms" }, + }; + + public static string GetName(string alias) + { + SectionMapping? mapping = _sectionMappings.FirstOrDefault(x => x.Alias == alias); + + if (mapping is not null) + { + return mapping.Name; + } + + // If we can't find it we just fall back to the alias + return alias; + } + + public static string GetAlias(string name) + { + SectionMapping? mapping = _sectionMappings.FirstOrDefault(x => x.Name == name); + + if (mapping is not null) + { + return mapping.Alias; + } + + // If we can't find it we just fall back to the name + return name; + } + + private class SectionMapping + { + public required string Alias { get; init; } + + public required string Name { get; init; } + } +} diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 050473a8b5..fb12d7a5c4 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -702,9 +702,6 @@ } } }, - "404": { - "description": "Not Found" - }, "400": { "description": "Bad Request", "content": { @@ -715,6 +712,9 @@ } } }, + "404": { + "description": "Not Found" + }, "409": { "description": "Conflict", "content": { @@ -998,7 +998,9 @@ ], "operationId": "PostDictionaryUpload", "requestBody": { - "content": { } + "content": { + + } }, "responses": { "200": { @@ -1494,9 +1496,6 @@ } ], "responses": { - "401": { - "description": "Unauthorized" - }, "200": { "description": "Success", "content": { @@ -1506,6 +1505,9 @@ } } } + }, + "401": { + "description": "Unauthorized" } } } @@ -1537,9 +1539,6 @@ } ], "responses": { - "401": { - "description": "Unauthorized" - }, "200": { "description": "Success", "content": { @@ -1549,6 +1548,9 @@ } } } + }, + "401": { + "description": "Unauthorized" } } } @@ -1783,9 +1785,6 @@ } ], "responses": { - "404": { - "description": "Not Found" - }, "200": { "description": "Success", "content": { @@ -1799,6 +1798,9 @@ } } } + }, + "404": { + "description": "Not Found" } } } @@ -1820,9 +1822,6 @@ } ], "responses": { - "404": { - "description": "Not Found" - }, "200": { "description": "Success", "content": { @@ -1836,6 +1835,9 @@ } } } + }, + "404": { + "description": "Not Found" } } } @@ -1860,16 +1862,6 @@ } }, "responses": { - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetailsModel" - } - } - } - }, "200": { "description": "Success", "content": { @@ -1883,6 +1875,16 @@ } } } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsModel" + } + } + } } } } @@ -1934,16 +1936,6 @@ } ], "responses": { - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetailsModel" - } - } - } - }, "200": { "description": "Success", "content": { @@ -1953,6 +1945,16 @@ } } } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsModel" + } + } + } } } } @@ -2012,16 +2014,6 @@ } ], "responses": { - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetailsModel" - } - } - } - }, "200": { "description": "Success", "content": { @@ -2035,6 +2027,16 @@ } } } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsModel" + } + } + } } } } @@ -2056,16 +2058,6 @@ } ], "responses": { - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetailsModel" - } - } - } - }, "200": { "description": "Success", "content": { @@ -2075,6 +2067,16 @@ } } } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsModel" + } + } + } } } } @@ -2086,6 +2088,20 @@ ], "operationId": "GetInstallSettings", "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/InstallSettingsModel" + } + ] + } + } + } + }, "400": { "description": "Bad Request", "content": { @@ -2105,20 +2121,6 @@ } } } - }, - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/InstallSettingsModel" - } - ] - } - } - } } } } @@ -2143,6 +2145,9 @@ } }, "responses": { + "200": { + "description": "Success" + }, "400": { "description": "Bad Request", "content": { @@ -2162,9 +2167,6 @@ } } } - }, - "200": { - "description": "Success" } } } @@ -2189,6 +2191,9 @@ } }, "responses": { + "200": { + "description": "Success" + }, "400": { "description": "Bad Request", "content": { @@ -2198,9 +2203,6 @@ } } } - }, - "200": { - "description": "Success" } } } @@ -2263,19 +2265,6 @@ } }, "responses": { - "404": { - "description": "Not Found" - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetailsModel" - } - } - } - }, "201": { "description": "Created", "headers": { @@ -2288,6 +2277,19 @@ } } } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsModel" + } + } + } + }, + "404": { + "description": "Not Found" } } } @@ -2309,9 +2311,6 @@ } ], "responses": { - "404": { - "description": "Not Found" - }, "200": { "description": "Success", "content": { @@ -2325,6 +2324,9 @@ } } } + }, + "404": { + "description": "Not Found" } } }, @@ -2344,6 +2346,9 @@ } ], "responses": { + "200": { + "description": "Success" + }, "400": { "description": "Bad Request", "content": { @@ -2363,9 +2368,6 @@ } } } - }, - "200": { - "description": "Success" } } }, @@ -2398,8 +2400,8 @@ } }, "responses": { - "404": { - "description": "Not Found" + "200": { + "description": "Success" }, "400": { "description": "Bad Request", @@ -2411,8 +2413,8 @@ } } }, - "200": { - "description": "Success" + "404": { + "description": "Not Found" } } } @@ -2482,6 +2484,9 @@ } ], "responses": { + "200": { + "description": "Success" + }, "400": { "description": "Bad Request", "content": { @@ -2491,9 +2496,6 @@ } } } - }, - "200": { - "description": "Success" } } } @@ -2621,16 +2623,6 @@ } ], "responses": { - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetailsModel" - } - } - } - }, "200": { "description": "Success", "content": { @@ -2640,6 +2632,16 @@ } } } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsModel" + } + } + } } } } @@ -2702,16 +2704,6 @@ } }, "responses": { - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetailsModel" - } - } - } - }, "201": { "description": "Created", "headers": { @@ -2724,6 +2716,16 @@ } } } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsModel" + } + } + } } } } @@ -2745,9 +2747,6 @@ } ], "responses": { - "404": { - "description": "Not Found" - }, "200": { "description": "Success", "content": { @@ -2761,6 +2760,9 @@ } } } + }, + "404": { + "description": "Not Found" } } }, @@ -2780,11 +2782,11 @@ } ], "responses": { - "404": { - "description": "Not Found" - }, "200": { "description": "Success" + }, + "404": { + "description": "Not Found" } } } @@ -2814,6 +2816,9 @@ } ], "responses": { + "200": { + "description": "Success" + }, "400": { "description": "Bad Request", "content": { @@ -2823,9 +2828,6 @@ } } } - }, - "200": { - "description": "Success" } } } @@ -3012,9 +3014,6 @@ } ], "responses": { - "401": { - "description": "Unauthorized" - }, "200": { "description": "Success", "content": { @@ -3024,6 +3023,9 @@ } } } + }, + "401": { + "description": "Unauthorized" } } } @@ -3055,9 +3057,6 @@ } ], "responses": { - "401": { - "description": "Unauthorized" - }, "200": { "description": "Success", "content": { @@ -3067,6 +3066,9 @@ } } } + }, + "401": { + "description": "Unauthorized" } } } @@ -3728,16 +3730,6 @@ } ], "responses": { - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetailsModel" - } - } - } - }, "200": { "description": "Success", "content": { @@ -3747,6 +3739,16 @@ } } } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsModel" + } + } + } } } } @@ -4298,16 +4300,6 @@ ], "operationId": "GetServerStatus", "responses": { - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetailsModel" - } - } - } - }, "200": { "description": "Success", "content": { @@ -4321,6 +4313,16 @@ } } } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsModel" + } + } + } } } } @@ -4332,16 +4334,6 @@ ], "operationId": "GetServerVersion", "responses": { - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetailsModel" - } - } - } - }, "200": { "description": "Success", "content": { @@ -4355,6 +4347,16 @@ } } } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsModel" + } + } + } } } } @@ -4691,6 +4693,9 @@ } }, "responses": { + "200": { + "description": "Success" + }, "400": { "description": "Bad Request", "content": { @@ -4700,9 +4705,6 @@ } } } - }, - "200": { - "description": "Success" } } } @@ -5338,6 +5340,191 @@ } } } + }, + "/umbraco/management/api/v1/user-groups": { + "post": { + "tags": [ + "User Groups" + ], + "operationId": "PostUserGroups", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UserGroupSaveModel" + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "headers": { + "Location": { + "description": "Location of the newly created resource", + "schema": { + "type": "string", + "description": "Location of the newly created resource", + "format": "uri" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsModel" + } + } + } + } + } + }, + "get": { + "tags": [ + "User Groups" + ], + "operationId": "GetUserGroups", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedUserGroupModel" + } + } + } + } + } + } + }, + "/umbraco/management/api/v1/user-groups/{key}": { + "get": { + "tags": [ + "User Groups" + ], + "operationId": "GetUserGroupsByKey", + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UserGroupModel" + } + ] + } + } + } + }, + "404": { + "description": "Not Found" + } + } + }, + "delete": { + "tags": [ + "User Groups" + ], + "operationId": "DeleteUserGroupsByKey", + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success" + }, + "404": { + "description": "Not Found" + } + } + }, + "put": { + "tags": [ + "User Groups" + ], + "operationId": "PutUserGroupsByKey", + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UserGroupUpdateModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Success" + }, + "404": { + "description": "Not Found" + } + } + } } }, "components": { @@ -5365,6 +5552,9 @@ "format": "int32" }, "ContentTreeItemModel": { + "required": [ + "$type" + ], "type": "object", "allOf": [ { @@ -5372,6 +5562,9 @@ } ], "properties": { + "$type": { + "type": "string" + }, "noAccess": { "type": "boolean" }, @@ -5379,7 +5572,14 @@ "type": "boolean" } }, - "additionalProperties": false + "additionalProperties": false, + "discriminator": { + "propertyName": "$type", + "mapping": { + "ContentTreeItemViewModel": "#/components/schemas/ContentTreeItemModel", + "DocumentTreeItemViewModel": "#/components/schemas/DocumentTreeItemModel" + } + } }, "ContentTypeCleanupModel": { "type": "object", @@ -5604,6 +5804,9 @@ "additionalProperties": false }, "DataTypeModel": { + "required": [ + "$type" + ], "type": "object", "allOf": [ { @@ -5611,6 +5814,9 @@ } ], "properties": { + "$type": { + "type": "string" + }, "key": { "type": "string", "format": "uuid" @@ -5621,7 +5827,13 @@ "nullable": true } }, - "additionalProperties": false + "additionalProperties": false, + "discriminator": { + "propertyName": "$type", + "mapping": { + "DataTypeViewModel": "#/components/schemas/DataTypeModel" + } + } }, "DataTypeModelBaseModel": { "type": "object", @@ -5829,6 +6041,9 @@ "additionalProperties": false }, "DictionaryItemModel": { + "required": [ + "$type" + ], "type": "object", "allOf": [ { @@ -5836,12 +6051,21 @@ } ], "properties": { + "$type": { + "type": "string" + }, "key": { "type": "string", "format": "uuid" } }, - "additionalProperties": false + "additionalProperties": false, + "discriminator": { + "propertyName": "$type", + "mapping": { + "DictionaryItemViewModel": "#/components/schemas/DictionaryItemModel" + } + } }, "DictionaryItemModelBaseModel": { "type": "object", @@ -5967,6 +6191,9 @@ "format": "int32" }, "DocumentBlueprintTreeItemModel": { + "required": [ + "$type" + ], "type": "object", "allOf": [ { @@ -5974,6 +6201,9 @@ } ], "properties": { + "$type": { + "type": "string" + }, "documentTypeKey": { "type": "string", "format": "uuid" @@ -5986,7 +6216,13 @@ "nullable": true } }, - "additionalProperties": false + "additionalProperties": false, + "discriminator": { + "propertyName": "$type", + "mapping": { + "DocumentBlueprintTreeItemViewModel": "#/components/schemas/DocumentBlueprintTreeItemModel" + } + } }, "DocumentModel": { "type": "object", @@ -6015,6 +6251,9 @@ "additionalProperties": false }, "DocumentTreeItemModel": { + "required": [ + "$type" + ], "type": "object", "allOf": [ { @@ -6022,6 +6261,9 @@ } ], "properties": { + "$type": { + "type": "string" + }, "isProtected": { "type": "boolean" }, @@ -6032,7 +6274,13 @@ "type": "boolean" } }, - "additionalProperties": false + "additionalProperties": false, + "discriminator": { + "propertyName": "$type", + "mapping": { + "DocumentTreeItemViewModel": "#/components/schemas/DocumentTreeItemModel" + } + } }, "DocumentTypeModel": { "type": "object", @@ -6076,6 +6324,9 @@ "additionalProperties": false }, "DocumentTypeTreeItemModel": { + "required": [ + "$type" + ], "type": "object", "allOf": [ { @@ -6083,11 +6334,20 @@ } ], "properties": { + "$type": { + "type": "string" + }, "isElement": { "type": "boolean" } }, - "additionalProperties": false + "additionalProperties": false, + "discriminator": { + "propertyName": "$type", + "mapping": { + "DocumentTypeTreeItemViewModel": "#/components/schemas/DocumentTypeTreeItemModel" + } + } }, "DocumentValueModel": { "type": "object", @@ -6118,6 +6378,9 @@ "additionalProperties": false }, "EntityTreeItemModel": { + "required": [ + "$type" + ], "type": "object", "allOf": [ { @@ -6125,6 +6388,9 @@ } ], "properties": { + "$type": { + "type": "string" + }, "key": { "type": "string", "format": "uuid" @@ -6138,7 +6404,18 @@ "nullable": true } }, - "additionalProperties": false + "additionalProperties": false, + "discriminator": { + "propertyName": "$type", + "mapping": { + "EntityTreeItemViewModel": "#/components/schemas/EntityTreeItemModel", + "ContentTreeItemViewModel": "#/components/schemas/ContentTreeItemModel", + "DocumentBlueprintTreeItemViewModel": "#/components/schemas/DocumentBlueprintTreeItemModel", + "DocumentTreeItemViewModel": "#/components/schemas/DocumentTreeItemModel", + "DocumentTypeTreeItemViewModel": "#/components/schemas/DocumentTypeTreeItemModel", + "FolderTreeItemViewModel": "#/components/schemas/FolderTreeItemModel" + } + } }, "FieldModel": { "type": "object", @@ -6189,6 +6466,9 @@ "additionalProperties": false }, "FolderModel": { + "required": [ + "$type" + ], "type": "object", "allOf": [ { @@ -6196,6 +6476,9 @@ } ], "properties": { + "$type": { + "type": "string" + }, "key": { "type": "string", "format": "uuid" @@ -6206,7 +6489,13 @@ "nullable": true } }, - "additionalProperties": false + "additionalProperties": false, + "discriminator": { + "propertyName": "$type", + "mapping": { + "FolderViewModel": "#/components/schemas/FolderModel" + } + } }, "FolderModelBaseModel": { "type": "object", @@ -6218,6 +6507,9 @@ "additionalProperties": false }, "FolderTreeItemModel": { + "required": [ + "$type" + ], "type": "object", "allOf": [ { @@ -6225,11 +6517,21 @@ } ], "properties": { + "$type": { + "type": "string" + }, "isFolder": { "type": "boolean" } }, - "additionalProperties": false + "additionalProperties": false, + "discriminator": { + "propertyName": "$type", + "mapping": { + "FolderTreeItemViewModel": "#/components/schemas/FolderTreeItemModel", + "DocumentTypeTreeItemViewModel": "#/components/schemas/DocumentTypeTreeItemModel" + } + } }, "FolderUpdateModel": { "type": "object", @@ -6463,7 +6765,9 @@ }, "providerProperties": { "type": "object", - "additionalProperties": { }, + "additionalProperties": { + + }, "nullable": true } }, @@ -7343,6 +7647,30 @@ }, "additionalProperties": false }, + "PagedUserGroupModel": { + "required": [ + "items", + "total" + ], + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/UserGroupModel" + } + ] + } + } + }, + "additionalProperties": false + }, "ProblemDetailsModel": { "type": "object", "properties": { @@ -7368,7 +7696,9 @@ "nullable": true } }, - "additionalProperties": { } + "additionalProperties": { + + } }, "ProfilingStatusModel": { "type": "object", @@ -7485,8 +7815,14 @@ "additionalProperties": false }, "RecycleBinItemModel": { + "required": [ + "$type" + ], "type": "object", "properties": { + "$type": { + "type": "string" + }, "key": { "type": "string", "format": "uuid" @@ -7512,7 +7848,13 @@ "nullable": true } }, - "additionalProperties": false + "additionalProperties": false, + "discriminator": { + "propertyName": "$type", + "mapping": { + "RecycleBinItemViewModel": "#/components/schemas/RecycleBinItemModel" + } + } }, "RedirectStatusModel": { "enum": [ @@ -7740,6 +8082,9 @@ "additionalProperties": false }, "TemplateModel": { + "required": [ + "$type" + ], "type": "object", "allOf": [ { @@ -7747,12 +8092,21 @@ } ], "properties": { + "$type": { + "type": "string" + }, "key": { "type": "string", "format": "uuid" } }, - "additionalProperties": false + "additionalProperties": false, + "discriminator": { + "propertyName": "$type", + "mapping": { + "TemplateViewModel": "#/components/schemas/TemplateModel" + } + } }, "TemplateModelBaseModel": { "type": "object", @@ -8001,6 +8355,96 @@ }, "additionalProperties": false }, + "UserGroupBaseModel": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "icon": { + "type": "string", + "nullable": true + }, + "sections": { + "type": "array", + "items": { + "type": "string" + } + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + }, + "hasAccessToAllLanguages": { + "type": "boolean" + }, + "documentStartNodeKey": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "mediaStartNodeKey": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "permissions": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "UserGroupModel": { + "required": [ + "$type" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/UserGroupBaseModel" + } + ], + "properties": { + "$type": { + "type": "string" + }, + "key": { + "type": "string", + "format": "uuid" + } + }, + "additionalProperties": false, + "discriminator": { + "propertyName": "$type", + "mapping": { + "UserGroupViewModel": "#/components/schemas/UserGroupModel" + } + } + }, + "UserGroupSaveModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/UserGroupBaseModel" + } + ], + "additionalProperties": false + }, + "UserGroupUpdateModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/UserGroupBaseModel" + } + ], + "additionalProperties": false + }, "UserInstallModel": { "required": [ "email", @@ -8117,7 +8561,9 @@ "authorizationCode": { "authorizationUrl": "/umbraco/management/api/v1.0/security/back-office/authorize", "tokenUrl": "/umbraco/management/api/v1.0/security/back-office/token", - "scopes": { } + "scopes": { + + } } } } @@ -8125,7 +8571,9 @@ }, "security": [ { - "OAuth": [ ] + "OAuth": [ + + ] } ] } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeViewModel.cs index 77a0a8df25..25d094df36 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeViewModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeViewModel.cs @@ -1,6 +1,6 @@ namespace Umbraco.Cms.Api.Management.ViewModels.DataType; -public class DataTypeViewModel : DataTypeModelBase +public class DataTypeViewModel : DataTypeModelBase, INamedEntityViewModel { public Guid Key { get; set; } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryItemViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryItemViewModel.cs index a7ee7e5b32..0b795c1fc2 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryItemViewModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Dictionary/DictionaryItemViewModel.cs @@ -1,6 +1,6 @@ namespace Umbraco.Cms.Api.Management.ViewModels.Dictionary; -public class DictionaryItemViewModel : DictionaryItemModelBase +public class DictionaryItemViewModel : DictionaryItemModelBase, INamedEntityViewModel { public Guid Key { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Folder/FolderViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Folder/FolderViewModel.cs index f76bf1493f..777ca988fd 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Folder/FolderViewModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Folder/FolderViewModel.cs @@ -1,6 +1,6 @@ namespace Umbraco.Cms.Api.Management.ViewModels.Folder; -public class FolderViewModel : FolderModelBase +public class FolderViewModel : FolderModelBase, INamedEntityViewModel { public Guid Key { get; set; } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/INamedEntityViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/INamedEntityViewModel.cs new file mode 100644 index 0000000000..1da1a24c0e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/INamedEntityViewModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Api.Management.ViewModels; + +public interface INamedEntityViewModel +{ + Guid Key { get; } + + string Name { get;} +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/RecycleBin/RecycleBinItemViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/RecycleBin/RecycleBinItemViewModel.cs index d3d25622f0..aa7ab3cd46 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/RecycleBin/RecycleBinItemViewModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/RecycleBin/RecycleBinItemViewModel.cs @@ -1,6 +1,6 @@ namespace Umbraco.Cms.Api.Management.ViewModels.RecycleBin; -public class RecycleBinItemViewModel +public class RecycleBinItemViewModel : INamedEntityViewModel { public Guid Key { get; set; } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Template/TemplateViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Template/TemplateViewModel.cs index 8fe8fc7ddd..e8958df583 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Template/TemplateViewModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Template/TemplateViewModel.cs @@ -1,6 +1,6 @@ namespace Umbraco.Cms.Api.Management.ViewModels.Template; -public class TemplateViewModel : TemplateModelBase +public class TemplateViewModel : TemplateModelBase, INamedEntityViewModel { public Guid Key { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Tree/EntityTreeItemViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Tree/EntityTreeItemViewModel.cs index a163a4f9a7..008bb81c5c 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Tree/EntityTreeItemViewModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Tree/EntityTreeItemViewModel.cs @@ -1,6 +1,6 @@ namespace Umbraco.Cms.Api.Management.ViewModels.Tree; -public class EntityTreeItemViewModel : TreeItemViewModel +public class EntityTreeItemViewModel : TreeItemViewModel, INamedEntityViewModel { public Guid Key { get; set; } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/UserGroups/UserGroupBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroups/UserGroupBase.cs new file mode 100644 index 0000000000..994ef00abf --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroups/UserGroupBase.cs @@ -0,0 +1,58 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.UserGroups; + +/// +/// +/// Base class for front-end representation of a User Group. +/// +/// +/// Contains all the properties shared between Save, Update, Representation, etc... +/// +/// +public class UserGroupBase +{ + /// + /// The name of the user groups + /// + public required string Name { get; init; } + + /// + /// The Icon for the user group + /// + public string? Icon { get; init; } + + /// + /// The sections that the user group has access to + /// + public required IEnumerable Sections { get; init; } + + /// + /// The languages that the user group has access to + /// + public required IEnumerable Languages { get; init; } + + /// + /// Flag indicating if the user group gives access to all languages, regardless of . + /// + public required bool HasAccessToAllLanguages { get; init; } + + /// + /// The key of the document that should act as root node for the user group + /// + /// This can be overwritten by a different user group if a user is a member of multiple groups + /// + /// + public Guid? DocumentStartNodeKey { get; init; } + + /// + /// The Key of the media that should act as root node for the user group + /// + /// This can be overwritten by a different user group if a user is a member of multiple groups + /// + /// + public Guid? MediaStartNodeKey { get; init; } + + /// + /// Ad-hoc list of permissions provided, and maintained by the front-end. The server has no concept of what these mean. + /// + public required ISet Permissions { get; init; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/UserGroups/UserGroupSaveModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroups/UserGroupSaveModel.cs new file mode 100644 index 0000000000..d42a2c2faf --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroups/UserGroupSaveModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.UserGroups; + +public class UserGroupSaveModel : UserGroupBase +{ + +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/UserGroups/UserGroupUpdateModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroups/UserGroupUpdateModel.cs new file mode 100644 index 0000000000..fa08b60ec9 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroups/UserGroupUpdateModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.UserGroups; + +public class UserGroupUpdateModel : UserGroupBase +{ + +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/UserGroups/UserGroupViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroups/UserGroupViewModel.cs new file mode 100644 index 0000000000..9b6212f7ef --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroups/UserGroupViewModel.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.UserGroups; + +public class UserGroupViewModel : UserGroupBase, INamedEntityViewModel +{ + /// + /// The key identifier for the user group. + /// + public required Guid Key { get; init; } + +} diff --git a/src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj b/src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj index d082dff945..5a8dd128a5 100644 --- a/src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj +++ b/src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj @@ -16,7 +16,9 @@ - $(ProjectDir)wwwroot\umbraco + $(ProjectDir)wwwroot\umbraco + $(BasePath)\lib + $(BasePath)\backoffice @@ -24,9 +26,19 @@ + + + + + + + + + + @@ -36,12 +48,26 @@ + + + + + + + - + - + + + + + + + + diff --git a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoWebsite/Maintenance.cshtml b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoWebsite/Maintenance.cshtml index 46739cdef7..94de5f3c52 100644 --- a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoWebsite/Maintenance.cshtml +++ b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoWebsite/Maintenance.cshtml @@ -17,7 +17,7 @@ Website is Under Maintainance - +