diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs index 0e3f4ddb81a5..4312bc48d7cf 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs @@ -64,11 +64,13 @@ internal static IActionResult ContentTypeOperationStatusResult(ContentTypeOperat .Build()), ContentTypeOperationStatus.InvalidComposition => new BadRequestObjectResult(new ProblemDetailsBuilder() .WithTitle("Invalid composition") - .WithDetail($"The specified {type} type composition is invalid")), + .WithDetail($"The specified {type} type composition is invalid") + .Build()), ContentTypeOperationStatus.InvalidParent => new BadRequestObjectResult(new ProblemDetailsBuilder() .WithTitle("Invalid parent") .WithDetail( - "The specified parent is invalid, or cannot be used in combination with the specified composition/inheritance")), + "The specified parent is invalid, or cannot be used in combination with the specified composition/inheritance") + .Build()), ContentTypeOperationStatus.DuplicatePropertyTypeAlias => new BadRequestObjectResult( new ProblemDetailsBuilder() .WithTitle("Duplicate property type alias") @@ -91,7 +93,8 @@ public static IActionResult ContentTypeStructureOperationStatusResult(ContentTyp .Build()), ContentTypeStructureOperationStatus.NotAllowedByPath => new BadRequestObjectResult(new ProblemDetailsBuilder() .WithTitle("Not allowed by path") - .WithDetail($"The {type} type operation cannot be performed due to not allowed path (i.e. a child of itself)")), + .WithDetail($"The {type} type operation cannot be performed due to not allowed path (i.e. a child of itself)") + .Build()), ContentTypeStructureOperationStatus.NotFound => new NotFoundObjectResult(new ProblemDetailsBuilder() .WithTitle("Not Found") .WithDetail($"The specified {type} type was not found") diff --git a/src/Umbraco.Cms.Api.Management/Controllers/ManagementApiControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/ManagementApiControllerBase.cs index 400ebd7cbf83..62dc995fab1e 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/ManagementApiControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/ManagementApiControllerBase.cs @@ -9,6 +9,7 @@ using Umbraco.Cms.Api.Management.DependencyInjection; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Features; +using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Web.Common.Authorization; @@ -40,9 +41,10 @@ protected IActionResult CreatedAtAction(Expression> action, o } protected static Guid CurrentUserKey(IBackOfficeSecurityAccessor backOfficeSecurityAccessor) - { - return backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Key ?? throw new InvalidOperationException("No backoffice user found"); - } + => CurrentUser(backOfficeSecurityAccessor).Key; + + protected static IUser CurrentUser(IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + => backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser ?? throw new InvalidOperationException("No backoffice user found"); /// /// Creates a 403 Forbidden result. diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/MediaControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/MediaControllerBase.cs index 301cd3fc9b66..96b2a62029e1 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/MediaControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/MediaControllerBase.cs @@ -24,6 +24,7 @@ protected IActionResult MediaNotFound() => NotFound(problemDetailsBuilder .WithTitle("The requested Media could not be found") .Build())); + protected IActionResult MediaEditingOperationStatusResult( ContentEditingOperationStatus status, TContentModelBase requestModel, diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Member/ByKeyMemberController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Member/ByKeyMemberController.cs new file mode 100644 index 000000000000..5da3383f93fc --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Member/ByKeyMemberController.cs @@ -0,0 +1,38 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.Member; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.Member; + +[ApiVersion("1.0")] +public class ByKeyMemberController : MemberControllerBase +{ + private readonly IMemberEditingService _memberEditingService; + private readonly IMemberPresentationFactory _memberPresentationFactory; + + public ByKeyMemberController(IMemberEditingService memberEditingService, IMemberPresentationFactory memberPresentationFactory) + { + _memberEditingService = memberEditingService; + _memberPresentationFactory = memberPresentationFactory; + } + + [HttpGet("{id:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(MemberResponseModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task ByKey(Guid id) + { + IMember? member = await _memberEditingService.GetAsync(id); + if (member == null) + { + return MemberNotFound(); + } + + MemberResponseModel model = await _memberPresentationFactory.CreateResponseModelAsync(member); + return Ok(model); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Member/CreateMemberController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Member/CreateMemberController.cs new file mode 100644 index 000000000000..719e2e07751c --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Member/CreateMemberController.cs @@ -0,0 +1,46 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.Member; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Member; + +[ApiVersion("1.0")] +public class CreateMemberController : MemberControllerBase +{ + private readonly IMemberEditingPresentationFactory _memberEditingPresentationFactory; + private readonly IMemberEditingService _memberEditingService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public CreateMemberController( + IMemberEditingPresentationFactory memberEditingPresentationFactory, + IMemberEditingService memberEditingService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _memberEditingPresentationFactory = memberEditingPresentationFactory; + _memberEditingService = memberEditingService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + [HttpPost] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + public async Task Create(CreateMemberRequestModel createRequestModel) + { + MemberCreateModel model = _memberEditingPresentationFactory.MapCreateModel(createRequestModel); + Attempt result = await _memberEditingService.CreateAsync(model, CurrentUser(_backOfficeSecurityAccessor)); + + return result.Success + ? CreatedAtId(controller => nameof(controller.ByKey), result.Result.Content!.Key) + : MemberEditingStatusResult(result.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Member/DeleteMemberController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Member/DeleteMemberController.cs new file mode 100644 index 000000000000..e13254fa0002 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Member/DeleteMemberController.cs @@ -0,0 +1,37 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Member; + +[ApiVersion("1.0")] +public class DeleteMemberController : MemberControllerBase +{ + private readonly IMemberEditingService _memberEditingService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public DeleteMemberController(IMemberEditingService memberEditingService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _memberEditingService = memberEditingService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + [HttpDelete("{id:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task Delete(Guid id) + { + Attempt result = await _memberEditingService.DeleteAsync(id, CurrentUserKey(_backOfficeSecurityAccessor)); + + return result.Success + ? Ok() + : MemberEditingStatusResult(result.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Member/Item/ItemMemberItemController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Member/Item/ItemMemberItemController.cs index 937a0e534787..8d1b77d0cf4f 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Member/Item/ItemMemberItemController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Member/Item/ItemMemberItemController.cs @@ -1,9 +1,11 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.ViewModels.Member.Item; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Api.Management.Controllers.Member.Item; @@ -11,13 +13,13 @@ namespace Umbraco.Cms.Api.Management.Controllers.Member.Item; [ApiVersion("1.0")] public class ItemMemberItemController : MemberItemControllerBase { - private readonly IMemberService _memberService; - private readonly IUmbracoMapper _mapper; + private readonly IEntityService _entityService; + private readonly IMemberPresentationFactory _memberPresentationFactory; - public ItemMemberItemController(IMemberService memberService, IUmbracoMapper mapper) + public ItemMemberItemController(IEntityService entityService, IMemberPresentationFactory memberPresentationFactory) { - _memberService = memberService; - _mapper = mapper; + _entityService = entityService; + _memberPresentationFactory = memberPresentationFactory; } [HttpGet("item")] @@ -25,8 +27,11 @@ public ItemMemberItemController(IMemberService memberService, IUmbracoMapper map [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] public async Task Item([FromQuery(Name = "id")] HashSet ids) { - IEnumerable members = await _memberService.GetByKeysAsync(ids.ToArray()); - List responseModels = _mapper.MapEnumerable(members); - return Ok(responseModels); + IEnumerable members = _entityService + .GetAll(UmbracoObjectTypes.Member, ids.ToArray()) + .OfType(); + + IEnumerable responseModels = members.Select(_memberPresentationFactory.CreateItemResponseModel); + return await Task.FromResult(Ok(responseModels)); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Member/MemberControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Member/MemberControllerBase.cs new file mode 100644 index 000000000000..4ed0e3a4a650 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Member/MemberControllerBase.cs @@ -0,0 +1,95 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.Builders; +using Umbraco.Cms.Api.Management.Controllers.Content; +using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Api.Management.ViewModels.Content; +using Umbraco.Cms.Api.Management.ViewModels.Member; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Member; + +[ApiController] +[VersionedApiBackOfficeRoute(Constants.UdiEntityType.Member)] +[ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Member))] +// FIXME: implement authorization +// [Authorize(Policy = "New" + AuthorizationPolicies.SectionAccessMembers)] +public class MemberControllerBase : ContentControllerBase +{ + protected IActionResult MemberNotFound() => NotFound(new ProblemDetailsBuilder() + .WithTitle("The requested member could not be found") + .Build()); + + protected IActionResult MemberEditingStatusResult(MemberEditingStatus status) + => status.MemberEditingOperationStatus is not MemberEditingOperationStatus.Success + ? MemberEditingOperationStatusResult(status.MemberEditingOperationStatus) + : status.ContentEditingOperationStatus is not ContentEditingOperationStatus.Success + ? ContentEditingOperationStatusResult(status.ContentEditingOperationStatus) + : throw new ArgumentException("Please handle success status explicitly in the controllers", nameof(status)); + + protected IActionResult MemberEditingOperationStatusResult(MemberEditingOperationStatus status) + => OperationStatusResult(status, problemDetailsBuilder => status switch + { + MemberEditingOperationStatus.MemberNotFound => NotFound(problemDetailsBuilder + .WithTitle("The requested member could not be found") + .Build()), + MemberEditingOperationStatus.MemberTypeNotFound => NotFound(problemDetailsBuilder + .WithTitle("The requested member type could not be found") + .Build()), + MemberEditingOperationStatus.UnlockFailed => BadRequest(problemDetailsBuilder + .WithTitle("Could not unlock the member") + .WithDetail("Please refer to the logs for additional information.") + .Build()), + MemberEditingOperationStatus.DisableTwoFactorFailed => BadRequest(problemDetailsBuilder + .WithTitle("Could not disable 2FA for the member") + .WithDetail("Please refer to the logs for additional information.") + .Build()), + MemberEditingOperationStatus.RoleAssignmentFailed => BadRequest(problemDetailsBuilder + .WithTitle("Could not update role assignments for the member") + .WithDetail("Please refer to the logs for additional information.") + .Build()), + MemberEditingOperationStatus.PasswordChangeFailed => BadRequest(problemDetailsBuilder + .WithTitle("Could not change the password") + .WithDetail( + "This is likely because the password did not meet the complexity requirements. The logs might hold additional information.") + .Build()), + MemberEditingOperationStatus.InvalidPassword => BadRequest(problemDetailsBuilder + .WithTitle("Invalid password supplied") + .WithDetail("The password did not meet the complexity requirements.") + .Build()), + MemberEditingOperationStatus.InvalidName => BadRequest(problemDetailsBuilder + .WithTitle("Invalid name supplied") + .Build()), + MemberEditingOperationStatus.InvalidUsername => BadRequest(problemDetailsBuilder + .WithTitle("Invalid username supplied") + .Build()), + MemberEditingOperationStatus.InvalidEmail => BadRequest(problemDetailsBuilder + .WithTitle("Invalid email supplied") + .Build()), + MemberEditingOperationStatus.DuplicateUsername => BadRequest(problemDetailsBuilder + .WithTitle("Duplicate username detected") + .WithDetail("The supplied username is already in use by another member.") + .Build()), + MemberEditingOperationStatus.DuplicateEmail => BadRequest(problemDetailsBuilder + .WithTitle("Duplicate email detected") + .WithDetail("The supplied email is already in use by another member.") + .Build()), + MemberEditingOperationStatus.Unknown => StatusCode( + StatusCodes.Status500InternalServerError, + problemDetailsBuilder + .WithTitle("Unknown error. Please see the log for more details.") + .Build()), + _ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder + .WithTitle("Unknown member operation status.") + .Build()) + }); + + protected IActionResult MemberEditingOperationStatusResult( + ContentEditingOperationStatus status, + TContentModelBase requestModel, + ContentValidationResult validationResult) + where TContentModelBase : ContentModelBase + => ContentEditingOperationStatusResult(status, requestModel, validationResult); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Member/UpdateMemberController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Member/UpdateMemberController.cs new file mode 100644 index 000000000000..d0b1e542bbaa --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Member/UpdateMemberController.cs @@ -0,0 +1,52 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.Member; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Member; + +[ApiVersion("1.0")] +public class UpdateMemberController : MemberControllerBase +{ + private readonly IMemberEditingService _memberEditingService; + private readonly IMemberEditingPresentationFactory _memberEditingPresentationFactory; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public UpdateMemberController( + IMemberEditingService memberEditingService, + IMemberEditingPresentationFactory memberEditingPresentationFactory, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _memberEditingService = memberEditingService; + _memberEditingPresentationFactory = memberEditingPresentationFactory; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + [HttpPut("{id:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task Update(Guid id, UpdateMemberRequestModel updateRequestModel) + { + IMember? member = await _memberEditingService.GetAsync(id); + if (member == null) + { + return MemberNotFound(); + } + + MemberUpdateModel model = _memberEditingPresentationFactory.MapUpdateModel(updateRequestModel); + Attempt result = await _memberEditingService.UpdateAsync(member, model, CurrentUser(_backOfficeSecurityAccessor)); + + return result.Success + ? Ok() + : MemberEditingStatusResult(result.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Member/ValidateCreateMemberController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Member/ValidateCreateMemberController.cs new file mode 100644 index 000000000000..3929bf9b26de --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Member/ValidateCreateMemberController.cs @@ -0,0 +1,41 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.Member; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Member; + +[ApiVersion("1.0")] +public class ValidateCreateMemberController : MemberControllerBase +{ + private readonly IMemberEditingService _memberEditingService; + private readonly IMemberEditingPresentationFactory _memberEditingPresentationFactory; + + public ValidateCreateMemberController( + IMemberEditingService memberEditingService, + IMemberEditingPresentationFactory memberEditingPresentationFactory) + { + _memberEditingService = memberEditingService; + _memberEditingPresentationFactory = memberEditingPresentationFactory; + } + + [HttpPost("validate")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task Validate(CreateMemberRequestModel requestModel) + { + MemberCreateModel model = _memberEditingPresentationFactory.MapCreateModel(requestModel); + Attempt result = await _memberEditingService.ValidateCreateAsync(model); + + return result.Success + ? Ok() + : MemberEditingOperationStatusResult(result.Status, requestModel, result.Result); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Member/ValidateUpdateMemberController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Member/ValidateUpdateMemberController.cs new file mode 100644 index 000000000000..d14ea5cd3e8c --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Member/ValidateUpdateMemberController.cs @@ -0,0 +1,48 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.Member; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Member; + +[ApiVersion("1.0")] +public class ValidateUpdateMemberController : MemberControllerBase +{ + private readonly IMemberEditingService _memberEditingService; + private readonly IMemberEditingPresentationFactory _memberEditingPresentationFactory; + + public ValidateUpdateMemberController( + IMemberEditingService memberEditingService, + IMemberEditingPresentationFactory memberEditingPresentationFactory) + { + _memberEditingService = memberEditingService; + _memberEditingPresentationFactory = memberEditingPresentationFactory; + } + + [HttpPut("{id:guid}/validate")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task Validate(Guid id, UpdateMemberRequestModel requestModel) + { + IMember? member = await _memberEditingService.GetAsync(id); + if (member is null) + { + return MemberNotFound(); + } + + MemberUpdateModel model = _memberEditingPresentationFactory.MapUpdateModel(requestModel); + Attempt result = await _memberEditingService.ValidateUpdateAsync(member, model); + + return result.Success + ? Ok() + : MemberEditingOperationStatusResult(result.Status, requestModel, result.Result); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/AvailableCompositionMemberTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/AvailableCompositionMemberTypeController.cs new file mode 100644 index 000000000000..f8b3f2d030a5 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/AvailableCompositionMemberTypeController.cs @@ -0,0 +1,37 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.MemberType; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services.ContentTypeEditing; + +namespace Umbraco.Cms.Api.Management.Controllers.MemberType; + +[ApiVersion("1.0")] +public class AvailableCompositionMemberTypeController : MemberTypeControllerBase +{ + private readonly IMemberTypeEditingService _memberTypeEditingService; + private readonly IMemberTypeEditingPresentationFactory _presentationFactory; + + public AvailableCompositionMemberTypeController(IMemberTypeEditingService memberTypeEditingService, IMemberTypeEditingPresentationFactory presentationFactory) + { + _memberTypeEditingService = memberTypeEditingService; + _presentationFactory = presentationFactory; + } + + [HttpPost("available-compositions")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task AvailableCompositions(MemberTypeCompositionRequestModel compositionModel) + { + IEnumerable availableCompositions = await _memberTypeEditingService.GetAvailableCompositionsAsync( + compositionModel.Id, + compositionModel.CurrentCompositeIds, + compositionModel.CurrentPropertyAliases); + + IEnumerable responseModels = _presentationFactory.MapCompositionModels(availableCompositions); + + return Ok(responseModels); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/ByKeyMemberTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/ByKeyMemberTypeController.cs new file mode 100644 index 000000000000..6e6e057d27cb --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/ByKeyMemberTypeController.cs @@ -0,0 +1,39 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.MemberType; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.MemberType; + +[ApiVersion("1.0")] +public class ByKeyMemberTypeController : MemberTypeControllerBase +{ + private readonly IMemberTypeService _memberTypeService; + private readonly IUmbracoMapper _umbracoMapper; + + public ByKeyMemberTypeController(IMemberTypeService memberTypeService, IUmbracoMapper umbracoMapper) + { + _memberTypeService = memberTypeService; + _umbracoMapper = umbracoMapper; + } + + [HttpGet("{id:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(MemberTypeResponseModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task ByKey(Guid id) + { + IMemberType? MemberType = await _memberTypeService.GetAsync(id); + if (MemberType == null) + { + return OperationStatusResult(ContentTypeOperationStatus.NotFound); + } + + MemberTypeResponseModel model = _umbracoMapper.Map(MemberType)!; + return await Task.FromResult(Ok(model)); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/CompositionReferenceMemberTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/CompositionReferenceMemberTypeController.cs new file mode 100644 index 000000000000..ac77eb5a8131 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/CompositionReferenceMemberTypeController.cs @@ -0,0 +1,43 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.MemberType; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.MemberType; + +[ApiVersion("1.0")] +public class CompositionReferenceMemberTypeController : MemberTypeControllerBase +{ + private readonly IMemberTypeService _memberTypeService; + private readonly IUmbracoMapper _umbracoMapper; + + public CompositionReferenceMemberTypeController(IMemberTypeService memberTypeService, IUmbracoMapper umbracoMapper) + { + _memberTypeService = memberTypeService; + _umbracoMapper = umbracoMapper; + } + + [HttpGet("{id:guid}/composition-references")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task CompositionReferences(Guid id) + { + var memberType = await _memberTypeService.GetAsync(id); + + if (memberType is null) + { + return OperationStatusResult(ContentTypeOperationStatus.NotFound); + } + + IEnumerable composedOf = _memberTypeService.GetComposedOf(memberType.Id); + List responseModels = _umbracoMapper.MapEnumerable(composedOf); + + return Ok(responseModels); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/CopyMemberTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/CopyMemberTypeController.cs new file mode 100644 index 000000000000..eb90eccff0b4 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/CopyMemberTypeController.cs @@ -0,0 +1,32 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.MemberType; + +[ApiVersion("1.0")] +public class CopyMemberTypeController : MemberTypeControllerBase +{ + private readonly IMemberTypeService _memberTypeService; + + public CopyMemberTypeController(IMemberTypeService memberTypeService) + => _memberTypeService = memberTypeService; + + [HttpPost("{id:guid}/copy")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task Copy(Guid id) + { + Attempt result = await _memberTypeService.CopyAsync(id, containerKey: null); + + return result.Success + ? CreatedAtId(controller => nameof(controller.ByKey), result.Result!.Key) + : StructureOperationStatusResult(result.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/CreateMemberTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/CreateMemberTypeController.cs new file mode 100644 index 000000000000..54024fc7bc4a --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/CreateMemberTypeController.cs @@ -0,0 +1,46 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.MemberType; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services.ContentTypeEditing; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.MemberType; + +[ApiVersion("1.0")] +public class CreateMemberTypeController : MemberTypeControllerBase +{ + private readonly IMemberTypeEditingPresentationFactory _memberTypeEditingPresentationFactory; + private readonly IMemberTypeEditingService _memberTypeEditingService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public CreateMemberTypeController( + IMemberTypeEditingPresentationFactory memberTypeEditingPresentationFactory, + IMemberTypeEditingService memberTypeEditingService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _memberTypeEditingPresentationFactory = memberTypeEditingPresentationFactory; + _memberTypeEditingService = memberTypeEditingService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + [HttpPost] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task Create(CreateMemberTypeRequestModel requestModel) + { + MemberTypeCreateModel model = _memberTypeEditingPresentationFactory.MapCreateModel(requestModel); + Attempt result = await _memberTypeEditingService.CreateAsync(model, CurrentUserKey(_backOfficeSecurityAccessor)); + + return result.Success + ? CreatedAtId(controller => nameof(controller.ByKey), result.Result!.Key) + : OperationStatusResult(result.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/DeleteMemberTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/DeleteMemberTypeController.cs new file mode 100644 index 000000000000..a06b0eb5d3ef --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/DeleteMemberTypeController.cs @@ -0,0 +1,31 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.MemberType; + +[ApiVersion("1.0")] +public class DeleteMemberTypeController : MemberTypeControllerBase +{ + private readonly IMemberTypeService _memberTypeService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public DeleteMemberTypeController(IMemberTypeService memberTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _memberTypeService = memberTypeService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + [HttpDelete("{id:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task Delete(Guid id) + { + ContentTypeOperationStatus status = await _memberTypeService.DeleteAsync(id, CurrentUserKey(_backOfficeSecurityAccessor)); + return OperationStatusResult(status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/MemberTypeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/MemberTypeControllerBase.cs new file mode 100644 index 000000000000..a672f23ab86e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/MemberTypeControllerBase.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Controllers.DocumentType; +using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; + +namespace Umbraco.Cms.Api.Management.Controllers.MemberType; + +[ApiController] +[VersionedApiBackOfficeRoute(Constants.UdiEntityType.MemberType)] +[ApiExplorerSettings(GroupName = "Member Type")] +[Authorize(Policy = "New" + AuthorizationPolicies.TreeAccessMemberTypes)] +public abstract class MemberTypeControllerBase : ManagementApiControllerBase +{ + protected IActionResult OperationStatusResult(ContentTypeOperationStatus status) + => DocumentTypeControllerBase.ContentTypeOperationStatusResult(status, "member"); + + protected IActionResult StructureOperationStatusResult(ContentTypeStructureOperationStatus status) + => DocumentTypeControllerBase.ContentTypeStructureOperationStatusResult(status, "member"); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/UpdateMemberTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/UpdateMemberTypeController.cs new file mode 100644 index 000000000000..3d8ae202af0f --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/UpdateMemberTypeController.cs @@ -0,0 +1,56 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.MemberType; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.ContentTypeEditing; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.MemberType; + +[ApiVersion("1.0")] +public class UpdateMemberTypeController : MemberTypeControllerBase +{ + private readonly IMemberTypeEditingPresentationFactory _memberTypeEditingPresentationFactory; + private readonly IMemberTypeEditingService _memberTypeEditingService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IMemberTypeService _memberTypeService; + + public UpdateMemberTypeController( + IMemberTypeEditingPresentationFactory memberTypeEditingPresentationFactory, + IMemberTypeEditingService memberTypeEditingService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IMemberTypeService memberTypeService) + { + _memberTypeEditingPresentationFactory = memberTypeEditingPresentationFactory; + _memberTypeEditingService = memberTypeEditingService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _memberTypeService = memberTypeService; + } + + [HttpPut("{id:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task Update(Guid id, UpdateMemberTypeRequestModel requestModel) + { + IMemberType? memberType = await _memberTypeService.GetAsync(id); + if (memberType is null) + { + return OperationStatusResult(ContentTypeOperationStatus.NotFound); + } + + MemberTypeUpdateModel model = _memberTypeEditingPresentationFactory.MapUpdateModel(requestModel); + Attempt result = await _memberTypeEditingService.UpdateAsync(memberType, model, CurrentUserKey(_backOfficeSecurityAccessor)); + + return result.Success + ? Ok() + : OperationStatusResult(result.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/MemberBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/MemberBuilderExtensions.cs new file mode 100644 index 000000000000..90ea476c189e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/MemberBuilderExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Mapping.Member; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Mapping; + +namespace Umbraco.Cms.Api.Management.DependencyInjection; + +internal static class MemberBuilderExtensions +{ + internal static IUmbracoBuilder AddMember(this IUmbracoBuilder builder) + { + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + builder.WithCollectionBuilder().Add(); + + return builder; + } +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/MemberTypeBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/MemberTypeBuilderExtensions.cs new file mode 100644 index 000000000000..2cd581fbca22 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/MemberTypeBuilderExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Mapping.MemberType; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Mapping; + +namespace Umbraco.Cms.Api.Management.DependencyInjection; + +internal static class MemberTypeBuilderExtensions +{ + internal static IUmbracoBuilder AddMemberTypes(this IUmbracoBuilder builder) + { + builder.Services.AddTransient(); + + builder.WithCollectionBuilder().Add(); + builder.WithCollectionBuilder().Add(); + + return builder; + } +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs index 0e3bb6d0fa39..5a2f10fb168d 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs @@ -30,6 +30,8 @@ public static IUmbracoBuilder AddUmbracoManagementApi(this IUmbracoBuilder build .AddDocumentTypes() .AddMedia() .AddMediaTypes() + .AddMember() + .AddMemberTypes() .AddLanguages() .AddDictionary() .AddHealthChecks() diff --git a/src/Umbraco.Cms.Api.Management/Factories/IMemberEditingPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IMemberEditingPresentationFactory.cs new file mode 100644 index 000000000000..2d0fa0bce7f7 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/IMemberEditingPresentationFactory.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Api.Management.ViewModels.Member; +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.Api.Management.Factories; + +public interface IMemberEditingPresentationFactory +{ + MemberCreateModel MapCreateModel(CreateMemberRequestModel createRequestModel); + + MemberUpdateModel MapUpdateModel(UpdateMemberRequestModel updateRequestModel); +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/IMemberPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IMemberPresentationFactory.cs new file mode 100644 index 000000000000..68d21329843d --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/IMemberPresentationFactory.cs @@ -0,0 +1,19 @@ +using Umbraco.Cms.Api.Management.ViewModels.Content; +using Umbraco.Cms.Api.Management.ViewModels.Member; +using Umbraco.Cms.Api.Management.ViewModels.Member.Item; +using Umbraco.Cms.Api.Management.ViewModels.MemberType; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; + +namespace Umbraco.Cms.Api.Management.Factories; + +public interface IMemberPresentationFactory +{ + Task CreateResponseModelAsync(IMember Member); + + MemberItemResponseModel CreateItemResponseModel(IMemberEntitySlim entity); + + IEnumerable CreateVariantsItemResponseModels(IMemberEntitySlim entity); + + MemberTypeReferenceResponseModel CreateMemberTypeReferenceResponseModel(IMemberEntitySlim entity); +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/IMemberTypeEditingPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IMemberTypeEditingPresentationFactory.cs new file mode 100644 index 000000000000..62304a9071a0 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/IMemberTypeEditingPresentationFactory.cs @@ -0,0 +1,14 @@ +using Umbraco.Cms.Api.Management.ViewModels.MemberType; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentTypeEditing; + +namespace Umbraco.Cms.Api.Management.Factories; + +public interface IMemberTypeEditingPresentationFactory +{ + MemberTypeCreateModel MapCreateModel(CreateMemberTypeRequestModel requestModel); + + MemberTypeUpdateModel MapUpdateModel(UpdateMemberTypeRequestModel requestModel); + + IEnumerable MapCompositionModels(IEnumerable compositionResults); +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/MemberEditingPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/MemberEditingPresentationFactory.cs new file mode 100644 index 000000000000..8547e1e57bca --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/MemberEditingPresentationFactory.cs @@ -0,0 +1,38 @@ +using Umbraco.Cms.Api.Management.ViewModels.Member; +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.Api.Management.Factories; + +internal sealed class MemberEditingPresentationFactory : ContentEditingPresentationFactory, IMemberEditingPresentationFactory +{ + public MemberCreateModel MapCreateModel(CreateMemberRequestModel createRequestModel) + { + MemberCreateModel model = MapContentEditingModel(createRequestModel); + + model.Key = createRequestModel.Id; + model.ContentTypeKey = createRequestModel.MemberType.Id; + model.IsApproved = createRequestModel.IsApproved; + model.Email = createRequestModel.Email; + model.Username = createRequestModel.Username; + model.Password = createRequestModel.Password; + model.Roles = createRequestModel.Groups; + + return model; + } + + public MemberUpdateModel MapUpdateModel(UpdateMemberRequestModel updateRequestModel) + { + MemberUpdateModel model = MapContentEditingModel(updateRequestModel); + + model.IsApproved = updateRequestModel.IsApproved; + model.IsLockedOut = updateRequestModel.IsLockedOut; + model.IsTwoFactorEnabled = updateRequestModel.IsTwoFactorEnabled; + model.OldPassword = updateRequestModel.OldPassword; + model.NewPassword = updateRequestModel.NewPassword; + model.Email = updateRequestModel.Email; + model.Username = updateRequestModel.Username; + model.Roles = updateRequestModel.Groups; + + return model; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/MemberPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/MemberPresentationFactory.cs new file mode 100644 index 000000000000..3f8d9435530c --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/MemberPresentationFactory.cs @@ -0,0 +1,74 @@ +using Umbraco.Cms.Api.Management.ViewModels.Content; +using Umbraco.Cms.Api.Management.ViewModels.Member; +using Umbraco.Cms.Api.Management.ViewModels.Member.Item; +using Umbraco.Cms.Api.Management.ViewModels.MemberType; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Factories; + +internal sealed class MemberPresentationFactory + : ContentPresentationFactoryBase, IMemberPresentationFactory +{ + private readonly IUmbracoMapper _umbracoMapper; + private readonly IMemberService _memberService; + private readonly IMemberTypeService _memberTypeService; + private readonly ITwoFactorLoginService _twoFactorLoginService; + + public MemberPresentationFactory( + IUmbracoMapper umbracoMapper, + IMemberService memberService, + IMemberTypeService memberTypeService, + ITwoFactorLoginService twoFactorLoginService) + : base(memberTypeService, umbracoMapper) + { + _umbracoMapper = umbracoMapper; + _memberService = memberService; + _memberTypeService = memberTypeService; + _twoFactorLoginService = twoFactorLoginService; + } + + public async Task CreateResponseModelAsync(IMember member) + { + MemberResponseModel responseModel = _umbracoMapper.Map(member)!; + + responseModel.IsTwoFactorEnabled = await _twoFactorLoginService.IsTwoFactorEnabledAsync(member.Key); + responseModel.Groups = _memberService.GetAllRoles(member.Username); + + return responseModel; + } + + public MemberItemResponseModel CreateItemResponseModel(IMemberEntitySlim entity) + { + var responseModel = new MemberItemResponseModel + { + Id = entity.Key, + }; + + IMemberType? memberType = _memberTypeService.Get(entity.ContentTypeAlias); + if (memberType is not null) + { + responseModel.MemberType = _umbracoMapper.Map(memberType)!; + } + + // TODO: does this make sense? + responseModel.Variants = CreateVariantsItemResponseModels(entity); + + return responseModel; + } + + public IEnumerable CreateVariantsItemResponseModels(IMemberEntitySlim entity) + => new[] + { + new VariantItemResponseModel + { + Name = entity.Name ?? string.Empty, + Culture = null + } + }; + + public MemberTypeReferenceResponseModel CreateMemberTypeReferenceResponseModel(IMemberEntitySlim entity) + => CreateContentTypeReferenceResponseModel(entity); +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/MemberTypeEditingPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/MemberTypeEditingPresentationFactory.cs new file mode 100644 index 000000000000..7f4e391b460c --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/MemberTypeEditingPresentationFactory.cs @@ -0,0 +1,53 @@ +using Umbraco.Cms.Api.Management.ViewModels.MemberType; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Factories; + +internal sealed class MemberTypeEditingPresentationFactory : ContentTypeEditingPresentationFactory, IMemberTypeEditingPresentationFactory +{ + public MemberTypeEditingPresentationFactory(IMemberTypeService memberTypeService) + : base(memberTypeService) + { + } + + public MemberTypeCreateModel MapCreateModel(CreateMemberTypeRequestModel requestModel) + { + MemberTypeCreateModel createModel = MapContentTypeEditingModel< + MemberTypeCreateModel, + MemberTypePropertyTypeModel, + MemberTypePropertyContainerModel, + CreateMemberTypePropertyTypeRequestModel, + CreateMemberTypePropertyTypeContainerRequestModel + >(requestModel); + + createModel.Key = requestModel.Id; + createModel.Compositions = MapCompositions(requestModel.Compositions); + + return createModel; + } + + public MemberTypeUpdateModel MapUpdateModel(UpdateMemberTypeRequestModel requestModel) + { + MemberTypeUpdateModel updateModel = MapContentTypeEditingModel< + MemberTypeUpdateModel, + MemberTypePropertyTypeModel, + MemberTypePropertyContainerModel, + UpdateMemberTypePropertyTypeRequestModel, + UpdateMemberTypePropertyTypeContainerRequestModel + >(requestModel); + + updateModel.Compositions = MapCompositions(requestModel.Compositions); + + return updateModel; + } + + public IEnumerable MapCompositionModels(IEnumerable compositionResults) + => compositionResults.Select(MapCompositionModel); + + private IEnumerable MapCompositions(IEnumerable documentTypeCompositions) + => MapCompositions(documentTypeCompositions + .DistinctBy(c => c.MemberType.Id) + .ToDictionary(c => c.MemberType.Id, c => c.CompositionType)); +} diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Item/ItemTypeMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Item/ItemTypeMapDefinition.cs index 9328f1a3f892..78dd870217c8 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Item/ItemTypeMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Item/ItemTypeMapDefinition.cs @@ -30,7 +30,6 @@ public void DefineMaps(IUmbracoMapper mapper) mapper.Define((_, _) => new TemplateItemResponseModel { Alias = string.Empty }, Map); mapper.Define((_, _) => new MemberTypeItemResponseModel(), Map); mapper.Define((_, _) => new RelationTypeItemResponseModel(), Map); - mapper.Define((_, _) => new MemberItemResponseModel(), Map); mapper.Define((_, _) => new UserItemResponseModel(), Map); mapper.Define((_, _) => new UserGroupItemResponseModel(), Map); } @@ -104,14 +103,6 @@ private static void Map(IRelationType source, RelationTypeItemResponseModel targ target.Name = source.Name ?? string.Empty; } - // Umbraco.Code.MapAll - private static void Map(IMember source, MemberItemResponseModel target, MapperContext context) - { - target.Icon = source.ContentType.Icon; - target.Id = source.Key; - target.Name = source.Name ?? source.Username; - } - // Umbraco.Code.MapAll private static void Map(IUser source, UserItemResponseModel target, MapperContext context) { diff --git a/src/Umbraco.Cms.Api.Management/Mapping/MediaType/MediaTypeCompositionMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/MediaType/MediaTypeCompositionMapDefinition.cs index 962468649ad5..a72f4b094c89 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/MediaType/MediaTypeCompositionMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/MediaType/MediaTypeCompositionMapDefinition.cs @@ -7,11 +7,11 @@ namespace Umbraco.Cms.Api.Management.Mapping.MediaType; public class MediaTypeCompositionMapDefinition : IMapDefinition { public void DefineMaps(IUmbracoMapper mapper) - => mapper.Define( + => mapper.Define( (_, _) => new MediaTypeCompositionResponseModel(), Map); // Umbraco.Code.MapAll - private static void Map(IContentType source, MediaTypeCompositionResponseModel target, MapperContext context) + private static void Map(IMediaType source, MediaTypeCompositionResponseModel target, MapperContext context) { target.Id = source.Key; target.Name = source.Name ?? string.Empty; diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Member/MemberMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Member/MemberMapDefinition.cs new file mode 100644 index 000000000000..0bee4c2c9ffa --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Mapping/Member/MemberMapDefinition.cs @@ -0,0 +1,37 @@ +using Umbraco.Cms.Api.Management.Mapping.Content; +using Umbraco.Cms.Api.Management.ViewModels.Member; +using Umbraco.Cms.Api.Management.ViewModels.MemberType; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.PropertyEditors; + +namespace Umbraco.Cms.Api.Management.Mapping.Member; + +public class MemberMapDefinition : ContentMapDefinition, IMapDefinition +{ + public MemberMapDefinition(PropertyEditorCollection propertyEditorCollection) + : base(propertyEditorCollection) + { + } + + public void DefineMaps(IUmbracoMapper mapper) + => mapper.Define((_, _) => new MemberResponseModel(), Map); + + // Umbraco.Code.MapAll -IsTwoFactorEnabled -Groups + private void Map(IMember source, MemberResponseModel target, MapperContext context) + { + target.Id = source.Key; + target.MemberType = context.Map(source.ContentType)!; + target.Values = MapValueViewModels(source); + target.Variants = MapVariantViewModels(source); + + target.IsApproved = source.IsApproved; + target.IsLockedOut = source.IsLockedOut; + target.Email = source.Email; + target.Username = source.Username; + target.FailedPasswordAttempts = source.FailedPasswordAttempts; + target.LastLoginDate = source.LastLoginDate; + target.LastLockoutDate = source.LastLockoutDate; + target.LastPasswordChangeDate = source.LastPasswordChangeDate; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Mapping/MemberType/MemberTypeCompositionMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/MemberType/MemberTypeCompositionMapDefinition.cs new file mode 100644 index 000000000000..df7fba102d49 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Mapping/MemberType/MemberTypeCompositionMapDefinition.cs @@ -0,0 +1,20 @@ +using Umbraco.Cms.Api.Management.ViewModels.MemberType; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Api.Management.Mapping.MemberType; + +public class MemberTypeCompositionMapDefinition : IMapDefinition +{ + public void DefineMaps(IUmbracoMapper mapper) + => mapper.Define( + (_, _) => new MemberTypeCompositionResponseModel(), Map); + + // Umbraco.Code.MapAll + private static void Map(IMemberType source, MemberTypeCompositionResponseModel target, MapperContext context) + { + target.Id = source.Key; + target.Name = source.Name ?? string.Empty; + target.Icon = source.Icon ?? string.Empty; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Mapping/MemberType/MemberTypeMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/MemberType/MemberTypeMapDefinition.cs new file mode 100644 index 000000000000..205c2a43cd8c --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Mapping/MemberType/MemberTypeMapDefinition.cs @@ -0,0 +1,55 @@ +using Umbraco.Cms.Api.Management.Mapping.ContentType; +using Umbraco.Cms.Api.Management.ViewModels; +using Umbraco.Cms.Api.Management.ViewModels.MemberType; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Mapping.MemberType; + +public class MemberTypeMapDefinition : ContentTypeMapDefinition, IMapDefinition +{ + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((_, _) => new MemberTypeResponseModel(), Map); + mapper.Define((_, _) => new MemberTypeReferenceResponseModel(), Map); + mapper.Define((_, _) => new MemberTypeReferenceResponseModel(), Map); + } + + // Umbraco.Code.MapAll + private void Map(IMemberType source, MemberTypeResponseModel target, MapperContext context) + { + target.Id = source.Key; + target.Alias = source.Alias; + target.Name = source.Name ?? string.Empty; + target.Description = source.Description; + target.Icon = source.Icon ?? string.Empty; + target.AllowedAsRoot = source.AllowedAsRoot; + target.VariesByCulture = source.VariesByCulture(); + target.VariesBySegment = source.VariesBySegment(); + target.IsElement = source.IsElement; + target.Containers = MapPropertyTypeContainers(source); + target.Properties = MapPropertyTypes(source); + target.Compositions = source.ContentTypeComposition.Select(contentTypeComposition => new MemberTypeComposition + { + MemberType = new ReferenceByIdModel(contentTypeComposition.Key), + CompositionType = CalculateCompositionType(source, contentTypeComposition) + }).ToArray(); + } + + // Umbraco.Code.MapAll + private void Map(IMemberType source, MemberTypeReferenceResponseModel target, MapperContext context) + { + target.Id = source.Key; + target.Icon = source.Icon ?? string.Empty; + target.HasListView = source.IsContainer; + } + + // Umbraco.Code.MapAll + private void Map(ISimpleContentType source, MemberTypeReferenceResponseModel target, MapperContext context) + { + target.Id = source.Key; + target.Icon = source.Icon ?? string.Empty; + target.HasListView = source.IsContainer; + } +} diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 36ee269ddb97..dd11ee6edb52 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -11590,64 +11590,1113 @@ ] } }, - "/umbraco/management/api/v1/member-type/item": { + "/umbraco/management/api/v1/member-type": { + "post": { + "tags": [ + "Member Type" + ], + "operationId": "PostMemberType", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateMemberTypeRequestModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateMemberTypeRequestModel" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateMemberTypeRequestModel" + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "headers": { + "Umb-Generated-Resource": { + "description": "Identifier of the newly created resource", + "schema": { + "type": "string", + "description": "Identifier of the newly created resource" + } + }, + "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/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, + "/umbraco/management/api/v1/member-type/{id}": { "get": { "tags": [ "Member Type" ], - "operationId": "GetMemberTypeItem", + "operationId": "GetMemberTypeById", "parameters": [ { "name": "id", - "in": "query", + "in": "path", + "required": true, "schema": { - "uniqueItems": true, - "type": "array", - "items": { - "type": "string", - "format": "uuid" + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberTypeResponseModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberTypeResponseModel" + } + ] + } + }, + "text/plain": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberTypeResponseModel" + } + ] + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + }, + "delete": { + "tags": [ + "Member Type" + ], + "operationId": "DeleteMemberTypeById", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success" + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + }, + "put": { + "tags": [ + "Member Type" + ], + "operationId": "PutMemberTypeById", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateMemberTypeRequestModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateMemberTypeRequestModel" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateMemberTypeRequestModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, + "/umbraco/management/api/v1/member-type/{id}/composition-references": { + "get": { + "tags": [ + "Member Type" + ], + "operationId": "GetMemberTypeByIdCompositionReferences", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberTypeCompositionResponseModel" + } + ] + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberTypeCompositionResponseModel" + } + ] + } + } + }, + "text/plain": { + "schema": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberTypeCompositionResponseModel" + } + ] + } + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, + "/umbraco/management/api/v1/member-type/{id}/copy": { + "post": { + "tags": [ + "Member Type" + ], + "operationId": "PostMemberTypeByIdCopy", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "201": { + "description": "Created", + "headers": { + "Umb-Generated-Resource": { + "description": "Identifier of the newly created resource", + "schema": { + "type": "string", + "description": "Identifier of the newly created resource" + } + }, + "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/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, + "/umbraco/management/api/v1/member-type/available-compositions": { + "post": { + "tags": [ + "Member Type" + ], + "operationId": "PostMemberTypeAvailableCompositions", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberTypeCompositionRequestModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberTypeCompositionRequestModel" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberTypeCompositionRequestModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/AvailableMemberTypeCompositionResponseModel" + } + ] + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/AvailableMemberTypeCompositionResponseModel" + } + ] + } + } + }, + "text/plain": { + "schema": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/AvailableMemberTypeCompositionResponseModel" + } + ] + } + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, + "/umbraco/management/api/v1/member-type/item": { + "get": { + "tags": [ + "Member Type" + ], + "operationId": "GetMemberTypeItem", + "parameters": [ + { + "name": "id", + "in": "query", + "schema": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberTypeItemResponseModel" + } + ] + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberTypeItemResponseModel" + } + ] + } + } + }, + "text/plain": { + "schema": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberTypeItemResponseModel" + } + ] + } + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, + "/umbraco/management/api/v1/tree/member-type/root": { + "get": { + "tags": [ + "Member Type" + ], + "operationId": "GetTreeMemberTypeRoot", + "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/PagedNamedEntityTreeItemResponseModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/PagedNamedEntityTreeItemResponseModel" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/PagedNamedEntityTreeItemResponseModel" + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, + "/umbraco/management/api/v1/member": { + "post": { + "tags": [ + "Member" + ], + "operationId": "PostMember", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateMemberRequestModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateMemberRequestModel" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateMemberRequestModel" + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "headers": { + "Umb-Generated-Resource": { + "description": "Identifier of the newly created resource", + "schema": { + "type": "string", + "description": "Identifier of the newly created resource" + } + }, + "Location": { + "description": "Location of the newly created resource", + "schema": { + "type": "string", + "description": "Location of the newly created resource", + "format": "uri" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, + "/umbraco/management/api/v1/member/{id}": { + "get": { + "tags": [ + "Member" + ], + "operationId": "GetMemberById", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberResponseModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberResponseModel" + } + ] + } + }, + "text/plain": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberResponseModel" + } + ] + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + }, + "delete": { + "tags": [ + "Member" + ], + "operationId": "DeleteMemberById", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + }, + "put": { + "tags": [ + "Member" + ], + "operationId": "PutMemberById", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateMemberRequestModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateMemberRequestModel" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateMemberRequestModel" + } + ] } } } - ], + }, "responses": { "200": { - "description": "Success", + "description": "Success" + }, + "400": { + "description": "Bad Request", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/MemberTypeItemResponseModel" - } - ] - } + "$ref": "#/components/schemas/ProblemDetails" } }, "text/json": { "schema": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/MemberTypeItemResponseModel" - } - ] - } + "$ref": "#/components/schemas/ProblemDetails" } }, "text/plain": { "schema": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/MemberTypeItemResponseModel" - } - ] - } + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" } } } @@ -11663,49 +12712,94 @@ ] } }, - "/umbraco/management/api/v1/tree/member-type/root": { - "get": { + "/umbraco/management/api/v1/member/{id}/validate": { + "put": { "tags": [ - "Member Type" + "Member" ], - "operationId": "GetTreeMemberTypeRoot", + "operationId": "PutMemberByIdValidate", "parameters": [ { - "name": "skip", - "in": "query", - "schema": { - "type": "integer", - "format": "int32", - "default": 0 - } - }, - { - "name": "take", - "in": "query", + "name": "id", + "in": "path", + "required": true, "schema": { - "type": "integer", - "format": "int32", - "default": 100 + "type": "string", + "format": "uuid" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateMemberRequestModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateMemberRequestModel" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateMemberRequestModel" + } + ] + } + } + } + }, "responses": { "200": { - "description": "Success", + "description": "Success" + }, + "400": { + "description": "Bad Request", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PagedNamedEntityTreeItemResponseModel" + "$ref": "#/components/schemas/ProblemDetails" } }, "text/json": { "schema": { - "$ref": "#/components/schemas/PagedNamedEntityTreeItemResponseModel" + "$ref": "#/components/schemas/ProblemDetails" } }, "text/plain": { "schema": { - "$ref": "#/components/schemas/PagedNamedEntityTreeItemResponseModel" + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" } } } @@ -11739,46 +12833,138 @@ "format": "uuid" } } - } - ], - "responses": { - "200": { - "description": "Success", + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberItemResponseModel" + } + ] + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberItemResponseModel" + } + ] + } + } + }, + "text/plain": { + "schema": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberItemResponseModel" + } + ] + } + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, + "/umbraco/management/api/v1/member/validate": { + "post": { + "tags": [ + "Member" + ], + "operationId": "PostMemberValidate", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateMemberRequestModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateMemberRequestModel" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateMemberRequestModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/MemberItemResponseModel" - } - ] - } + "$ref": "#/components/schemas/ProblemDetails" } }, "text/json": { "schema": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/MemberItemResponseModel" - } - ] - } + "$ref": "#/components/schemas/ProblemDetails" } }, "text/plain": { "schema": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/MemberItemResponseModel" - } - ] - } + "$ref": "#/components/schemas/ProblemDetails" } } } @@ -22829,6 +24015,15 @@ ], "additionalProperties": false }, + "AvailableMemberTypeCompositionResponseModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AvailableContentTypeCompositionResponseModelBaseModel" + } + ], + "additionalProperties": false + }, "ChangePasswordUserRequestModel": { "required": [ "newPassword" @@ -22939,6 +24134,41 @@ }, "additionalProperties": false }, + "ContentForMemberResponseModel": { + "required": [ + "id", + "values", + "variants" + ], + "type": "object", + "properties": { + "values": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberValueModel" + } + ] + } + }, + "variants": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberVariantResponseModel" + } + ] + } + }, + "id": { + "type": "string", + "format": "uuid" + } + }, + "additionalProperties": false + }, "ContentStateModel": { "enum": [ "NotCreated", @@ -23185,6 +24415,76 @@ }, "additionalProperties": false }, + "ContentTypeForMemberTypeResponseModel": { + "required": [ + "alias", + "allowedAsRoot", + "containers", + "icon", + "id", + "isElement", + "name", + "properties", + "variesByCulture", + "variesBySegment" + ], + "type": "object", + "properties": { + "alias": { + "minLength": 1, + "type": "string" + }, + "name": { + "minLength": 1, + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "icon": { + "minLength": 1, + "type": "string" + }, + "allowedAsRoot": { + "type": "boolean" + }, + "variesByCulture": { + "type": "boolean" + }, + "variesBySegment": { + "type": "boolean" + }, + "isElement": { + "type": "boolean" + }, + "properties": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberTypePropertyTypeResponseModel" + } + ] + } + }, + "containers": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberTypePropertyTypeContainerResponseModel" + } + ] + } + }, + "id": { + "type": "string", + "format": "uuid" + } + }, + "additionalProperties": false + }, "ContentTypeReferenceResponseModelBaseModel": { "required": [ "hasListView", @@ -23350,7 +24650,120 @@ "items": { "oneOf": [ { - "$ref": "#/components/schemas/MediaVariantRequestModel" + "$ref": "#/components/schemas/MediaVariantRequestModel" + } + ] + } + }, + "id": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "parent": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ], + "nullable": true + } + }, + "additionalProperties": false + }, + "CreateContentForMemberRequestModel": { + "required": [ + "values", + "variants" + ], + "type": "object", + "properties": { + "values": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberValueModel" + } + ] + } + }, + "variants": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberVariantRequestModel" + } + ] + } + }, + "id": { + "type": "string", + "format": "uuid", + "nullable": true + } + }, + "additionalProperties": false + }, + "CreateContentTypeForDocumentTypeRequestModel": { + "required": [ + "alias", + "allowedAsRoot", + "containers", + "icon", + "isElement", + "name", + "properties", + "variesByCulture", + "variesBySegment" + ], + "type": "object", + "properties": { + "alias": { + "minLength": 1, + "type": "string" + }, + "name": { + "minLength": 1, + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "icon": { + "minLength": 1, + "type": "string" + }, + "allowedAsRoot": { + "type": "boolean" + }, + "variesByCulture": { + "type": "boolean" + }, + "variesBySegment": { + "type": "boolean" + }, + "isElement": { + "type": "boolean" + }, + "properties": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateDocumentTypePropertyTypeRequestModel" + } + ] + } + }, + "containers": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateDocumentTypePropertyTypeContainerRequestModel" } ] } @@ -23360,7 +24773,7 @@ "format": "uuid", "nullable": true }, - "parent": { + "folder": { "oneOf": [ { "$ref": "#/components/schemas/ReferenceByIdModel" @@ -23371,7 +24784,7 @@ }, "additionalProperties": false }, - "CreateContentTypeForDocumentTypeRequestModel": { + "CreateContentTypeForMediaTypeRequestModel": { "required": [ "alias", "allowedAsRoot", @@ -23418,7 +24831,7 @@ "items": { "oneOf": [ { - "$ref": "#/components/schemas/CreateDocumentTypePropertyTypeRequestModel" + "$ref": "#/components/schemas/CreateMediaTypePropertyTypeRequestModel" } ] } @@ -23428,7 +24841,7 @@ "items": { "oneOf": [ { - "$ref": "#/components/schemas/CreateDocumentTypePropertyTypeContainerRequestModel" + "$ref": "#/components/schemas/CreateMediaTypePropertyTypeContainerRequestModel" } ] } @@ -23449,7 +24862,7 @@ }, "additionalProperties": false }, - "CreateContentTypeForMediaTypeRequestModel": { + "CreateContentTypeForMemberTypeRequestModel": { "required": [ "alias", "allowedAsRoot", @@ -23496,7 +24909,7 @@ "items": { "oneOf": [ { - "$ref": "#/components/schemas/CreateMediaTypePropertyTypeRequestModel" + "$ref": "#/components/schemas/CreateMemberTypePropertyTypeRequestModel" } ] } @@ -23506,7 +24919,7 @@ "items": { "oneOf": [ { - "$ref": "#/components/schemas/CreateMediaTypePropertyTypeContainerRequestModel" + "$ref": "#/components/schemas/CreateMemberTypePropertyTypeContainerRequestModel" } ] } @@ -23515,14 +24928,6 @@ "type": "string", "format": "uuid", "nullable": true - }, - "folder": { - "oneOf": [ - { - "$ref": "#/components/schemas/ReferenceByIdModel" - } - ], - "nullable": true } }, "additionalProperties": false @@ -23808,6 +25213,92 @@ }, "additionalProperties": false }, + "CreateMemberRequestModel": { + "required": [ + "email", + "isApproved", + "memberType", + "password", + "username" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/CreateContentForMemberRequestModel" + } + ], + "properties": { + "email": { + "type": "string" + }, + "username": { + "type": "string" + }, + "password": { + "type": "string" + }, + "memberType": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ] + }, + "groups": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "isApproved": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "CreateMemberTypePropertyTypeContainerRequestModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/PropertyTypeContainerModelBaseModel" + } + ], + "additionalProperties": false + }, + "CreateMemberTypePropertyTypeRequestModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/PropertyTypeModelBaseModel" + } + ], + "additionalProperties": false + }, + "CreateMemberTypeRequestModel": { + "required": [ + "compositions" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/CreateContentTypeForMemberTypeRequestModel" + } + ], + "properties": { + "compositions": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberTypeCompositionModel" + } + ] + } + } + }, + "additionalProperties": false + }, "CreatePackageRequestModel": { "type": "object", "allOf": [ @@ -26380,121 +27871,322 @@ ] } }, - "compositions": { + "compositions": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/MediaTypeCompositionModel" + } + ] + } + } + }, + "additionalProperties": false + }, + "MediaTypeSortModel": { + "required": [ + "mediaType", + "sortOrder" + ], + "type": "object", + "properties": { + "mediaType": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ] + }, + "sortOrder": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "MediaTypeTreeItemResponseModel": { + "required": [ + "icon" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/FolderTreeItemResponseModel" + } + ], + "properties": { + "icon": { + "type": "string" + } + }, + "additionalProperties": false + }, + "MediaValueModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/ValueModelBaseModel" + } + ], + "additionalProperties": false + }, + "MediaVariantRequestModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/VariantModelBaseModel" + } + ], + "additionalProperties": false + }, + "MediaVariantResponseModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/VariantResponseModelBaseModel" + } + ], + "additionalProperties": false + }, + "MemberGroupItemResponseModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/NamedItemResponseModelBaseModel" + } + ], + "additionalProperties": false + }, + "MemberItemResponseModel": { + "required": [ + "memberType", + "variants" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/NamedItemResponseModelBaseModel" + } + ], + "properties": { + "memberType": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberTypeReferenceResponseModel" + } + ] + }, + "variants": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/VariantItemResponseModel" + } + ] + } + } + }, + "additionalProperties": false + }, + "MemberResponseModel": { + "required": [ + "email", + "failedPasswordAttempts", + "groups", + "isApproved", + "isLockedOut", + "isTwoFactorEnabled", + "memberType", + "username" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/ContentForMemberResponseModel" + } + ], + "properties": { + "email": { + "type": "string" + }, + "username": { + "type": "string" + }, + "memberType": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberTypeReferenceResponseModel" + } + ] + }, + "isApproved": { + "type": "boolean" + }, + "isLockedOut": { + "type": "boolean" + }, + "isTwoFactorEnabled": { + "type": "boolean" + }, + "failedPasswordAttempts": { + "type": "integer", + "format": "int32" + }, + "lastLoginDate": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "lastLockoutDate": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "lastPasswordChangeDate": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "groups": { "type": "array", "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/MediaTypeCompositionModel" - } - ] + "type": "string" } } }, "additionalProperties": false }, - "MediaTypeSortModel": { + "MemberTypeCompositionModel": { "required": [ - "mediaType", - "sortOrder" + "compositionType", + "memberType" ], "type": "object", "properties": { - "mediaType": { + "memberType": { "oneOf": [ { "$ref": "#/components/schemas/ReferenceByIdModel" } ] }, - "sortOrder": { - "type": "integer", - "format": "int32" + "compositionType": { + "$ref": "#/components/schemas/CompositionTypeModel" } }, "additionalProperties": false }, - "MediaTypeTreeItemResponseModel": { - "required": [ - "icon" + "MemberTypeCompositionRequestModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/ContentTypeCompositionRequestModelBaseModel" + } ], + "additionalProperties": false + }, + "MemberTypeCompositionResponseModel": { "type": "object", "allOf": [ { - "$ref": "#/components/schemas/FolderTreeItemResponseModel" + "$ref": "#/components/schemas/ContentTypeCompositionResponseModelBaseModel" + } + ], + "additionalProperties": false + }, + "MemberTypeItemResponseModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/NamedItemResponseModelBaseModel" } ], "properties": { "icon": { - "type": "string" + "type": "string", + "nullable": true } }, "additionalProperties": false }, - "MediaValueModel": { + "MemberTypePropertyTypeContainerResponseModel": { "type": "object", "allOf": [ { - "$ref": "#/components/schemas/ValueModelBaseModel" + "$ref": "#/components/schemas/PropertyTypeContainerModelBaseModel" } ], "additionalProperties": false }, - "MediaVariantRequestModel": { + "MemberTypePropertyTypeResponseModel": { "type": "object", "allOf": [ { - "$ref": "#/components/schemas/VariantModelBaseModel" + "$ref": "#/components/schemas/PropertyTypeModelBaseModel" } ], "additionalProperties": false }, - "MediaVariantResponseModel": { + "MemberTypeReferenceResponseModel": { "type": "object", "allOf": [ { - "$ref": "#/components/schemas/VariantResponseModelBaseModel" + "$ref": "#/components/schemas/ContentTypeReferenceResponseModelBaseModel" } ], "additionalProperties": false }, - "MemberGroupItemResponseModel": { + "MemberTypeResponseModel": { + "required": [ + "compositions" + ], "type": "object", "allOf": [ { - "$ref": "#/components/schemas/NamedItemResponseModelBaseModel" + "$ref": "#/components/schemas/ContentTypeForMemberTypeResponseModel" } ], + "properties": { + "compositions": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberTypeCompositionModel" + } + ] + } + } + }, "additionalProperties": false }, - "MemberItemResponseModel": { + "MemberValueModel": { "type": "object", "allOf": [ { - "$ref": "#/components/schemas/NamedItemResponseModelBaseModel" + "$ref": "#/components/schemas/ValueModelBaseModel" } ], - "properties": { - "icon": { - "type": "string", - "nullable": true - } - }, "additionalProperties": false }, - "MemberTypeItemResponseModel": { + "MemberVariantRequestModel": { "type": "object", "allOf": [ { - "$ref": "#/components/schemas/NamedItemResponseModelBaseModel" + "$ref": "#/components/schemas/VariantModelBaseModel" } ], - "properties": { - "icon": { - "type": "string", - "nullable": true + "additionalProperties": false + }, + "MemberVariantResponseModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/VariantResponseModelBaseModel" } - }, + ], "additionalProperties": false }, "ModelsBuilderResponseModel": { @@ -29384,6 +31076,36 @@ }, "additionalProperties": false }, + "UpdateContentForMemberRequestModel": { + "required": [ + "values", + "variants" + ], + "type": "object", + "properties": { + "values": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberValueModel" + } + ] + } + }, + "variants": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberVariantRequestModel" + } + ] + } + } + }, + "additionalProperties": false + }, "UpdateContentTypeForDocumentTypeRequestModel": { "required": [ "alias", @@ -29514,6 +31236,71 @@ }, "additionalProperties": false }, + "UpdateContentTypeForMemberTypeRequestModel": { + "required": [ + "alias", + "allowedAsRoot", + "containers", + "icon", + "isElement", + "name", + "properties", + "variesByCulture", + "variesBySegment" + ], + "type": "object", + "properties": { + "alias": { + "minLength": 1, + "type": "string" + }, + "name": { + "minLength": 1, + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "icon": { + "minLength": 1, + "type": "string" + }, + "allowedAsRoot": { + "type": "boolean" + }, + "variesByCulture": { + "type": "boolean" + }, + "variesBySegment": { + "type": "boolean" + }, + "isElement": { + "type": "boolean" + }, + "properties": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateMemberTypePropertyTypeRequestModel" + } + ] + } + }, + "containers": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateMemberTypePropertyTypeContainerRequestModel" + } + ] + } + } + }, + "additionalProperties": false + }, "UpdateDataTypeRequestModel": { "type": "object", "allOf": [ @@ -29735,6 +31522,96 @@ }, "additionalProperties": false }, + "UpdateMemberRequestModel": { + "required": [ + "email", + "isApproved", + "isLockedOut", + "isTwoFactorEnabled", + "username" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/UpdateContentForMemberRequestModel" + } + ], + "properties": { + "email": { + "type": "string" + }, + "username": { + "type": "string" + }, + "oldPassword": { + "type": "string", + "nullable": true + }, + "newPassword": { + "type": "string", + "nullable": true + }, + "groups": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "isApproved": { + "type": "boolean" + }, + "isLockedOut": { + "type": "boolean" + }, + "isTwoFactorEnabled": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "UpdateMemberTypePropertyTypeContainerRequestModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/PropertyTypeContainerModelBaseModel" + } + ], + "additionalProperties": false + }, + "UpdateMemberTypePropertyTypeRequestModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/PropertyTypeModelBaseModel" + } + ], + "additionalProperties": false + }, + "UpdateMemberTypeRequestModel": { + "required": [ + "compositions" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/UpdateContentTypeForMemberTypeRequestModel" + } + ], + "properties": { + "compositions": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberTypeCompositionModel" + } + ] + } + } + }, + "additionalProperties": false + }, "UpdatePackageRequestModel": { "required": [ "packagePath" diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Content/CreateContentRequestModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Content/CreateContentRequestModelBase.cs index 8083e68ba9ca..adddf02ba2a9 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Content/CreateContentRequestModelBase.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Content/CreateContentRequestModelBase.cs @@ -6,6 +6,4 @@ public abstract class CreateContentRequestModelBase where TVariantModel : VariantModelBase { public Guid? Id { get; set; } - - public ReferenceByIdModel? Parent { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Content/CreateContentWithParentRequestModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Content/CreateContentWithParentRequestModelBase.cs new file mode 100644 index 000000000000..bcabec7c97f2 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Content/CreateContentWithParentRequestModelBase.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Content; + +public abstract class CreateContentWithParentRequestModelBase + : CreateContentRequestModelBase + where TValueModel : ValueModelBase + where TVariantModel : VariantModelBase +{ + public ReferenceByIdModel? Parent { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/CreateContentTypeInFolderRequestModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/CreateContentTypeInFolderRequestModelBase.cs new file mode 100644 index 000000000000..0ebc930038bf --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/CreateContentTypeInFolderRequestModelBase.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.ContentType; + +public abstract class CreateContentTypeInFolderRequestModelBase + : CreateContentTypeRequestModelBase + where TPropertyType : PropertyTypeModelBase + where TPropertyTypeContainer : PropertyTypeContainerModelBase +{ + public ReferenceByIdModel? Folder { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/CreateContentTypeRequestModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/CreateContentTypeRequestModelBase.cs index 000f78190c2b..9fe4f18ecbbc 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/CreateContentTypeRequestModelBase.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/CreateContentTypeRequestModelBase.cs @@ -6,6 +6,4 @@ public abstract class CreateContentTypeRequestModelBase("CreateContentForDocumentRequestModel")] -public class CreateDocumentRequestModel : CreateContentRequestModelBase +public class CreateDocumentRequestModel : CreateContentWithParentRequestModelBase { public required ReferenceByIdModel DocumentType { get; set; } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/CreateDocumentTypeRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/CreateDocumentTypeRequestModel.cs index 0490e0fbb95b..488b210ab374 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/CreateDocumentTypeRequestModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/CreateDocumentTypeRequestModel.cs @@ -5,7 +5,7 @@ namespace Umbraco.Cms.Api.Management.ViewModels.DocumentType; [ShortGenericSchemaName("CreateContentTypeForDocumentTypeRequestModel")] public class CreateDocumentTypeRequestModel - : CreateContentTypeRequestModelBase + : CreateContentTypeInFolderRequestModelBase { public IEnumerable AllowedTemplates { get; set; } = Enumerable.Empty(); diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Media/CreateMediaRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Media/CreateMediaRequestModel.cs index 1d55a790775f..66acfb026026 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Media/CreateMediaRequestModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Media/CreateMediaRequestModel.cs @@ -4,7 +4,7 @@ namespace Umbraco.Cms.Api.Management.ViewModels.Media; [ShortGenericSchemaName("CreateContentForMediaRequestModel")] -public class CreateMediaRequestModel : CreateContentRequestModelBase +public class CreateMediaRequestModel : CreateContentWithParentRequestModelBase { public required ReferenceByIdModel MediaType { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/CreateMediaTypeRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/CreateMediaTypeRequestModel.cs index edb7b7cf03e0..562c17646986 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/CreateMediaTypeRequestModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/CreateMediaTypeRequestModel.cs @@ -5,7 +5,7 @@ namespace Umbraco.Cms.Api.Management.ViewModels.MediaType; [ShortGenericSchemaName("CreateContentTypeForMediaTypeRequestModel")] public class CreateMediaTypeRequestModel - : CreateContentTypeRequestModelBase + : CreateContentTypeInFolderRequestModelBase { public IEnumerable AllowedMediaTypes { get; set; } = Enumerable.Empty(); diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Member/CreateMemberRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Member/CreateMemberRequestModel.cs new file mode 100644 index 000000000000..c8def61a2627 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Member/CreateMemberRequestModel.cs @@ -0,0 +1,20 @@ +using Umbraco.Cms.Api.Common.Attributes; +using Umbraco.Cms.Api.Management.ViewModels.Content; + +namespace Umbraco.Cms.Api.Management.ViewModels.Member; + +[ShortGenericSchemaName("CreateContentForMemberRequestModel")] +public class CreateMemberRequestModel : CreateContentRequestModelBase +{ + public string Email { get; set; } = string.Empty; + + public string Username { get; set; } = string.Empty; + + public string Password { get; set; } = string.Empty; + + public required ReferenceByIdModel MemberType { get; set; } + + public IEnumerable? Groups { get; set; } + + public bool IsApproved { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Member/Item/MemberItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Member/Item/MemberItemResponseModel.cs index fe7cfb9fff5e..d8847e34fb97 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Member/Item/MemberItemResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Member/Item/MemberItemResponseModel.cs @@ -1,8 +1,12 @@ -using Umbraco.Cms.Api.Management.ViewModels.Item; +using Umbraco.Cms.Api.Management.ViewModels.Content; +using Umbraco.Cms.Api.Management.ViewModels.Item; +using Umbraco.Cms.Api.Management.ViewModels.MemberType; namespace Umbraco.Cms.Api.Management.ViewModels.Member.Item; public class MemberItemResponseModel : NamedItemResponseModelBase { - public string? Icon { get; set; } + public MemberTypeReferenceResponseModel MemberType { get; set; } = new(); + + public IEnumerable Variants { get; set; } = Enumerable.Empty(); } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberResponseModel.cs new file mode 100644 index 000000000000..5fb6f2fb55e3 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberResponseModel.cs @@ -0,0 +1,31 @@ +using Umbraco.Cms.Api.Common.Attributes; +using Umbraco.Cms.Api.Management.ViewModels.Content; +using Umbraco.Cms.Api.Management.ViewModels.MemberType; + +namespace Umbraco.Cms.Api.Management.ViewModels.Member; + +[ShortGenericSchemaName("ContentForMemberResponseModel")] +public class MemberResponseModel : ContentResponseModelBase +{ + public string Email { get; set; } = string.Empty; + + public string Username { get; set; } = string.Empty; + + public MemberTypeReferenceResponseModel MemberType { get; set; } = new(); + + public bool IsApproved { get; set; } + + public bool IsLockedOut { get; set; } + + public bool IsTwoFactorEnabled { get; set; } + + public int FailedPasswordAttempts { get; set; } + + public DateTime? LastLoginDate { get; set; } + + public DateTime? LastLockoutDate { get; set; } + + public DateTime? LastPasswordChangeDate { get; set; } + + public IEnumerable Groups { get; set; } = []; +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberValueModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberValueModel.cs new file mode 100644 index 000000000000..bd3fe1fefa18 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberValueModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.Content; + +namespace Umbraco.Cms.Api.Management.ViewModels.Member; + +public class MemberValueModel : ValueModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberVariantRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberVariantRequestModel.cs new file mode 100644 index 000000000000..bf629db57443 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberVariantRequestModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.Content; + +namespace Umbraco.Cms.Api.Management.ViewModels.Member; + +public class MemberVariantRequestModel : VariantModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberVariantResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberVariantResponseModel.cs new file mode 100644 index 000000000000..b9be1dadd961 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberVariantResponseModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.Content; + +namespace Umbraco.Cms.Api.Management.ViewModels.Member; + +public class MemberVariantResponseModel : VariantResponseModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Member/UpdateMemberRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Member/UpdateMemberRequestModel.cs new file mode 100644 index 000000000000..a31c3fe35f15 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Member/UpdateMemberRequestModel.cs @@ -0,0 +1,24 @@ +using Umbraco.Cms.Api.Common.Attributes; +using Umbraco.Cms.Api.Management.ViewModels.Content; + +namespace Umbraco.Cms.Api.Management.ViewModels.Member; + +[ShortGenericSchemaName("UpdateContentForMemberRequestModel")] +public class UpdateMemberRequestModel : UpdateContentRequestModelBase +{ + public string Email { get; set; } = string.Empty; + + public string Username { get; set; } = string.Empty; + + public string? OldPassword { get; set; } + + public string? NewPassword { get; set; } + + public IEnumerable? Groups { get; set; } + + public bool IsApproved { get; set; } + + public bool IsLockedOut { get; set; } + + public bool IsTwoFactorEnabled { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/AvailableMemberTypeCompositionResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/AvailableMemberTypeCompositionResponseModel.cs new file mode 100644 index 000000000000..f306fed0fab8 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/AvailableMemberTypeCompositionResponseModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.ContentType; + +namespace Umbraco.Cms.Api.Management.ViewModels.MemberType; + +public class AvailableMemberTypeCompositionResponseModel : AvailableContentTypeCompositionResponseModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/CreateMediaTypePropertyTypeContainerRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/CreateMediaTypePropertyTypeContainerRequestModel.cs new file mode 100644 index 000000000000..8885b2b0fd2e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/CreateMediaTypePropertyTypeContainerRequestModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.ContentType; + +namespace Umbraco.Cms.Api.Management.ViewModels.MemberType; + +public class CreateMemberTypePropertyTypeContainerRequestModel : PropertyTypeContainerModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/CreateMemberTypePropertyTypeRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/CreateMemberTypePropertyTypeRequestModel.cs new file mode 100644 index 000000000000..66828c040e32 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/CreateMemberTypePropertyTypeRequestModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.ContentType; + +namespace Umbraco.Cms.Api.Management.ViewModels.MemberType; + +public class CreateMemberTypePropertyTypeRequestModel : PropertyTypeModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/CreateMemberTypeRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/CreateMemberTypeRequestModel.cs new file mode 100644 index 000000000000..eacee7cf4b36 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/CreateMemberTypeRequestModel.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Api.Common.Attributes; +using Umbraco.Cms.Api.Management.ViewModels.ContentType; + +namespace Umbraco.Cms.Api.Management.ViewModels.MemberType; + +[ShortGenericSchemaName("CreateContentTypeForMemberTypeRequestModel")] +public class CreateMemberTypeRequestModel + : CreateContentTypeRequestModelBase +{ + public IEnumerable Compositions { get; set; } = Enumerable.Empty(); +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/MemberTypeComposition.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/MemberTypeComposition.cs new file mode 100644 index 000000000000..b97016d0d1c5 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/MemberTypeComposition.cs @@ -0,0 +1,10 @@ +using Umbraco.Cms.Api.Management.ViewModels.ContentType; + +namespace Umbraco.Cms.Api.Management.ViewModels.MemberType; + +public class MemberTypeComposition +{ + public required ReferenceByIdModel MemberType { get; init; } + + public required CompositionType CompositionType { get; init; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/MemberTypeCompositionRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/MemberTypeCompositionRequestModel.cs new file mode 100644 index 000000000000..49d32755e3b4 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/MemberTypeCompositionRequestModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.ContentType; + +namespace Umbraco.Cms.Api.Management.ViewModels.MemberType; + +public class MemberTypeCompositionRequestModel : ContentTypeCompositionRequestModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/MemberTypeCompositionResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/MemberTypeCompositionResponseModel.cs new file mode 100644 index 000000000000..08b571922e66 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/MemberTypeCompositionResponseModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.ContentType; + +namespace Umbraco.Cms.Api.Management.ViewModels.MemberType; + +public class MemberTypeCompositionResponseModel : ContentTypeCompositionResponseModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/MemberTypePropertyTypeContainerResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/MemberTypePropertyTypeContainerResponseModel.cs new file mode 100644 index 000000000000..fb8f335baff5 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/MemberTypePropertyTypeContainerResponseModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.ContentType; + +namespace Umbraco.Cms.Api.Management.ViewModels.MemberType; + +public class MemberTypePropertyTypeContainerResponseModel : PropertyTypeContainerModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/MemberTypePropertyTypeResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/MemberTypePropertyTypeResponseModel.cs new file mode 100644 index 000000000000..5337423d81e2 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/MemberTypePropertyTypeResponseModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.ContentType; + +namespace Umbraco.Cms.Api.Management.ViewModels.MemberType; + +public class MemberTypePropertyTypeResponseModel : PropertyTypeModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/MemberTypeReferenceResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/MemberTypeReferenceResponseModel.cs new file mode 100644 index 000000000000..386089989446 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/MemberTypeReferenceResponseModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.ContentType; + +namespace Umbraco.Cms.Api.Management.ViewModels.MemberType; + +public class MemberTypeReferenceResponseModel : ContentTypeReferenceResponseModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/MemberTypeResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/MemberTypeResponseModel.cs new file mode 100644 index 000000000000..9ff1c8bac190 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/MemberTypeResponseModel.cs @@ -0,0 +1,10 @@ +using Umbraco.Cms.Api.Common.Attributes; +using Umbraco.Cms.Api.Management.ViewModels.ContentType; + +namespace Umbraco.Cms.Api.Management.ViewModels.MemberType; + +[ShortGenericSchemaName("ContentTypeForMemberTypeResponseModel")] +public class MemberTypeResponseModel : ContentTypeResponseModelBase +{ + public IEnumerable Compositions { get; set; } = Enumerable.Empty(); +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/UpdateMemberTypePropertyTypeContainerRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/UpdateMemberTypePropertyTypeContainerRequestModel.cs new file mode 100644 index 000000000000..e1638a53a180 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/UpdateMemberTypePropertyTypeContainerRequestModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.ContentType; + +namespace Umbraco.Cms.Api.Management.ViewModels.MemberType; + +public class UpdateMemberTypePropertyTypeContainerRequestModel : PropertyTypeContainerModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/UpdateMemberTypePropertyTypeRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/UpdateMemberTypePropertyTypeRequestModel.cs new file mode 100644 index 000000000000..4aec00389936 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/UpdateMemberTypePropertyTypeRequestModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.ContentType; + +namespace Umbraco.Cms.Api.Management.ViewModels.MemberType; + +public class UpdateMemberTypePropertyTypeRequestModel : PropertyTypeModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/UpdateMemberTypeRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/UpdateMemberTypeRequestModel.cs new file mode 100644 index 000000000000..647588234d7e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/UpdateMemberTypeRequestModel.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Api.Common.Attributes; +using Umbraco.Cms.Api.Management.ViewModels.ContentType; + +namespace Umbraco.Cms.Api.Management.ViewModels.MemberType; + +[ShortGenericSchemaName("UpdateContentTypeForMemberTypeRequestModel")] +public class UpdateMemberTypeRequestModel + : UpdateContentTypeRequestModelBase +{ + public IEnumerable Compositions { get; set; } = Enumerable.Empty(); +} diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 0f8b03a9c943..c517554c8f53 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -309,6 +309,7 @@ private void AddCoreServices() Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); + Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); @@ -331,6 +332,8 @@ private void AddCoreServices() Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberCreateModel.cs b/src/Umbraco.Core/Models/ContentEditing/MemberCreateModel.cs new file mode 100644 index 000000000000..0369d15af55b --- /dev/null +++ b/src/Umbraco.Core/Models/ContentEditing/MemberCreateModel.cs @@ -0,0 +1,11 @@ +namespace Umbraco.Cms.Core.Models.ContentEditing; + +public class MemberCreateModel : MemberEditingModelBase +{ + public string Password { get; set; } = string.Empty; + + public Guid? Key { get; set; } + + public Guid ContentTypeKey { get; set; } = Guid.Empty; +} + diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberCreateResult.cs b/src/Umbraco.Core/Models/ContentEditing/MemberCreateResult.cs new file mode 100644 index 000000000000..394f5b842c69 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentEditing/MemberCreateResult.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Models.ContentEditing; + +public class MemberCreateResult : ContentCreateResultBase +{ +} diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberEditingModelBase.cs b/src/Umbraco.Core/Models/ContentEditing/MemberEditingModelBase.cs new file mode 100644 index 000000000000..0a3020ecb31f --- /dev/null +++ b/src/Umbraco.Core/Models/ContentEditing/MemberEditingModelBase.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.Core.Models.ContentEditing; + +public abstract class MemberEditingModelBase : ContentEditingModelBase +{ + public bool IsApproved { get; set; } + + public IEnumerable? Roles { get; set; } + + public string Email { get; set; } = string.Empty; + + public string Username { get; set; } = string.Empty; +} diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberUpdateModel.cs b/src/Umbraco.Core/Models/ContentEditing/MemberUpdateModel.cs new file mode 100644 index 000000000000..555fbed928a5 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentEditing/MemberUpdateModel.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.Core.Models.ContentEditing; + +public class MemberUpdateModel : MemberEditingModelBase +{ + public bool IsLockedOut { get; set; } + + public bool IsTwoFactorEnabled { get; set; } + + public string? OldPassword { get; set; } + + public string? NewPassword { get; set; } +} diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberUpdateResult.cs b/src/Umbraco.Core/Models/ContentEditing/MemberUpdateResult.cs new file mode 100644 index 000000000000..8624275ad5f9 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentEditing/MemberUpdateResult.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Models.ContentEditing; + +public class MemberUpdateResult : ContentUpdateResultBase +{ +} diff --git a/src/Umbraco.Core/Models/ContentTypeEditing/MemberTypeCreateModel.cs b/src/Umbraco.Core/Models/ContentTypeEditing/MemberTypeCreateModel.cs new file mode 100644 index 000000000000..4193002a1932 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeEditing/MemberTypeCreateModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.Models.ContentTypeEditing; + +public class MemberTypeCreateModel : MemberTypeModelBase +{ + public Guid? Key { get; set; } +} diff --git a/src/Umbraco.Core/Models/ContentTypeEditing/MemberTypeModelBase.cs b/src/Umbraco.Core/Models/ContentTypeEditing/MemberTypeModelBase.cs new file mode 100644 index 000000000000..2a416bb9c1af --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeEditing/MemberTypeModelBase.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Models.ContentTypeEditing; + +public class MemberTypeModelBase : ContentTypeEditingModelBase +{ +} diff --git a/src/Umbraco.Core/Models/ContentTypeEditing/MemberTypePropertyContainerModel.cs b/src/Umbraco.Core/Models/ContentTypeEditing/MemberTypePropertyContainerModel.cs new file mode 100644 index 000000000000..67dc372ad714 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeEditing/MemberTypePropertyContainerModel.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Models.ContentTypeEditing; + +public class MemberTypePropertyContainerModel : PropertyTypeContainerModelBase +{ +} diff --git a/src/Umbraco.Core/Models/ContentTypeEditing/MemberTypePropertyTypeModel.cs b/src/Umbraco.Core/Models/ContentTypeEditing/MemberTypePropertyTypeModel.cs new file mode 100644 index 000000000000..db2462f94d54 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeEditing/MemberTypePropertyTypeModel.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Models.ContentTypeEditing; + +public class MemberTypePropertyTypeModel : PropertyTypeModelBase +{ +} diff --git a/src/Umbraco.Core/Models/ContentTypeEditing/MemberTypeUpdateModel.cs b/src/Umbraco.Core/Models/ContentTypeEditing/MemberTypeUpdateModel.cs new file mode 100644 index 000000000000..3d36a78ca7e6 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeEditing/MemberTypeUpdateModel.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Models.ContentTypeEditing; + +public class MemberTypeUpdateModel : MemberTypeModelBase +{ +} diff --git a/src/Umbraco.Core/Models/MemberType.cs b/src/Umbraco.Core/Models/MemberType.cs index 502a61df9f4a..7ed2e3721717 100644 --- a/src/Umbraco.Core/Models/MemberType.cs +++ b/src/Umbraco.Core/Models/MemberType.cs @@ -53,7 +53,15 @@ public override ContentVariation Variations // and therefore are disabled - they are fully supported at service level, though, // but not at published snapshot level. get => base.Variations; - set => throw new NotSupportedException("Variations are not supported on members."); + set + { + if (value is not ContentVariation.Nothing) + { + throw new NotSupportedException("Variations are not supported on members."); + } + + base.Variations = value; + } } /// diff --git a/src/Umbraco.Core/Services/ContentEditingService.cs b/src/Umbraco.Core/Services/ContentEditingService.cs index c2f86c5f6146..0bcf1e856f64 100644 --- a/src/Umbraco.Core/Services/ContentEditingService.cs +++ b/src/Umbraco.Core/Services/ContentEditingService.cs @@ -8,9 +8,8 @@ namespace Umbraco.Cms.Core.Services; -// FIXME: add granular permissions check (for inspiration, check how the old ContentController utilizes IAuthorizationService) internal sealed class ContentEditingService - : ContentEditingServiceBase, IContentEditingService + : ContentEditingServiceWithSortingBase, IContentEditingService { private readonly ITemplateService _templateService; private readonly ILogger _logger; @@ -26,7 +25,7 @@ public ContentEditingService( IUserIdKeyResolver userIdKeyResolver, ITreeEntitySortingService treeEntitySortingService, IContentValidationService contentValidationService) - : base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider, userIdKeyResolver, treeEntitySortingService, contentValidationService) + : base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider, userIdKeyResolver, contentValidationService, treeEntitySortingService) { _templateService = templateService; _logger = logger; diff --git a/src/Umbraco.Core/Services/ContentEditingServiceBase.cs b/src/Umbraco.Core/Services/ContentEditingServiceBase.cs index 88222086cfd2..0fadeaa76a61 100644 --- a/src/Umbraco.Core/Services/ContentEditingServiceBase.cs +++ b/src/Umbraco.Core/Services/ContentEditingServiceBase.cs @@ -18,7 +18,6 @@ internal abstract class ContentEditingServiceBase> _logger; - private readonly ITreeEntitySortingService _treeEntitySortingService; private readonly IUserIdKeyResolver _userIdKeyResolver; private readonly IContentValidationServiceBase _validationService; @@ -30,14 +29,12 @@ protected ContentEditingServiceBase( ILogger> logger, ICoreScopeProvider scopeProvider, IUserIdKeyResolver userIdKeyResolver, - ITreeEntitySortingService treeEntitySortingService, IContentValidationServiceBase validationService) { _propertyEditorCollection = propertyEditorCollection; _dataTypeService = dataTypeService; _logger = logger; _userIdKeyResolver = userIdKeyResolver; - _treeEntitySortingService = treeEntitySortingService; _validationService = validationService; CoreScopeProvider = scopeProvider; ContentService = contentService; @@ -54,10 +51,6 @@ protected ContentEditingServiceBase( protected abstract OperationResult? Delete(TContent content, int userId); - protected abstract IEnumerable GetPagedChildren(int parentId, int pageIndex, int pageSize, out long total); - - protected abstract ContentEditingOperationStatus Sort(IEnumerable items, int userId); - protected ICoreScopeProvider CoreScopeProvider { get; } protected TContentService ContentService { get; } @@ -241,48 +234,6 @@ private async Task(ContentEditingOperationStatus.CancelledByNotification, null); } - protected async Task HandleSortAsync( - Guid? parentKey, - IEnumerable sortingModels, - Guid userKey) - { - var contentId = parentKey.HasValue - ? ContentService.GetById(parentKey.Value)?.Id - : Constants.System.Root; - - if (contentId.HasValue is false) - { - return await Task.FromResult(ContentEditingOperationStatus.NotFound); - } - - const int pageSize = 500; - var pageNumber = 0; - IEnumerable page = GetPagedChildren(contentId.Value, pageNumber++, pageSize, out var total); - var children = new List((int)total); - children.AddRange(page); - while (pageNumber * pageSize < total) - { - page = GetPagedChildren(contentId.Value, pageNumber++, pageSize, out _); - children.AddRange(page); - } - - try - { - TContent[] sortedChildren = _treeEntitySortingService - .SortEntities(children, sortingModels) - .ToArray(); - - var userId = await GetUserIdAsync(userKey); - - return Sort(sortedChildren, userId); - } - catch (ArgumentException argumentException) - { - _logger.LogError(argumentException, "Invalid sorting instructions, see exception for details."); - return ContentEditingOperationStatus.SortingInvalid; - } - } - private Attempt OperationResultToAttempt(TContent? content, OperationResult? operationResult) { ContentEditingOperationStatus operationStatus = OperationResultToOperationStatus(operationResult); diff --git a/src/Umbraco.Core/Services/ContentEditingServiceWithSortingBase.cs b/src/Umbraco.Core/Services/ContentEditingServiceWithSortingBase.cs new file mode 100644 index 000000000000..523b38f15499 --- /dev/null +++ b/src/Umbraco.Core/Services/ContentEditingServiceWithSortingBase.cs @@ -0,0 +1,89 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services; + +internal abstract class ContentEditingServiceWithSortingBase + : ContentEditingServiceBase + where TContent : class, IContentBase + where TContentType : class, IContentTypeComposition + where TContentService : IContentServiceBase + where TContentTypeService : IContentTypeBaseService +{ + private readonly ILogger> _logger; + private readonly ITreeEntitySortingService _treeEntitySortingService; + + protected ContentEditingServiceWithSortingBase( + TContentService contentService, + TContentTypeService contentTypeService, + PropertyEditorCollection propertyEditorCollection, + IDataTypeService dataTypeService, + ILogger> logger, + ICoreScopeProvider scopeProvider, + IUserIdKeyResolver userIdKeyResolver, + IContentValidationServiceBase validationService, + ITreeEntitySortingService treeEntitySortingService) + : base( + contentService, + contentTypeService, + propertyEditorCollection, + dataTypeService, + logger, + scopeProvider, + userIdKeyResolver, + validationService) + { + _logger = logger; + _treeEntitySortingService = treeEntitySortingService; + } + + protected abstract ContentEditingOperationStatus Sort(IEnumerable items, int userId); + + protected abstract IEnumerable GetPagedChildren(int parentId, int pageIndex, int pageSize, out long total); + + protected async Task HandleSortAsync( + Guid? parentKey, + IEnumerable sortingModels, + Guid userKey) + { + var contentId = parentKey.HasValue + ? ContentService.GetById(parentKey.Value)?.Id + : Constants.System.Root; + + if (contentId.HasValue is false) + { + return await Task.FromResult(ContentEditingOperationStatus.NotFound); + } + + const int pageSize = 500; + var pageNumber = 0; + IEnumerable page = GetPagedChildren(contentId.Value, pageNumber++, pageSize, out var total); + var children = new List((int)total); + children.AddRange(page); + while (pageNumber * pageSize < total) + { + page = GetPagedChildren(contentId.Value, pageNumber++, pageSize, out _); + children.AddRange(page); + } + + try + { + TContent[] sortedChildren = _treeEntitySortingService + .SortEntities(children, sortingModels) + .ToArray(); + + var userId = await GetUserIdAsync(userKey); + + return Sort(sortedChildren, userId); + } + catch (ArgumentException argumentException) + { + _logger.LogError(argumentException, "Invalid sorting instructions, see exception for details."); + return ContentEditingOperationStatus.SortingInvalid; + } + } +} diff --git a/src/Umbraco.Core/Services/ContentTypeEditing/IMemberTypeEditingService.cs b/src/Umbraco.Core/Services/ContentTypeEditing/IMemberTypeEditingService.cs new file mode 100644 index 000000000000..9960e3baafe6 --- /dev/null +++ b/src/Umbraco.Core/Services/ContentTypeEditing/IMemberTypeEditingService.cs @@ -0,0 +1,17 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services.ContentTypeEditing; + +public interface IMemberTypeEditingService +{ + Task> CreateAsync(MemberTypeCreateModel model, Guid userKey); + + Task> UpdateAsync(IMemberType memberType, MemberTypeUpdateModel model, Guid userKey); + + Task> GetAvailableCompositionsAsync( + Guid? key, + IEnumerable currentCompositeKeys, + IEnumerable currentPropertyAliases); +} diff --git a/src/Umbraco.Core/Services/ContentTypeEditing/MemberTypeEditingService.cs b/src/Umbraco.Core/Services/ContentTypeEditing/MemberTypeEditingService.cs new file mode 100644 index 000000000000..240fd6b01545 --- /dev/null +++ b/src/Umbraco.Core/Services/ContentTypeEditing/MemberTypeEditingService.cs @@ -0,0 +1,59 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Core.Strings; + +namespace Umbraco.Cms.Core.Services.ContentTypeEditing; + +internal sealed class MemberTypeEditingService : ContentTypeEditingServiceBase, IMemberTypeEditingService +{ + private readonly IMemberTypeService _memberTypeService; + + public MemberTypeEditingService( + IContentTypeService contentTypeService, + IMemberTypeService memberTypeService, + IDataTypeService dataTypeService, + IEntityService entityService, + IShortStringHelper shortStringHelper) + : base(contentTypeService, memberTypeService, dataTypeService, entityService, shortStringHelper) + => _memberTypeService = memberTypeService; + + public async Task> CreateAsync(MemberTypeCreateModel model, Guid userKey) + { + Attempt result = await ValidateAndMapForCreationAsync(model, model.Key, containerKey: null); + if (result.Success) + { + IMemberType memberType = result.Result ?? throw new InvalidOperationException($"{nameof(ValidateAndMapForCreationAsync)} succeeded but did not yield any result"); + await _memberTypeService.SaveAsync(memberType, userKey); + } + + return result; + } + + public async Task> UpdateAsync(IMemberType memberType, MemberTypeUpdateModel model, Guid userKey) + { + Attempt result = await ValidateAndMapForUpdateAsync(memberType, model); + if (result.Success) + { + memberType = result.Result ?? throw new InvalidOperationException($"{nameof(ValidateAndMapForUpdateAsync)} succeeded but did not yield any result"); + await _memberTypeService.SaveAsync(memberType, userKey); + } + + return result; + } + + public async Task> GetAvailableCompositionsAsync( + Guid? key, + IEnumerable currentCompositeKeys, + IEnumerable currentPropertyAliases) => + await FindAvailableCompositionsAsync(key, currentCompositeKeys, currentPropertyAliases); + + protected override IMemberType CreateContentType(IShortStringHelper shortStringHelper, int parentId) + => new MemberType(shortStringHelper, parentId); + + protected override bool SupportsPublishing => false; + + protected override UmbracoObjectTypes ContentTypeObjectType => UmbracoObjectTypes.MemberType; + + protected override UmbracoObjectTypes ContainerObjectType => throw new NotSupportedException("Member type tree does not support containers"); +} diff --git a/src/Umbraco.Core/Services/IMemberContentEditingService.cs b/src/Umbraco.Core/Services/IMemberContentEditingService.cs new file mode 100644 index 000000000000..365b85862a88 --- /dev/null +++ b/src/Umbraco.Core/Services/IMemberContentEditingService.cs @@ -0,0 +1,14 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services; + +public interface IMemberContentEditingService +{ + Task> ValidateAsync(MemberEditingModelBase editingModel, Guid memberTypeKey); + + Task> UpdateAsync(IMember member, MemberEditingModelBase updateModel, Guid userKey); + + Task> DeleteAsync(Guid key, Guid userKey); +} diff --git a/src/Umbraco.Core/Services/IMemberEditingService.cs b/src/Umbraco.Core/Services/IMemberEditingService.cs new file mode 100644 index 000000000000..dacc19032e79 --- /dev/null +++ b/src/Umbraco.Core/Services/IMemberEditingService.cs @@ -0,0 +1,21 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services; + +public interface IMemberEditingService +{ + Task GetAsync(Guid key); + + Task> ValidateCreateAsync(MemberCreateModel createModel); + + Task> ValidateUpdateAsync(IMember member, MemberUpdateModel updateModel); + + Task> CreateAsync(MemberCreateModel createModel, IUser user); + + Task> UpdateAsync(IMember member, MemberUpdateModel updateModel, IUser user); + + Task> DeleteAsync(Guid key, Guid userKey); +} diff --git a/src/Umbraco.Core/Services/IMemberService.cs b/src/Umbraco.Core/Services/IMemberService.cs index b426741aa7ac..935dc4cf5ba3 100644 --- a/src/Umbraco.Core/Services/IMemberService.cs +++ b/src/Umbraco.Core/Services/IMemberService.cs @@ -6,7 +6,7 @@ namespace Umbraco.Cms.Core.Services; /// /// Defines the MemberService, which is an easy access to operations involving (umbraco) members. /// -public interface IMemberService : IMembershipMemberService +public interface IMemberService : IMembershipMemberService, IContentServiceBase { /// /// Gets a list of paged objects @@ -173,6 +173,20 @@ IMember CreateMemberWithIdentity(string username, string email, string name, str /// IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType); + /// + /// Saves a single object + /// + /// The to save + /// Id of the User saving the Member + Attempt Save(IMember media, int userId = Constants.Security.SuperUserId); + + /// + /// Saves a list of objects + /// + /// Collection of to save + /// Id of the User saving the Members + Attempt Save(IEnumerable members, int userId = Constants.Security.SuperUserId); + /// /// Gets the count of Members by an optional MemberType alias /// @@ -256,6 +270,13 @@ IMember CreateMemberWithIdentity(string username, string email, string name, str /// Task> GetByKeysAsync(params Guid[] ids); + /// + /// Permanently deletes an object + /// + /// The to delete + /// Id of the User deleting the Member + Attempt Delete(IMember member, int userId = Constants.Security.SuperUserId); + /// /// Delete Members of the specified MemberType id /// diff --git a/src/Umbraco.Core/Services/IMemberValidationService.cs b/src/Umbraco.Core/Services/IMemberValidationService.cs new file mode 100644 index 000000000000..a575c70a00e2 --- /dev/null +++ b/src/Umbraco.Core/Services/IMemberValidationService.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services; + +internal interface IMemberValidationService : IContentValidationServiceBase +{ +} diff --git a/src/Umbraco.Core/Services/MediaEditingService.cs b/src/Umbraco.Core/Services/MediaEditingService.cs index a8b1b54a83a9..0646b2c86e33 100644 --- a/src/Umbraco.Core/Services/MediaEditingService.cs +++ b/src/Umbraco.Core/Services/MediaEditingService.cs @@ -8,9 +8,8 @@ namespace Umbraco.Cms.Core.Services; -// FIXME: add granular permissions check (for inspiration, check how the old MediaController utilizes IAuthorizationService) internal sealed class MediaEditingService - : ContentEditingServiceBase, IMediaEditingService + : ContentEditingServiceWithSortingBase, IMediaEditingService { private readonly ILogger> _logger; @@ -24,7 +23,7 @@ public MediaEditingService( IUserIdKeyResolver userIdKeyResolver, ITreeEntitySortingService treeEntitySortingService, IMediaValidationService mediaValidationService) - : base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider, userIdKeyResolver, treeEntitySortingService, mediaValidationService) + : base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider, userIdKeyResolver, mediaValidationService, treeEntitySortingService) => _logger = logger; public async Task GetAsync(Guid key) diff --git a/src/Umbraco.Core/Services/MemberContentEditingService.cs b/src/Umbraco.Core/Services/MemberContentEditingService.cs new file mode 100644 index 000000000000..285a01067602 --- /dev/null +++ b/src/Umbraco.Core/Services/MemberContentEditingService.cs @@ -0,0 +1,91 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services; + +internal sealed class MemberContentEditingService + : ContentEditingServiceBase, IMemberContentEditingService +{ + private readonly ILogger> _logger; + + public MemberContentEditingService( + IMemberService contentService, + IMemberTypeService contentTypeService, + PropertyEditorCollection propertyEditorCollection, + IDataTypeService dataTypeService, + ILogger> logger, + ICoreScopeProvider scopeProvider, + IUserIdKeyResolver userIdKeyResolver, + IMemberValidationService memberValidationService) + : base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider, userIdKeyResolver, memberValidationService) + => _logger = logger; + + public async Task> ValidateAsync(MemberEditingModelBase editingModel, Guid memberTypeKey) + => await ValidatePropertiesAsync(editingModel, memberTypeKey); + + public async Task> UpdateAsync(IMember member, MemberEditingModelBase updateModel, Guid userKey) + { + // FIXME: handle sensitive property data + Attempt result = await MapUpdate(member, updateModel); + if (result.Success == false) + { + return Attempt.FailWithStatus(result.Status, result.Result); + } + + // the create mapping might succeed, but this doesn't mean the model is valid at property level. + // we'll return the actual property validation status if the entire operation succeeds. + ContentEditingOperationStatus validationStatus = result.Status; + ContentValidationResult validationResult = result.Result.ValidationResult; + + var currentUserId = await GetUserIdAsync(userKey); + ContentEditingOperationStatus operationStatus = Save(member, currentUserId); + return operationStatus == ContentEditingOperationStatus.Success + ? Attempt.SucceedWithStatus(validationStatus, new MemberUpdateResult { Content = member, ValidationResult = validationResult }) + : Attempt.FailWithStatus(operationStatus, new MemberUpdateResult { Content = member }); + } + + public async Task> DeleteAsync(Guid key, Guid userKey) + => await HandleDeleteAsync(key, userKey, false); + + + protected override IMember New(string? name, int parentId, IMemberType memberType) + => throw new NotSupportedException("Member creation is not supported by this service. This should never be called."); + + protected override OperationResult? Move(IMember member, int newParentId, int userId) + => throw new InvalidOperationException("Move is not supported for members"); + + protected override IMember? Copy(IMember member, int newParentId, bool relateToOriginal, bool includeDescendants, int userId) + => throw new NotSupportedException("Copy is not supported for Member"); + + protected override OperationResult? MoveToRecycleBin(IMember member, int userId) + => throw new InvalidOperationException("Recycle bin is not supported for members"); + + protected override OperationResult? Delete(IMember member, int userId) + => ContentService.Delete(member, userId).Result; + + private ContentEditingOperationStatus Save(IMember member, int userId) + { + try + { + Attempt saveResult = ContentService.Save(member, userId); + return saveResult.Result?.Result switch + { + // these are the only result states currently expected from Save + OperationResultType.Success => ContentEditingOperationStatus.Success, + OperationResultType.FailedCancelledByEvent => ContentEditingOperationStatus.CancelledByNotification, + + // for any other state we'll return "unknown" so we know that we need to amend this + _ => ContentEditingOperationStatus.Unknown + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Member save operation failed"); + return ContentEditingOperationStatus.Unknown; + } + } +} diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index 291699660757..bf0b0935f501 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -315,7 +315,7 @@ private IMember CreateMemberWithIdentity(string username, string email, string n /// and the user id in the membership provider. /// Id /// - public IMember? GetByKey(Guid id) + public IMember? GetById(Guid id) { using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); scope.ReadLock(Constants.Locks.MemberTree); @@ -323,6 +323,10 @@ private IMember CreateMemberWithIdentity(string username, string email, string n return _memberRepository.Get(query)?.FirstOrDefault(); } + [Obsolete($"Use {nameof(GetById)}. Will be removed in V15.")] + public IMember? GetByKey(Guid id) + => GetById(id); + /// /// Gets a list of paged objects /// @@ -745,7 +749,7 @@ public bool Exists(string username) public void SetLastLogin(string username, DateTime date) => throw new NotImplementedException(); /// - public void Save(IMember member) + public Attempt Save(IMember member, int userId = Constants.Security.SuperUserId) { // trimming username and email to make sure we have no trailing space member.Username = member.Username.Trim(); @@ -758,7 +762,7 @@ public void Save(IMember member) if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); - return; + return OperationResult.Attempt.Cancel(evtMsgs); } if (string.IsNullOrWhiteSpace(member.Name)) @@ -775,10 +779,14 @@ public void Save(IMember member) Audit(AuditType.Save, 0, member.Id); scope.Complete(); + return OperationResult.Attempt.Succeed(evtMsgs); } + public void Save(IMember member) + => Save(member, Constants.Security.SuperUserId); + /// - public void Save(IEnumerable members) + public Attempt Save(IEnumerable members, int userId = Constants.Security.SuperUserId) { IMember[] membersA = members.ToArray(); @@ -789,7 +797,7 @@ public void Save(IEnumerable members) if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); - return; + return OperationResult.Attempt.Cancel(evtMsgs); } scope.WriteLock(Constants.Locks.MemberTree); @@ -805,20 +813,22 @@ public void Save(IEnumerable members) scope.Notifications.Publish(new MemberSavedNotification(membersA, evtMsgs).WithStateFrom(savingNotification)); - Audit(AuditType.Save, 0, -1, "Save multiple Members"); + Audit(AuditType.Save, userId, Constants.System.Root, "Save multiple Members"); scope.Complete(); + return OperationResult.Attempt.Succeed(evtMsgs); } + [Obsolete($"Use the {nameof(Save)} method that yields an Attempt. Will be removed in V15.")] + public void Save(IEnumerable members) + => Save(members, Constants.Security.SuperUserId); + #endregion #region Delete - /// - /// Deletes an - /// - /// to Delete - public void Delete(IMember member) + /// + public Attempt Delete(IMember member, int userId = Constants.Security.SuperUserId) { EventMessages evtMsgs = EventMessagesFactory.Get(); @@ -827,7 +837,7 @@ public void Delete(IMember member) if (scope.Notifications.PublishCancelable(deletingNotification)) { scope.Complete(); - return; + return OperationResult.Attempt.Cancel(evtMsgs); } scope.WriteLock(Constants.Locks.MemberTree); @@ -835,8 +845,14 @@ public void Delete(IMember member) Audit(AuditType.Delete, 0, member.Id); scope.Complete(); + + return OperationResult.Attempt.Succeed(evtMsgs); } + /// + public void Delete(IMember member) + => Delete(member, Constants.Security.SuperUserId); + private void DeleteLocked(ICoreScope scope, IMember member, EventMessages evtMsgs, IDictionary? notificationState = null) { // a member has no descendants @@ -1025,6 +1041,16 @@ public void ReplaceRoles(int[] memberIds, string[] roleNames) #endregion + #region Others + + // NOTE: at the time of writing we do not have MemberTreeChangeNotification to publish changes as a result of a data integrity + // check. we cannot support this feature until such notification exists. + // see the content or media services for implementation details if this is ever going to be a relevant feature for members. + public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options) + => throw new InvalidOperationException("Data integrity checks are not (yet) implemented for members."); + + #endregion + #region Private Methods private void Audit(AuditType type, int userId, int objectId, string? message = null) => _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.Member), message)); diff --git a/src/Umbraco.Core/Services/MemberValidationService.cs b/src/Umbraco.Core/Services/MemberValidationService.cs new file mode 100644 index 000000000000..79ec4d75fc3f --- /dev/null +++ b/src/Umbraco.Core/Services/MemberValidationService.cs @@ -0,0 +1,17 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.Core.Services; + +internal sealed class MemberValidationService : ContentValidationServiceBase, IMemberValidationService +{ + public MemberValidationService(IPropertyValidationService propertyValidationService, ILanguageService languageService) + : base(propertyValidationService, languageService) + { + } + + public async Task ValidatePropertiesAsync( + ContentEditingModelBase contentEditingModelBase, + IMemberType memberType) + => await HandlePropertiesValidationAsync(contentEditingModelBase, memberType); +} diff --git a/src/Umbraco.Core/Services/OperationStatus/MemberEditingOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/MemberEditingOperationStatus.cs new file mode 100644 index 000000000000..ca9693f736b0 --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus/MemberEditingOperationStatus.cs @@ -0,0 +1,19 @@ +namespace Umbraco.Cms.Core.Services.OperationStatus; + +public enum MemberEditingOperationStatus +{ + Success, + MemberNotFound, + MemberTypeNotFound, + UnlockFailed, + DisableTwoFactorFailed, + RoleAssignmentFailed, + PasswordChangeFailed, + InvalidPassword, + InvalidName, + InvalidUsername, + InvalidEmail, + DuplicateUsername, + DuplicateEmail, + Unknown, +} diff --git a/src/Umbraco.Core/Services/OperationStatus/MemberEditingStatus.cs b/src/Umbraco.Core/Services/OperationStatus/MemberEditingStatus.cs new file mode 100644 index 000000000000..ca29e521eacd --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus/MemberEditingStatus.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Core.Services.OperationStatus; + +public sealed class MemberEditingStatus +{ + public ContentEditingOperationStatus ContentEditingOperationStatus { get; set; } = ContentEditingOperationStatus.Unknown; + + public MemberEditingOperationStatus MemberEditingOperationStatus { get; set; } = MemberEditingOperationStatus.Unknown; +} diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 36732dddfe10..c58c705a5271 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -242,6 +242,10 @@ public static IUmbracoBuilder AddCoreInitialServices(this IUmbracoBuilder builde builder.AddDeliveryApiCoreServices(); builder.Services.AddTransient(); + builder.Services.AddUnique, PasswordChanger>(); + builder.Services.AddUnique, PasswordChanger>(); + builder.Services.AddTransient(); + return builder; } diff --git a/src/Umbraco.Web.BackOffice/Security/IPasswordChanger.cs b/src/Umbraco.Infrastructure/Security/IPasswordChanger.cs similarity index 94% rename from src/Umbraco.Web.BackOffice/Security/IPasswordChanger.cs rename to src/Umbraco.Infrastructure/Security/IPasswordChanger.cs index 3bc5f35abf66..c0e733901aae 100644 --- a/src/Umbraco.Web.BackOffice/Security/IPasswordChanger.cs +++ b/src/Umbraco.Infrastructure/Security/IPasswordChanger.cs @@ -3,7 +3,7 @@ using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Web.Common.Security; +namespace Umbraco.Cms.Core.Security; public interface IPasswordChanger where TUser : UmbracoIdentityUser { diff --git a/src/Umbraco.Infrastructure/Security/MemberIdentityUser.cs b/src/Umbraco.Infrastructure/Security/MemberIdentityUser.cs index c9e87edfe9bf..a6e779d0b198 100644 --- a/src/Umbraco.Infrastructure/Security/MemberIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/MemberIdentityUser.cs @@ -55,8 +55,7 @@ public string? Comments /// /// Used to construct a new instance without an identity /// - public static MemberIdentityUser CreateNew(string username, string email, string memberTypeAlias, bool isApproved, - string? name = null) + public static MemberIdentityUser CreateNew(string username, string email, string memberTypeAlias, bool isApproved, string? name = null, Guid? key = null) { if (string.IsNullOrWhiteSpace(username)) { @@ -70,6 +69,7 @@ public static MemberIdentityUser CreateNew(string username, string email, string user.MemberTypeAlias = memberTypeAlias; user.IsApproved = isApproved; user.Id = null!; + user.Key = key ?? user.Key; user.HasIdentity = false; user.Name = name; user.EnableChangeTracking(); diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index fcc87dacf3a0..8f82e9edccc1 100644 --- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -107,6 +107,23 @@ public override Task CreateAsync( ? Constants.Security.DefaultMemberTypeAlias : user.MemberTypeAlias!); + if (user.Key != Guid.Empty) + { + // at the time of writing, the memberEntity identity is not set until the member is saved. as we rely on + // that behavior when setting an explicit key, we need to know immediately if it changes. integration tests + // will detect this change of behavior. + if (memberEntity.HasIdentity) + { + return Task.FromResult(IdentityResult.Failed(new IdentityError + { + Code = GenericIdentityErrorCode, + Description = "Cannot assign a new key to a member that already has identity." + })); + } + + memberEntity.Key = user.Key; + } + UpdateMemberProperties(memberEntity, user, out bool _); // create the member diff --git a/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs b/src/Umbraco.Infrastructure/Security/PasswordChanger.cs similarity index 98% rename from src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs rename to src/Umbraco.Infrastructure/Security/PasswordChanger.cs index fd3fc00ab4b8..03d298ffdadf 100644 --- a/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs +++ b/src/Umbraco.Infrastructure/Security/PasswordChanger.cs @@ -1,13 +1,11 @@ using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; -using Umbraco.Cms.Core.Security; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Security; +namespace Umbraco.Cms.Core.Security; /// /// Changes the password for an identity user diff --git a/src/Umbraco.Infrastructure/Services/MemberEditingService.cs b/src/Umbraco.Infrastructure/Services/MemberEditingService.cs new file mode 100644 index 000000000000..06ca877c4049 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/MemberEditingService.cs @@ -0,0 +1,357 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Services; + +internal sealed class MemberEditingService : IMemberEditingService +{ + private readonly IMemberService _memberService; + private readonly IMemberTypeService _memberTypeService; + private readonly IMemberContentEditingService _memberContentEditingService; + private readonly IMemberManager _memberManager; + private readonly ITwoFactorLoginService _twoFactorLoginService; + private readonly IPasswordChanger _passwordChanger; + private readonly ILogger _logger; + + public MemberEditingService( + IMemberService memberService, + IMemberTypeService memberTypeService, + IMemberContentEditingService memberContentEditingService, + IMemberManager memberManager, + ITwoFactorLoginService twoFactorLoginService, + IPasswordChanger passwordChanger, + ILogger logger) + { + _memberService = memberService; + _memberTypeService = memberTypeService; + _memberContentEditingService = memberContentEditingService; + _memberManager = memberManager; + _twoFactorLoginService = twoFactorLoginService; + _passwordChanger = passwordChanger; + _logger = logger; + } + + public async Task GetAsync(Guid key) + => await Task.FromResult(_memberService.GetByKey(key)); + + public async Task> ValidateCreateAsync(MemberCreateModel createModel) + => await _memberContentEditingService.ValidateAsync(createModel, createModel.ContentTypeKey); + + public async Task> ValidateUpdateAsync(IMember member, MemberUpdateModel updateModel) + => await _memberContentEditingService.ValidateAsync(updateModel, member.ContentType.Key); + + public async Task> CreateAsync(MemberCreateModel createModel, IUser user) + { + var status = new MemberEditingStatus(); + + MemberEditingOperationStatus validationStatus = await ValidateMemberDataAsync(createModel, null, createModel.Password); + if (validationStatus is not MemberEditingOperationStatus.Success) + { + status.MemberEditingOperationStatus = validationStatus; + return Attempt.FailWithStatus(status, new MemberCreateResult()); + } + + IMemberType? memberType = await _memberTypeService.GetAsync(createModel.ContentTypeKey); + if (memberType is null) + { + status.MemberEditingOperationStatus = MemberEditingOperationStatus.MemberTypeNotFound; + return Attempt.FailWithStatus(status, new MemberCreateResult()); + } + + var identityMember = MemberIdentityUser.CreateNew( + createModel.Username, + createModel.Email, + memberType.Alias, + createModel.IsApproved, + createModel.InvariantName, + createModel.Key); + + IdentityResult createResult = await _memberManager.CreateAsync(identityMember, createModel.Password); + if (createResult.Succeeded is false) + { + return IdentityMemberCreationFailed(createResult, status); + } + + IMember member = _memberService.GetByEmail(createModel.Email) + ?? throw new InvalidOperationException("Member creation succeeded, but member could not be found by email."); + + var updateRolesResult = await UpdateRoles(createModel.Roles, identityMember); + if (updateRolesResult is false) + { + status.MemberEditingOperationStatus = MemberEditingOperationStatus.RoleAssignmentFailed; + return Attempt.FailWithStatus(status, new MemberCreateResult { Content = member }); + } + + Attempt contentUpdateResult = await _memberContentEditingService.UpdateAsync(member, createModel, user.Key); + + status.MemberEditingOperationStatus = MemberEditingOperationStatus.Success; + status.ContentEditingOperationStatus = contentUpdateResult.Status; + + return contentUpdateResult.Success + ? Attempt.SucceedWithStatus(status, new MemberCreateResult { Content = member, ValidationResult = contentUpdateResult.Result.ValidationResult }) + : Attempt.FailWithStatus(status, new MemberCreateResult { Content = member }); + } + + public async Task> UpdateAsync(IMember member, MemberUpdateModel updateModel, IUser user) + { + var status = new MemberEditingStatus(); + + MemberIdentityUser? identityMember = await _memberManager.FindByIdAsync(member.Id.ToString()); + if (identityMember is null) + { + status.MemberEditingOperationStatus = MemberEditingOperationStatus.MemberNotFound; + return Attempt.FailWithStatus(status, new MemberUpdateResult { Content = member }); + } + + MemberEditingOperationStatus validationStatus = await ValidateMemberDataAsync(updateModel, member.Key, updateModel.NewPassword); + if (validationStatus is not MemberEditingOperationStatus.Success) + { + status.MemberEditingOperationStatus = validationStatus; + return Attempt.FailWithStatus(status, new MemberUpdateResult { Content = member }); + } + + if (identityMember.IsLockedOut && updateModel.IsLockedOut is false) + { + var unlockResult = await UnlockMember(identityMember); + if (unlockResult is false) + { + status.MemberEditingOperationStatus = MemberEditingOperationStatus.UnlockFailed; + return Attempt.FailWithStatus(status, new MemberUpdateResult { Content = member }); + } + } + + if (updateModel.IsTwoFactorEnabled is false) + { + var disableTwoFactorResult = await DisableTwoFactor(member); + if (disableTwoFactorResult is false) + { + status.MemberEditingOperationStatus = MemberEditingOperationStatus.DisableTwoFactorFailed; + return Attempt.FailWithStatus(status, new MemberUpdateResult { Content = member }); + } + } + + if (updateModel.NewPassword.IsNullOrWhiteSpace() is false) + { + var changePasswordResult = await ChangePassword(member, updateModel.OldPassword, updateModel.NewPassword, user); + if (changePasswordResult is false) + { + status.MemberEditingOperationStatus = MemberEditingOperationStatus.PasswordChangeFailed; + return Attempt.FailWithStatus(status, new MemberUpdateResult { Content = member }); + } + } + + var updateRolesResult = await UpdateRoles(updateModel.Roles, identityMember); + if (updateRolesResult is false) + { + status.MemberEditingOperationStatus = MemberEditingOperationStatus.RoleAssignmentFailed; + return Attempt.FailWithStatus(status, new MemberUpdateResult { Content = member }); + } + + // FIXME: handle sensitive data. certain properties (IsApproved, IsLockedOut, ...) are subject to "sensitive data" rules. + // reverse engineer what's happening in the old backoffice MemberController and replicate here + member.IsLockedOut = updateModel.IsLockedOut; + member.IsApproved = updateModel.IsApproved; + member.Email = updateModel.Email; + member.Username = updateModel.Username; + + Attempt contentUpdateResult = await _memberContentEditingService.UpdateAsync(member, updateModel, user.Key); + + status.MemberEditingOperationStatus = MemberEditingOperationStatus.Success; + status.ContentEditingOperationStatus = contentUpdateResult.Status; + + return contentUpdateResult.Success + ? Attempt.SucceedWithStatus(status, new MemberUpdateResult { Content = member, ValidationResult = contentUpdateResult.Result.ValidationResult }) + : Attempt.FailWithStatus(status, new MemberUpdateResult { Content = member }); + } + + public async Task> DeleteAsync(Guid key, Guid userKey) + { + Attempt contentDeleteResult = await _memberContentEditingService.DeleteAsync(key, userKey); + return contentDeleteResult.Success + ? Attempt.SucceedWithStatus( + new MemberEditingStatus + { + MemberEditingOperationStatus = MemberEditingOperationStatus.Success, + ContentEditingOperationStatus = contentDeleteResult.Status + }, + contentDeleteResult.Result) + : Attempt.FailWithStatus( + new MemberEditingStatus + { + MemberEditingOperationStatus = MemberEditingOperationStatus.Unknown, + ContentEditingOperationStatus = contentDeleteResult.Status + }, + contentDeleteResult.Result); + } + + private async Task ValidateMemberDataAsync(MemberEditingModelBase model, Guid? memberKey, string? password) + { + if (model.InvariantName.IsNullOrWhiteSpace()) + { + return MemberEditingOperationStatus.InvalidName; + } + + if (model.Username.IsNullOrWhiteSpace()) + { + return MemberEditingOperationStatus.InvalidUsername; + } + + if (password is not null) + { + IdentityResult validatePassword = await _memberManager.ValidatePasswordAsync(password); + if (validatePassword.Succeeded == false) + { + return MemberEditingOperationStatus.InvalidPassword; + } + } + + IMember? byUsername = _memberService.GetByUsername(model.Username); + if (byUsername is not null && byUsername.Key != memberKey) + { + return MemberEditingOperationStatus.DuplicateUsername; + } + + IMember? byEmail = _memberService.GetByEmail(model.Email); + if (byEmail is not null && byEmail.Key != memberKey) + { + return MemberEditingOperationStatus.DuplicateEmail; + } + + return MemberEditingOperationStatus.Success; + } + + private async Task UpdateRoles(IEnumerable? roles, MemberIdentityUser identityMember) + { + // We're gonna look up the current roles now because the below code can cause + // events to be raised and developers could be manually adding roles to members in + // their handlers. If we don't look this up now there's a chance we'll just end up + // removing the roles they've assigned. + IEnumerable currentRoles = (await _memberManager.GetRolesAsync(identityMember)).ToList(); + + // find the ones to remove and remove them + var rolesToRemove = currentRoles.Except(roles ?? Enumerable.Empty()).ToArray(); + + // Now let's do the role provider stuff - now that we've saved the content item (that is important since + // if we are changing the username, it must be persisted before looking up the member roles). + if (rolesToRemove.Any()) + { + IdentityResult identityResult = await _memberManager.RemoveFromRolesAsync(identityMember, rolesToRemove); + if (!identityResult.Succeeded) + { + _logger.LogError("Could not remove roles from member: {errorMessage}", identityResult.Errors.ToErrorMessage()); + return false; + } + } + + // find the ones to add and add them + var rolesToAdd = roles?.Except(currentRoles).ToArray(); + if (rolesToAdd?.Any() is true) + { + // add the ones submitted + IdentityResult identityResult = await _memberManager.AddToRolesAsync(identityMember, rolesToAdd); + if (!identityResult.Succeeded) + { + _logger.LogError("Could not add roles to member: {errorMessage}", identityResult.Errors.ToErrorMessage()); + return false; + } + } + + return true; + } + + private static Attempt IdentityMemberCreationFailed(IdentityResult created, MemberEditingStatus status) + { + MemberEditingOperationStatus createStatus = MemberEditingOperationStatus.Unknown; + foreach (IdentityError error in created.Errors) + { + switch (error.Code) + { + case nameof(IdentityErrorDescriber.InvalidUserName): + createStatus = MemberEditingOperationStatus.InvalidUsername; + break; + case nameof(IdentityErrorDescriber.PasswordMismatch): + case nameof(IdentityErrorDescriber.PasswordRequiresDigit): + case nameof(IdentityErrorDescriber.PasswordRequiresLower): + case nameof(IdentityErrorDescriber.PasswordRequiresNonAlphanumeric): + case nameof(IdentityErrorDescriber.PasswordRequiresUniqueChars): + case nameof(IdentityErrorDescriber.PasswordRequiresUpper): + case nameof(IdentityErrorDescriber.PasswordTooShort): + createStatus = MemberEditingOperationStatus.InvalidPassword; + break; + case nameof(IdentityErrorDescriber.InvalidEmail): + createStatus = MemberEditingOperationStatus.InvalidEmail; + break; + case nameof(IdentityErrorDescriber.DuplicateUserName): + createStatus = MemberEditingOperationStatus.DuplicateUsername; + break; + case nameof(IdentityErrorDescriber.DuplicateEmail): + createStatus = MemberEditingOperationStatus.DuplicateEmail; + break; + } + + if (createStatus is not MemberEditingOperationStatus.Unknown) + { + break; + } + } + + status.MemberEditingOperationStatus = createStatus; + return Attempt.FailWithStatus(status, new MemberCreateResult()); + } + + private async Task UnlockMember(MemberIdentityUser identityMember) + { + // Handle unlocking with the member manager (takes care of other nuances) + IdentityResult unlockResult = await _memberManager.SetLockoutEndDateAsync(identityMember, DateTimeOffset.Now.AddMinutes(-1)); + if (unlockResult.Succeeded is false) + { + _logger.LogError("Could not unlock member: {errorMessage}", unlockResult.Errors.ToErrorMessage()); + } + + return unlockResult.Succeeded; + } + + private async Task DisableTwoFactor(IMember member) + { + IEnumerable providers = await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(member.Key); + foreach (var provider in providers) + { + var disableResult = await _twoFactorLoginService.DisableAsync(member.Key, provider); + if (disableResult is false) + { + _logger.LogError("2FA provider \"{provider}\" could not disable member", provider); + return false; + } + } + + return true; + } + + private async Task ChangePassword(IMember member, string? oldPassword, string newPassword, IUser user) + { + var changingPasswordModel = new ChangingPasswordModel + { + Id = member.Id, + OldPassword = oldPassword, + NewPassword = newPassword + }; + + // change and persist the password + Attempt passwordChangeResult = + await _passwordChanger.ChangePasswordWithIdentityAsync(changingPasswordModel, _memberManager, user); + + if (passwordChangeResult.Success is false) + { + _logger.LogError("Could not change member password: {errorMessage}", passwordChangeResult.Result?.Error?.ErrorMessage ?? "no error details available"); + return false; + } + + return true; + } +} diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs index e8151559ac56..fcbb758f280e 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs @@ -54,8 +54,6 @@ public static IUmbracoBuilder AddBackOfficeAuthentication(this IUmbracoBuilder b builder.Services.AddSingleton(); builder.Services.AddUnique(); - builder.Services.AddUnique, PasswordChanger>(); - builder.Services.AddUnique, PasswordChanger>(); builder.Services.AddScoped(); builder.AddNotificationHandler(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberEditingServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberEditingServiceTests.cs new file mode 100644 index 000000000000..92c30ee9fb02 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberEditingServiceTests.cs @@ -0,0 +1,303 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; + +[TestFixture] +[Category("Slow")] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +public class MemberEditingServiceTests : UmbracoIntegrationTest +{ + private IMemberEditingService MemberEditingService => GetRequiredService(); + + private IMemberService MemberService => GetRequiredService(); + + private IMemberTypeService MemberTypeService => GetRequiredService(); + + [Test] + public async Task Can_Create_Member() + { + var member = await CreateMemberAsync(); + Assert.IsTrue(member.HasIdentity); + Assert.Greater(member.Id, 0); + + member = await MemberEditingService.GetAsync(member.Key); + Assert.IsNotNull(member); + Assert.AreEqual("test@test.com", member.Email); + Assert.AreEqual("test", member.Username); + Assert.AreEqual("T. Est", member.Name); + Assert.IsTrue(member.IsApproved); + + Assert.AreEqual("The title value", member.GetValue("title")); + Assert.AreEqual("The author value", member.GetValue("author")); + + var memberManager = GetRequiredService(); + var memberIdentityUser = await memberManager.FindByEmailAsync(member.Email); + Assert.IsNotNull(memberIdentityUser); + Assert.IsTrue(await memberManager.CheckPasswordAsync(memberIdentityUser, "SuperSecret123")); + + var roles = MemberService.GetAllRoles(member.Id).ToArray(); + Assert.AreEqual(1, roles.Length); + Assert.AreEqual("RoleOne", roles[0]); + } + + [Test] + public async Task Can_Create_Member_With_Explicit_Key() + { + var key = Guid.NewGuid(); + var member = await CreateMemberAsync(key); + Assert.IsTrue(member.HasIdentity); + Assert.Greater(member.Id, 0); + Assert.AreEqual(key, member.Key); + + member = await MemberEditingService.GetAsync(member.Key); + Assert.IsNotNull(member); + } + + [Test] + public async Task Can_Update_Member() + { + var member = await CreateMemberAsync(); + + var updateModel = new MemberUpdateModel + { + Email = "test-updated@test.com", + Username = "test-updated", + IsApproved = false, + InvariantName = "T. Est Updated", + InvariantProperties = new[] + { + new PropertyValueModel { Alias = "title", Value = "The updated title value" }, + new PropertyValueModel { Alias = "author", Value = "The updated author value" } + } + }; + + var result = await MemberEditingService.UpdateAsync(member, updateModel, SuperUser()); + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status.ContentEditingOperationStatus); + Assert.AreEqual(MemberEditingOperationStatus.Success, result.Status.MemberEditingOperationStatus); + + member = result.Result.Content; + Assert.IsNotNull(member); + Assert.AreEqual("test-updated@test.com", member.Email); + Assert.AreEqual("test-updated", member.Username); + Assert.AreEqual("T. Est Updated", member.Name); + Assert.IsFalse(member.IsApproved); + + Assert.AreEqual("The updated title value", member.GetValue("title")); + Assert.AreEqual("The updated author value", member.GetValue("author")); + } + + [Test] + public async Task Can_Change_Member_Password() + { + var member = await CreateMemberAsync(); + var memberManager = GetRequiredService(); + + var updateModel = new MemberUpdateModel + { + Email = member.Email, + Username = member.Username, + IsApproved = true, + InvariantName = member.Name, + NewPassword = "NewSuperSecret123" + }; + + var result = await MemberEditingService.UpdateAsync(member, updateModel, SuperUser()); + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status.ContentEditingOperationStatus); + Assert.AreEqual(MemberEditingOperationStatus.Success, result.Status.MemberEditingOperationStatus); + + var memberIdentityUser = await memberManager.FindByEmailAsync(member.Email); + Assert.IsNotNull(memberIdentityUser); + Assert.IsTrue(await memberManager.CheckPasswordAsync(memberIdentityUser, "NewSuperSecret123")); + } + + [Test] + public async Task Can_Change_Member_Roles() + { + MemberService.AddRole("RoleTwo"); + MemberService.AddRole("RoleThree"); + + var member = await CreateMemberAsync(); + + var updateModel = new MemberUpdateModel + { + Email = member.Email, + Username = member.Username, + IsApproved = true, + InvariantName = member.Name, + Roles = new [] { "RoleTwo", "RoleThree" } + }; + + var result = await MemberEditingService.UpdateAsync(member, updateModel, SuperUser()); + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status.ContentEditingOperationStatus); + Assert.AreEqual(MemberEditingOperationStatus.Success, result.Status.MemberEditingOperationStatus); + + var roles = MemberService.GetAllRoles(member.Id).ToArray(); + Assert.AreEqual(2, roles.Length); + Assert.IsTrue(roles.Contains("RoleTwo")); + Assert.IsTrue(roles.Contains("RoleThree")); + } + + [Test] + public async Task Can_Delete_Member() + { + var member = await CreateMemberAsync(); + Assert.IsTrue(member.HasIdentity); + Assert.Greater(member.Id, 0); + + member = await MemberEditingService.GetAsync(member.Key); + Assert.IsNotNull(member); + + var result = await MemberEditingService.DeleteAsync(member.Key, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status.ContentEditingOperationStatus); + Assert.AreEqual(MemberEditingOperationStatus.Success, result.Status.MemberEditingOperationStatus); + + member = await MemberEditingService.GetAsync(member.Key); + Assert.IsNull(member); + } + + [TestCase(true)] + [TestCase(false)] + public async Task Can_Create_With_Property_Validation(bool addValidProperties) + { + IMemberType memberType = MemberTypeBuilder.CreateSimpleMemberType(); + memberType.PropertyTypes.First(pt => pt.Alias == "title").Mandatory = true; + memberType.PropertyTypes.First(pt => pt.Alias == "author").ValidationRegExp = "^\\d*$"; + MemberTypeService.Save(memberType); + + var titleValue = addValidProperties ? "The title value" : null; + var authorValue = addValidProperties ? "12345" : "This is not a number"; + + var createModel = new MemberCreateModel + { + Email = "test@test.com", + Username = "test", + Password = "SuperSecret123", + IsApproved = true, + ContentTypeKey = memberType.Key, + InvariantName = "T. Est", + InvariantProperties = new[] + { + new PropertyValueModel { Alias = "title", Value = titleValue }, + new PropertyValueModel { Alias = "author", Value = authorValue } + } + }; + + var result = await MemberEditingService.CreateAsync(createModel, SuperUser()); + + // success is expected regardless of property level validation - the validation error status is communicated in the attempt status (see below) + Assert.IsTrue(result.Success); + Assert.AreEqual(MemberEditingOperationStatus.Success, result.Status.MemberEditingOperationStatus); + Assert.AreEqual(addValidProperties ? ContentEditingOperationStatus.Success : ContentEditingOperationStatus.PropertyValidationError, result.Status.ContentEditingOperationStatus); + Assert.IsNotNull(result.Result); + + if (addValidProperties is false) + { + Assert.AreEqual(2, result.Result.ValidationResult.ValidationErrors.Count()); + Assert.IsNotNull(result.Result.ValidationResult.ValidationErrors.FirstOrDefault(v => v.Alias == "title" && v.ErrorMessages.Length == 1)); + Assert.IsNotNull(result.Result.ValidationResult.ValidationErrors.FirstOrDefault(v => v.Alias == "author" && v.ErrorMessages.Length == 1)); + } + + // NOTE: member creation must be successful, even if the mandatory property is missing + Assert.IsTrue(result.Result.Content!.HasIdentity); + Assert.AreEqual(titleValue, result.Result.Content!.GetValue("title")); + Assert.AreEqual(authorValue, result.Result.Content!.GetValue("author")); + } + + [TestCase(true)] + [TestCase(false)] + public async Task Can_Update_With_Property_Validation(bool addValidProperties) + { + var member = await CreateMemberAsync(); + var memberType = await MemberTypeService.GetAsync(member.ContentType.Key)!; + memberType.PropertyTypes.First(pt => pt.Alias == "title").Mandatory = true; + memberType.PropertyTypes.First(pt => pt.Alias == "author").ValidationRegExp = "^\\d*$"; + await MemberTypeService.SaveAsync(memberType, Constants.Security.SuperUserKey); + + var titleValue = addValidProperties ? "The title value" : null; + var authorValue = addValidProperties ? "12345" : "This is not a number"; + + var updateModel = new MemberUpdateModel + { + Email = member.Email, + Username = member.Username, + IsApproved = true, + InvariantName = member.Name, + InvariantProperties = new[] + { + new PropertyValueModel { Alias = "title", Value = titleValue }, + new PropertyValueModel { Alias = "author", Value = authorValue } + } + }; + + var result = await MemberEditingService.UpdateAsync(member, updateModel, SuperUser()); + + // success is expected regardless of property level validation - the validation error status is communicated in the attempt status (see below) + Assert.IsTrue(result.Success); + Assert.AreEqual(MemberEditingOperationStatus.Success, result.Status.MemberEditingOperationStatus); + Assert.AreEqual(addValidProperties ? ContentEditingOperationStatus.Success : ContentEditingOperationStatus.PropertyValidationError, result.Status.ContentEditingOperationStatus); + Assert.IsNotNull(result.Result); + + if (addValidProperties is false) + { + Assert.AreEqual(2, result.Result.ValidationResult.ValidationErrors.Count()); + Assert.IsNotNull(result.Result.ValidationResult.ValidationErrors.FirstOrDefault(v => v.Alias == "title" && v.ErrorMessages.Length == 1)); + Assert.IsNotNull(result.Result.ValidationResult.ValidationErrors.FirstOrDefault(v => v.Alias == "author" && v.ErrorMessages.Length == 1)); + } + + // NOTE: member update must be successful, even if the mandatory property is missing + Assert.AreEqual(titleValue, result.Result.Content!.GetValue("title")); + Assert.AreEqual(authorValue, result.Result.Content!.GetValue("author")); + } + + private IUser SuperUser() => GetRequiredService().GetAsync(Constants.Security.SuperUserKey).GetAwaiter().GetResult(); + + private async Task CreateMemberAsync(Guid? key = null) + { + IMemberType memberType = MemberTypeBuilder.CreateSimpleMemberType(); + MemberTypeService.Save(memberType); + MemberService.AddRole("RoleOne"); + + var createModel = new MemberCreateModel + { + Key = key, + Email = "test@test.com", + Username = "test", + Password = "SuperSecret123", + IsApproved = true, + ContentTypeKey = memberType.Key, + Roles = new [] { "RoleOne" }, + InvariantName = "T. Est", + InvariantProperties = new[] + { + new PropertyValueModel { Alias = "title", Value = "The title value" }, + new PropertyValueModel { Alias = "author", Value = "The author value" } + } + }; + + var result = await MemberEditingService.CreateAsync(createModel, SuperUser()); + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status.ContentEditingOperationStatus); + Assert.AreEqual(MemberEditingOperationStatus.Success, result.Status.MemberEditingOperationStatus); + + var member = result.Result.Content; + Assert.IsNotNull(member); + Assert.IsTrue(member.HasIdentity); + Assert.Greater(member.Id, 0); + + return await MemberEditingService.GetAsync(member.Key) ?? throw new ApplicationException("Created member could not be retrieved"); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs index 20f908a35938..4f184531fde0 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Mapping; @@ -264,6 +265,6 @@ private void MockMemberServiceForCreateMember(IMember fakeMember) _mockMemberService .Setup(x => x.CreateMember(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(fakeMember); - _mockMemberService.Setup(x => x.Save(fakeMember)); + _mockMemberService.Setup(x => x.Save(fakeMember, Constants.Security.SuperUserId)); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs index 87e0f17dff2b..891791e44fbd 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; using NUnit.Framework; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; @@ -90,7 +91,7 @@ public async Task GivenICreateUser_AndTheUserDoesNotHaveIdentity_ThenIShouldGetA _mockMemberService .Setup(x => x.CreateMember(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(mockMember); - _mockMemberService.Setup(x => x.Save(mockMember)); + _mockMemberService.Setup(x => x.Save(mockMember, Constants.Security.SuperUserId)); // act var actual = await sut.CreateAsync(null); @@ -122,7 +123,7 @@ public async Task GivenICreateANewUser_AndTheUserIsPopulatedCorrectly_ThenIShoul _mockMemberService .Setup(x => x.CreateMember(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(mockMember); - _mockMemberService.Setup(x => x.Save(mockMember)); + _mockMemberService.Setup(x => x.Save(mockMember, Constants.Security.SuperUserId)); // act var identityResult = await sut.CreateAsync(fakeUser, CancellationToken.None); @@ -132,7 +133,7 @@ public async Task GivenICreateANewUser_AndTheUserIsPopulatedCorrectly_ThenIShoul Assert.IsTrue(!identityResult.Errors.Any()); _mockMemberService.Verify(x => x.CreateMember(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); - _mockMemberService.Verify(x => x.Save(mockMember)); + _mockMemberService.Verify(x => x.Save(mockMember, Constants.Security.SuperUserId)); } [Test] @@ -176,7 +177,7 @@ public async Task GivenIUpdateAUser_ThenIShouldGetASuccessResultAsync() m.RawPasswordValue == "xyz" && m.SecurityStamp == "xyz"); - _mockMemberService.Setup(x => x.Save(mockMember)); + _mockMemberService.Setup(x => x.Save(mockMember, Constants.Security.SuperUserId)); _mockMemberService.Setup(x => x.GetById(123)).Returns(mockMember); // act @@ -199,7 +200,7 @@ public async Task GivenIUpdateAUser_ThenIShouldGetASuccessResultAsync() Assert.AreEqual(fakeUser.SecurityStamp, mockMember.SecurityStamp); Assert.AreNotEqual(DateTime.MinValue, mockMember.EmailConfirmedDate.Value); - _mockMemberService.Verify(x => x.Save(mockMember)); + _mockMemberService.Verify(x => x.Save(mockMember, Constants.Security.SuperUserId)); _mockMemberService.Verify(x => x.GetById(123)); _mockMemberService.Verify(x => x.ReplaceRoles(new[] { 123 }, new[] { "role1", "role2" })); } @@ -242,7 +243,7 @@ public async Task GivenIDeleteUser_AndTheUserIsDeletedCorrectly_ThenIShouldGetAS _mockMemberService.Setup(x => x.GetById(mockMember.Id)).Returns(mockMember); _mockMemberService.Setup(x => x.GetByKey(mockMember.Key)).Returns(mockMember); - _mockMemberService.Setup(x => x.Delete(mockMember)); + _mockMemberService.Setup(x => x.Delete(mockMember, Constants.Security.SuperUserId)); // act var identityResult = await sut.DeleteAsync(fakeUser, fakeCancellationToken); @@ -251,7 +252,7 @@ public async Task GivenIDeleteUser_AndTheUserIsDeletedCorrectly_ThenIShouldGetAS Assert.IsTrue(identityResult.Succeeded); Assert.IsTrue(!identityResult.Errors.Any()); _mockMemberService.Verify(x => x.GetByKey(mockMember.Key)); - _mockMemberService.Verify(x => x.Delete(mockMember)); + _mockMemberService.Verify(x => x.Delete(mockMember, Constants.Security.SuperUserId)); _mockMemberService.VerifyNoOtherCalls(); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs index 6d6d10d3bb9a..d3745452d2c1 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -496,7 +496,7 @@ public async Task PostSaveMember_SaveExistingMember_WithNoRoles_Add1Role_ExpectS Mock.Get(umbracoMembersUserManager) .Verify(x => x.GetRolesAsync(It.IsAny())); Mock.Get(memberService) - .Verify(m => m.Save(It.IsAny())); + .Verify(m => m.Save(It.IsAny(), It.IsAny())); AssertMemberDisplayPropertiesAreEqual(memberDisplay, result.Value); }