Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Members and member types in the Management API #15662

Merged
merged 17 commits into from Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -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;

Expand Down Expand Up @@ -40,9 +41,10 @@ protected IActionResult CreatedAtAction<T>(Expression<Func<T, string>> 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");

/// <summary>
/// Creates a 403 Forbidden result.
Expand Down
Expand Up @@ -24,6 +24,7 @@ protected IActionResult MediaNotFound()
=> NotFound(problemDetailsBuilder
.WithTitle("The requested Media could not be found")
.Build()));

protected IActionResult MediaEditingOperationStatusResult<TContentModelBase>(
ContentEditingOperationStatus status,
TContentModelBase requestModel,
Expand Down
@@ -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<IActionResult> ByKey(Guid id)
{
IMember? member = await _memberEditingService.GetAsync(id);
if (member == null)
{
return MemberNotFound();
}

MemberResponseModel model = await _memberPresentationFactory.CreateResponseModelAsync(member);
return Ok(model);
}
}
@@ -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<IActionResult> Create(CreateMemberRequestModel createRequestModel)
{
MemberCreateModel model = _memberEditingPresentationFactory.MapCreateModel(createRequestModel);
Attempt<MemberCreateResult, MemberEditingStatus> result = await _memberEditingService.CreateAsync(model, CurrentUser(_backOfficeSecurityAccessor));

return result.Success
? CreatedAtId<ByKeyMemberController>(controller => nameof(controller.ByKey), result.Result.Content!.Key)
: MemberEditingStatusResult(result.Status);
}
}
@@ -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<IActionResult> Delete(Guid id)
{
Attempt<IMember?, MemberEditingStatus> result = await _memberEditingService.DeleteAsync(id, CurrentUserKey(_backOfficeSecurityAccessor));

return result.Success
? Ok()
: MemberEditingStatusResult(result.Status);
}
}
@@ -1,32 +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.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;

[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")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<MemberItemResponseModel>), StatusCodes.Status200OK)]
public async Task<IActionResult> Item([FromQuery(Name = "id")] HashSet<Guid> ids)
{
IEnumerable<IMember> members = await _memberService.GetByKeysAsync(ids.ToArray());
List<MemberItemResponseModel> responseModels = _mapper.MapEnumerable<IMember, MemberItemResponseModel>(members);
return Ok(responseModels);
IEnumerable<IMemberEntitySlim> members = _entityService
.GetAll(UmbracoObjectTypes.Member, ids.ToArray())
.OfType<IMemberEntitySlim>();

IEnumerable<MemberItemResponseModel> responseModels = members.Select(_memberPresentationFactory.CreateItemResponseModel);
return await Task.FromResult(Ok(responseModels));
}
}
@@ -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<TContentModelBase>(
ContentEditingOperationStatus status,
TContentModelBase requestModel,
ContentValidationResult validationResult)
where TContentModelBase : ContentModelBase<MemberValueModel, MemberVariantRequestModel>
=> ContentEditingOperationStatusResult<TContentModelBase, MemberValueModel, MemberVariantRequestModel>(status, requestModel, validationResult);
}
@@ -0,0 +1,51 @@
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.Status404NotFound)]
kjac marked this conversation as resolved.
Show resolved Hide resolved
public async Task<IActionResult> Update(Guid id, UpdateMemberRequestModel updateRequestModel)
{
IMember? member = await _memberEditingService.GetAsync(id);
if (member == null)
{
return MemberNotFound();
}

MemberUpdateModel model = _memberEditingPresentationFactory.MapUpdateModel(updateRequestModel);
Attempt<MemberUpdateResult, MemberEditingStatus> result = await _memberEditingService.UpdateAsync(member, model, CurrentUser(_backOfficeSecurityAccessor));

return result.Success
? Ok()
: MemberEditingStatusResult(result.Status);
}
}
@@ -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<IActionResult> Validate(CreateMemberRequestModel requestModel)
{
MemberCreateModel model = _memberEditingPresentationFactory.MapCreateModel(requestModel);
Attempt<ContentValidationResult, ContentEditingOperationStatus> result = await _memberEditingService.ValidateCreateAsync(model);

return result.Success
? Ok()
: MemberEditingOperationStatusResult(result.Status, requestModel, result.Result);
}
}