V14: Current user controller (#14323)

* Add current user data endpoint

* Add Change password endpoint

* Add SetAvatar

* Add get node permissions

* Add endpoint for getting currently logged in users linked logins

* Add tour service

* Add get tours

* Add set tour endpoint

* Split permissions endpoint in two, one for media and one for document
This commit is contained in:
Mole
2023-06-05 08:42:29 +02:00
committed by GitHub
parent 4a07f9a839
commit 0ad0179cd6
37 changed files with 814 additions and 4 deletions

View File

@@ -0,0 +1,47 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tour;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
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.Tour;
[ApiVersion("1.0")]
public class GetTourController : TourControllerBase
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly ITourService _tourService;
private readonly IUmbracoMapper _umbracoMapper;
public GetTourController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
ITourService tourService,
IUmbracoMapper umbracoMapper)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_tourService = tourService;
_umbracoMapper = umbracoMapper;
}
[HttpGet]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(UserTourStatusesResponseModel), StatusCodes.Status200OK)]
public async Task<IActionResult> GetTours()
{
Guid currentUserKey = CurrentUserKey(_backOfficeSecurityAccessor);
Attempt<IEnumerable<UserTourStatus>, TourOperationStatus> toursAttempt = await _tourService.GetAllAsync(currentUserKey);
if (toursAttempt.Success == false)
{
return TourOperationStatusResult(toursAttempt.Status);
}
List<TourStatusViewModel> models = _umbracoMapper.MapEnumerable<UserTourStatus, TourStatusViewModel>(toursAttempt.Result);
return Ok(new UserTourStatusesResponseModel { TourStatuses = models });
}
}

View File

@@ -0,0 +1,42 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tour;
using Umbraco.Cms.Core.Mapping;
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.Tour;
[ApiVersion("1.0")]
public class SetTourController : TourControllerBase
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly ITourService _tourService;
private readonly IUmbracoMapper _umbracoMapper;
public SetTourController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
ITourService tourService,
IUmbracoMapper umbracoMapper)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_tourService = tourService;
_umbracoMapper = umbracoMapper;
}
[HttpPost]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> SetTour(SetTourStatusRequestModel model)
{
Guid currentUserKey = CurrentUserKey(_backOfficeSecurityAccessor);
UserTourStatus tourStatus = _umbracoMapper.Map<UserTourStatus>(model)!;
TourOperationStatus attempt = await _tourService.SetAsync(tourStatus, currentUserKey);
return TourOperationStatusResult(attempt);
}
}

View File

@@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.Builders;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Tour;
[ApiController]
[VersionedApiBackOfficeRoute("tour")]
[ApiExplorerSettings(GroupName = "Tour")]
public class TourControllerBase : ManagementApiControllerBase
{
protected IActionResult TourOperationStatusResult(TourOperationStatus status) =>
status switch
{
TourOperationStatus.Success => Ok(),
TourOperationStatus.UserNotFound => NotFound(new ProblemDetailsBuilder()
.WithTitle("User not found")
.WithDetail("Was not able to find currently logged in user")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown tour operation status.")
};
}

View File

@@ -20,7 +20,8 @@ public class ChangePasswordUserController : UserControllerBase
public ChangePasswordUserController(
IUserService userService,
IUmbracoMapper mapper, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
IUmbracoMapper mapper,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_userService = userService;
_mapper = mapper;
@@ -29,6 +30,7 @@ public class ChangePasswordUserController : UserControllerBase
[HttpPost("change-password/{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesErrorResponseType(typeof(ChangePasswordUserResponseModel))]
public async Task<IActionResult> ChangePassword(Guid id, ChangePasswordUserRequestModel model)
{
var passwordModel = new ChangeUserPasswordModel

View File

@@ -0,0 +1,51 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.User;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.User.Current;
[ApiVersion("1.0")]
public class ChangePasswordCurrentUserController : CurrentUserControllerBase
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IUserService _userService;
private readonly IUmbracoMapper _mapper;
public ChangePasswordCurrentUserController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IUserService userService,
IUmbracoMapper mapper)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_userService = userService;
_mapper = mapper;
}
[HttpPost("change-password")]
[MapToApiVersion("1.0")]
[ProducesErrorResponseType(typeof(ChangePasswordUserResponseModel))]
public async Task<IActionResult> ChangePassword(ChangePasswordUserRequestModel model)
{
Guid userKey = CurrentUserKey(_backOfficeSecurityAccessor);
var changeModel = new ChangeUserPasswordModel
{
NewPassword = model.NewPassword,
OldPassword = model.OldPassword,
UserKey = userKey,
};
Attempt<PasswordChangedModel, UserOperationStatus> response = await _userService.ChangePasswordAsync(userKey, changeModel);
return response.Success
? Ok(_mapper.Map<ChangePasswordUserResponseModel>(response.Result))
: UserOperationStatusResult(response.Status, response.Result);
}
}

View File

@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Routing;
namespace Umbraco.Cms.Api.Management.Controllers.User.Current;
[ApiController]
[VersionedApiBackOfficeRoute("user/current")]
[ApiExplorerSettings(GroupName = "User")]
public abstract class CurrentUserControllerBase : UserControllerBase
{
}

View File

@@ -0,0 +1,33 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.User.Current;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.User.Current;
[ApiVersion("1.0")]
public class GetDataCurrentUserController : CurrentUserControllerBase
{
private readonly IUserDataService _userDataService;
private readonly IUmbracoMapper _mapper;
public GetDataCurrentUserController(
IUserDataService userDataService,
IUmbracoMapper mapper)
{
_userDataService = userDataService;
_mapper = mapper;
}
[HttpGet("data")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(UserDataResponseModel), StatusCodes.Status200OK)]
public Task<IActionResult> GetUserData()
{
IEnumerable<UserDataViewModel?> userData = _userDataService.GetUserData().Select(x => _mapper.Map<UserDataViewModel>(x));
return Task.FromResult<IActionResult>(Ok(new UserDataResponseModel { UserData = userData! }));
}
}

View File

@@ -0,0 +1,46 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.User.Current;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
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.User.Current;
public class GetDocumentPermissionsCurrentUserController : CurrentUserControllerBase
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IUserService _userService;
private readonly IUmbracoMapper _mapper;
public GetDocumentPermissionsCurrentUserController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IUserService userService,
IUmbracoMapper mapper)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_userService = userService;
_mapper = mapper;
}
[MapToApiVersion("1.0")]
[HttpGet("permissions/document")]
[ProducesResponseType(typeof(IEnumerable<UserPermissionsResponseModel>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetPermissions([FromQuery(Name = "id")] HashSet<Guid> ids)
{
Attempt<IEnumerable<NodePermissions>, UserOperationStatus> permissionsAttempt = await _userService.GetDocumentPermissionsAsync(CurrentUserKey(_backOfficeSecurityAccessor), ids);
if (permissionsAttempt.Success is false)
{
return UserOperationStatusResult(permissionsAttempt.Status);
}
List<UserPermissionViewModel> viewModels = _mapper.MapEnumerable<NodePermissions, UserPermissionViewModel>(permissionsAttempt.Result);
return Ok(new UserPermissionsResponseModel { Permissions = viewModels });
}
}

View File

@@ -0,0 +1,48 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.User;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.User.Current;
[ApiVersion("1.0")]
public class GetLinkedLoginsCurrentUserController : CurrentUserControllerBase
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IUserService _userService;
private readonly IUmbracoMapper _umbracoMapper;
public GetLinkedLoginsCurrentUserController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IUserService userService,
IUmbracoMapper umbracoMapper)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_userService = userService;
_umbracoMapper = umbracoMapper;
}
[MapToApiVersion("1.0")]
[HttpGet("logins")]
[ProducesResponseType(typeof(LinkedLoginsRequestModel), StatusCodes.Status200OK)]
public async Task<IActionResult> GetLinkedLogins()
{
Guid currentUserKey = CurrentUserKey(_backOfficeSecurityAccessor);
Attempt<ICollection<IIdentityUserLogin>, UserOperationStatus> linkedLoginsAttempt = await _userService.GetLinkedLoginsAsync(currentUserKey);
if (linkedLoginsAttempt.Success == false)
{
return UserOperationStatusResult(linkedLoginsAttempt.Status);
}
List<LinkedLoginViewModel> models = _umbracoMapper.MapEnumerable<IIdentityUserLogin, LinkedLoginViewModel>(linkedLoginsAttempt.Result);
return Ok(new LinkedLoginsRequestModel { LinkedLogins = models });
}
}

View File

@@ -0,0 +1,46 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.User.Current;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
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.User.Current;
public class GetMediaPermissionsCurrentUserController : CurrentUserControllerBase
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IUserService _userService;
private readonly IUmbracoMapper _mapper;
public GetMediaPermissionsCurrentUserController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IUserService userService,
IUmbracoMapper mapper)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_userService = userService;
_mapper = mapper;
}
[MapToApiVersion("1.0")]
[HttpGet("permissions/media")]
[ProducesResponseType(typeof(IEnumerable<UserPermissionsResponseModel>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetPermissions([FromQuery(Name = "id")] HashSet<Guid> ids)
{
Attempt<IEnumerable<NodePermissions>, UserOperationStatus> permissionsAttempt = await _userService.GetMediaPermissionsAsync(CurrentUserKey(_backOfficeSecurityAccessor), ids);
if (permissionsAttempt.Success is false)
{
return UserOperationStatusResult(permissionsAttempt.Status);
}
List<UserPermissionViewModel> viewModels = _mapper.MapEnumerable<NodePermissions, UserPermissionViewModel>(permissionsAttempt.Result);
return Ok(new UserPermissionsResponseModel { Permissions = viewModels });
}
}

View File

@@ -0,0 +1,38 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.User;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.User.Current;
[ApiVersion("1.0")]
public class SetAvatarCurrentUserController : CurrentUserControllerBase
{
private readonly IUserService _userService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public SetAvatarCurrentUserController(
IUserService userService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_userService = userService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
[MapToApiVersion("1.0")]
[HttpPost("avatar")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> SetAvatar(SetAvatarRequestModel model)
{
Guid userKey = CurrentUserKey(_backOfficeSecurityAccessor);
UserOperationStatus result = await _userService.SetAvatarAsync(userKey, model.FileId);
return result is UserOperationStatus.Success
? Ok()
: UserOperationStatusResult(result);
}
}

View File

@@ -1,5 +1,4 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.Builders;
using Umbraco.Cms.Api.Management.Routing;
@@ -96,6 +95,14 @@ public abstract class UserControllerBase : ManagementApiControllerBase
.WithTitle("Invalid ISO code")
.WithDetail("The specified ISO code is invalid.")
.Build()),
UserOperationStatus.MediaNodeNotFound => NotFound(new ProblemDetailsBuilder()
.WithTitle("Media node not found")
.WithDetail("The specified media node was not found.")
.Build()),
UserOperationStatus.ContentNodeNotFound => NotFound(new ProblemDetailsBuilder()
.WithTitle("Content node not found")
.WithDetail("The specified content node was not found.")
.Build()),
UserOperationStatus.Forbidden => Forbid(),
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown user operation status."),
};

View File

@@ -0,0 +1,16 @@
using Umbraco.Cms.Api.Management.Mapping.Tour;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
namespace Umbraco.Cms.Api.Management.DependencyInjection;
internal static class TourBuilderExtensions
{
internal static IUmbracoBuilder AddTours(this IUmbracoBuilder builder)
{
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>()
.Add<TourViewModelsMapDefinition>();
return builder;
}
}

View File

@@ -13,7 +13,8 @@ internal static class UsersBuilderExtensions
builder.Services.AddTransient<IUserPresentationFactory, UserPresentationFactory>();
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>()
.Add<UsersViewModelsMapDefinition>();
.Add<UsersViewModelsMapDefinition>()
.Add<CurrentUserViewModelsMapDefinition>();
return builder;
}

View File

@@ -46,6 +46,7 @@ public class ManagementApiComposer : IComposer
.AddLogViewer()
.AddUsers()
.AddUserGroups()
.AddTours()
.AddPackages()
.AddEntities()
.AddPathFolders()

View File

@@ -0,0 +1,30 @@
using Umbraco.Cms.Api.Management.ViewModels.Tour;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Api.Management.Mapping.Tour;
public class TourViewModelsMapDefinition : IMapDefinition
{
public void DefineMaps(IUmbracoMapper mapper)
{
mapper.Define<UserTourStatus, TourStatusViewModel>((_, _) => new TourStatusViewModel{ Alias = string.Empty}, Map);
mapper.Define<SetTourStatusRequestModel, UserTourStatus>((_, _) => new UserTourStatus(), Map);
}
// Umbraco.Code.MapAll
private void Map(SetTourStatusRequestModel source, UserTourStatus target, MapperContext context)
{
target.Alias = source.Alias;
target.Completed = source.Completed;
target.Disabled = source.Disabled;
}
// Umbraco.Code.MapAll
private void Map(UserTourStatus source, TourStatusViewModel target, MapperContext context)
{
target.Alias = source.Alias;
target.Completed = source.Completed;
target.Disabled = source.Disabled;
}
}

View File

@@ -0,0 +1,28 @@
using Umbraco.Cms.Api.Management.ViewModels.User.Current;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Api.Management.Mapping.Users;
public class CurrentUserViewModelsMapDefinition : IMapDefinition
{
public void DefineMaps(IUmbracoMapper mapper)
{
mapper.Define<UserData, UserDataViewModel>((_, _) => new UserDataViewModel {Data = string.Empty, Name = string.Empty }, Map);
mapper.Define<NodePermissions, UserPermissionViewModel>((_, _) => new UserPermissionViewModel(), Map);
}
// Umbraco.Code.MapAll
private void Map(NodePermissions source, UserPermissionViewModel target, MapperContext context)
{
target.NodeKey = source.NodeKey;
target.Permissions = source.Permissions;
}
// Umbraco.Code.MapAll
private void Map(UserData source, UserDataViewModel target, MapperContext context)
{
target.Name = source.Name;
target.Data = source.Data;
}
}

View File

@@ -2,6 +2,7 @@
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
namespace Umbraco.Cms.Api.Management.Mapping.Users;
@@ -11,14 +12,24 @@ public class UsersViewModelsMapDefinition : IMapDefinition
{
mapper.Define<PasswordChangedModel, ChangePasswordUserResponseModel>((_, _) => new ChangePasswordUserResponseModel(), Map);
mapper.Define<UserCreationResult, CreateUserResponseModel>((_, _) => new CreateUserResponseModel(), Map);
mapper.Define<IIdentityUserLogin, LinkedLoginViewModel>((_, _) => new LinkedLoginViewModel { ProviderKey = string.Empty, ProviderName = string.Empty }, Map);
}
// Umbraco.Code.MapAll
private void Map(IIdentityUserLogin source, LinkedLoginViewModel target, MapperContext context)
{
target.ProviderKey = source.ProviderKey;
target.ProviderName = source.LoginProvider;
}
// Umbraco.Code.MapAll
private void Map(UserCreationResult source, CreateUserResponseModel target, MapperContext context)
{
target.UserId = source.CreatedUser?.Key ?? Guid.Empty;
target.InitialPassword = source.InitialPassword;
}
// Umbraco.Code.MapAll
private void Map(PasswordChangedModel source, ChangePasswordUserResponseModel target, MapperContext context)
{
target.ResetPassword = source.ResetPassword;

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Tour;
public class SetTourStatusRequestModel : TourStatusViewModel
{
}

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Tour;
public class UserTourStatusesResponseModel
{
public required IEnumerable<TourStatusViewModel> TourStatuses { get; set; }
}

View File

@@ -0,0 +1,10 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Tour;
public class TourStatusViewModel
{
public required string Alias { get; set; }
public bool Completed { get; set; }
public bool Disabled { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.User.Current;
public class UserDataResponseModel
{
public IEnumerable<UserDataViewModel> UserData { get; set; } = Enumerable.Empty<UserDataViewModel>();
}

View File

@@ -0,0 +1,8 @@
namespace Umbraco.Cms.Api.Management.ViewModels.User.Current;
public class UserDataViewModel
{
public required string Name { get; set; }
public required string Data { get; set; }
}

View File

@@ -0,0 +1,8 @@
namespace Umbraco.Cms.Api.Management.ViewModels.User.Current;
public class UserPermissionViewModel
{
public Guid NodeKey { get; set; }
public IEnumerable<string> Permissions { get; set; } = Array.Empty<string>();
}

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.User.Current;
public class UserPermissionsResponseModel
{
public IEnumerable<UserPermissionViewModel> Permissions { get; set; } = Array.Empty<UserPermissionViewModel>();
}

View File

@@ -0,0 +1,8 @@
namespace Umbraco.Cms.Api.Management.ViewModels.User;
public class LinkedLoginViewModel
{
public required string ProviderName { get; set; }
public required string ProviderKey { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.User;
public class LinkedLoginsRequestModel
{
public IEnumerable<LinkedLoginViewModel> LinkedLogins { get; set; } = Enumerable.Empty<LinkedLoginViewModel>();
}

View File

@@ -282,6 +282,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
Services.AddTransient<IUserGroupAuthorizationService, UserGroupAuthorizationService>();
Services.AddUnique<IUserGroupService, UserGroupService>();
Services.AddUnique<IUserService, UserService>();
Services.AddUnique<ITourService, TourService>();
Services.AddUnique<IWebProfilerService, WebProfilerService>();
Services.AddUnique<ILocalizationService, LocalizationService>();
Services.AddUnique<IDictionaryItemService, DictionaryItemService>();

View File

@@ -0,0 +1,11 @@
namespace Umbraco.Cms.Core.Models;
/// <summary>
/// A model representing a set of permissions for a given node.
/// </summary>
public class NodePermissions
{
public Guid NodeKey { get; set; }
public IEnumerable<string> Permissions { get; set; } = Array.Empty<string>();
}

View File

@@ -18,4 +18,6 @@ public interface ICoreBackOfficeUserManager
Task<Attempt<string, UserOperationStatus>> GenerateEmailConfirmationTokenAsync(IUser user);
Task<Attempt<UserUnlockResult, UserOperationStatus>> UnlockUser(IUser user);
Task<Attempt<ICollection<IIdentityUserLogin>, UserOperationStatus>> GetLoginsAsync(IUser user);
}

View File

@@ -0,0 +1,22 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services;
public interface ITourService
{
/// <summary>
/// Persists a <see cref="UserTourStatus"/> for a user.
/// </summary>
/// <param name="status">The status to persist.</param>
/// <param name="userKey">The key of the user to persist it for.</param>
/// <returns>An operation status specifying if the operation was successful.</returns>
Task<TourOperationStatus> SetAsync(UserTourStatus status, Guid userKey);
/// <summary>
/// Gets all <see cref="UserTourStatus"/> for a user.
/// </summary>
/// <param name="userKey">The key of the user to get tour data for.</param>
/// <returns>An attempt containing an enumerable of <see cref="UserTourStatus"/> and a status.</returns>
Task<Attempt<IEnumerable<UserTourStatus>, TourOperationStatus>> GetAllAsync(Guid userKey);
}

View File

@@ -1,6 +1,7 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services;
@@ -74,6 +75,8 @@ public interface IUserService : IMembershipUserService
Task<UserOperationStatus> ClearAvatarAsync(Guid userKey);
Task<Attempt<ICollection<IIdentityUserLogin>, UserOperationStatus>> GetLinkedLoginsAsync(Guid userKey);
/// <summary>
/// Gets all users that the requesting user is allowed to see.
/// </summary>
@@ -200,6 +203,22 @@ public interface IUserService : IMembershipUserService
/// <param name="sectionAlias">Alias of the section to remove</param>
void DeleteSectionFromAllUserGroups(string sectionAlias);
/// <summary>
/// Get explicitly assigned content permissions for a user and node keys.
/// </summary>
/// <param name="userKey">Key of user to retrieve permissions for. </param>
/// <param name="mediaKeys">The keys of the media to get permissions for.</param>
/// <returns>An attempt indicating if the operation was a success as well as a more detailed <see cref="UserOperationStatus"/>, and an enumerable of permissions.</returns>
Task<Attempt<IEnumerable<NodePermissions>, UserOperationStatus>> GetMediaPermissionsAsync(Guid userKey, IEnumerable<Guid> mediaKeys);
/// <summary>
/// Get explicitly assigned media permissions for a user and node keys.
/// </summary>
/// <param name="userKey">Key of user to retrieve permissions for. </param>
/// <param name="contentKeys">The keys of the content to get permissions for.</param>
/// <returns>An attempt indicating if the operation was a success as well as a more detailed <see cref="UserOperationStatus"/>, and an enumerable of permissions.</returns>
Task<Attempt<IEnumerable<NodePermissions>, UserOperationStatus>> GetDocumentPermissionsAsync(Guid userKey, IEnumerable<Guid> contentKeys);
/// <summary>
/// Get explicitly assigned permissions for a user and optional node ids
/// </summary>

View File

@@ -0,0 +1,7 @@
namespace Umbraco.Cms.Core.Services.OperationStatus;
public enum TourOperationStatus
{
Success,
UserNotFound,
}

View File

@@ -25,5 +25,7 @@ public enum UserOperationStatus
InvalidIsoCode,
ContentStartNodeNotFound,
MediaStartNodeNotFound,
ContentNodeNotFound,
MediaNodeNotFound,
UnknownFailure,
}

View File

@@ -0,0 +1,88 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services;
/**
* TODO: This implementation is not the greatest,
* ideally we should store tour information in its own table
* making it its own feature, instead of an ad-hoc "add-on" to users.
* additionally we should probably not store it as a JSON blob, but instead as a proper table.
* For now we'll keep doing the deserialize/serialize dance here,
* because there is no reason to spend cycles to deserialize/serialize the tour data every time we fetch/save a user.
*/
public class TourService : ITourService
{
private readonly IJsonSerializer _jsonSerializer;
private readonly IUserService _userService;
public TourService(
IJsonSerializer jsonSerializer,
IUserService userService)
{
_jsonSerializer = jsonSerializer;
_userService = userService;
}
/// <inheritdoc />
public async Task<TourOperationStatus> SetAsync(UserTourStatus status, Guid userKey)
{
IUser? user = await _userService.GetAsync(userKey);
if (user is null)
{
return TourOperationStatus.UserNotFound;
}
// If the user currently have no tour data, we can just add the data and save it.
if (string.IsNullOrWhiteSpace(user.TourData))
{
List<UserTourStatus> tours = new() { status };
user.TourData = _jsonSerializer.Serialize(tours);
_userService.Save(user);
return TourOperationStatus.Success;
}
// Otherwise we have to check it it already exists, and if so, replace it.
List<UserTourStatus> existingTours =
_jsonSerializer.Deserialize<IEnumerable<UserTourStatus>>(user.TourData)?.ToList() ?? new List<UserTourStatus>();
UserTourStatus? found = existingTours.FirstOrDefault(x => x.Alias == status.Alias);
if (found is not null)
{
existingTours.Remove(found);
}
existingTours.Add(status);
user.TourData = _jsonSerializer.Serialize(existingTours);
_userService.Save(user);
return TourOperationStatus.Success;
}
/// <inheritdoc />
public async Task<Attempt<IEnumerable<UserTourStatus>, TourOperationStatus>> GetAllAsync(Guid userKey)
{
IUser? user = await _userService.GetAsync(userKey);
if (user is null)
{
return Attempt.FailWithStatus(TourOperationStatus.UserNotFound, Enumerable.Empty<UserTourStatus>());
}
// No tour data, we'll just return empty.
if (string.IsNullOrWhiteSpace(user.TourData))
{
return Attempt.SucceedWithStatus(TourOperationStatus.Success, Enumerable.Empty<UserTourStatus>());
}
IEnumerable<UserTourStatus> tours = _jsonSerializer.Deserialize<IEnumerable<UserTourStatus>>(user.TourData)
?? Enumerable.Empty<UserTourStatus>();
return Attempt.SucceedWithStatus(TourOperationStatus.Success, tours);
}
}

View File

@@ -1642,6 +1642,26 @@ internal class UserService : RepositoryService, IUserService
return backOfficeUserStore.GetUsersAsync(keys.ToArray());
}
public async Task<Attempt<ICollection<IIdentityUserLogin>, UserOperationStatus>> GetLinkedLoginsAsync(Guid userKey)
{
using IServiceScope scope = _serviceScopeFactory.CreateScope();
IBackOfficeUserStore backOfficeUserStore = scope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
IUser? user = await backOfficeUserStore.GetAsync(userKey);
if (user is null)
{
return Attempt.FailWithStatus<ICollection<IIdentityUserLogin>, UserOperationStatus>(UserOperationStatus.UserNotFound, Array.Empty<IIdentityUserLogin>());
}
ICoreBackOfficeUserManager manager = scope.ServiceProvider.GetRequiredService<ICoreBackOfficeUserManager>();
Attempt<ICollection<IIdentityUserLogin>, UserOperationStatus> loginsAttempt = await manager.GetLoginsAsync(user);
return loginsAttempt.Success is false
? Attempt.FailWithStatus<ICollection<IIdentityUserLogin>, UserOperationStatus>(loginsAttempt.Status, Array.Empty<IIdentityUserLogin>())
: Attempt.SucceedWithStatus(UserOperationStatus.Success, loginsAttempt.Result);
}
public IEnumerable<IUser> GetUsersById(params int[]? ids)
{
using IServiceScope scope = _serviceScopeFactory.CreateScope();
@@ -1900,6 +1920,84 @@ internal class UserService : RepositoryService, IUserService
}
}
/// <inheritdoc/>
public async Task<Attempt<IEnumerable<NodePermissions>, UserOperationStatus>> GetMediaPermissionsAsync(Guid userKey, IEnumerable<Guid> mediaKeys)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope();
Attempt<Dictionary<Guid, int>?> idAttempt = CreateIdKeyMap(mediaKeys, UmbracoObjectTypes.Media);
if (idAttempt.Success is false || idAttempt.Result is null)
{
return Attempt.FailWithStatus(UserOperationStatus.MediaNodeNotFound, Enumerable.Empty<NodePermissions>());
}
Attempt<IEnumerable<NodePermissions>, UserOperationStatus> permissions = await GetPermissionsAsync(userKey, idAttempt.Result);
scope.Complete();
return permissions;
}
/// <inheritdoc/>
public async Task<Attempt<IEnumerable<NodePermissions>, UserOperationStatus>> GetDocumentPermissionsAsync(Guid userKey, IEnumerable<Guid> contentKeys)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope();
Attempt<Dictionary<Guid, int>?> idAttempt = CreateIdKeyMap(contentKeys, UmbracoObjectTypes.Document);
if (idAttempt.Success is false || idAttempt.Result is null)
{
return Attempt.FailWithStatus(UserOperationStatus.ContentNodeNotFound, Enumerable.Empty<NodePermissions>());
}
Attempt<IEnumerable<NodePermissions>, UserOperationStatus> permissions = await GetPermissionsAsync(userKey, idAttempt.Result);
scope.Complete();
return permissions;
}
private async Task<Attempt<IEnumerable<NodePermissions>, UserOperationStatus>> GetPermissionsAsync(Guid userKey, Dictionary<Guid, int> nodes)
{
IUser? user = await GetAsync(userKey);
if (user is null)
{
return Attempt.FailWithStatus(UserOperationStatus.UserNotFound, Enumerable.Empty<NodePermissions>());
}
EntityPermissionCollection permissionsCollection = _userGroupRepository.GetPermissions(
user.Groups.ToArray(),
true,
nodes.Select(x => x.Value).ToArray());
var results = new List<NodePermissions>();
foreach (KeyValuePair<Guid, int> node in nodes)
{
var permissions = permissionsCollection.GetAllPermissions(node.Value).ToArray();
results.Add(new NodePermissions { NodeKey = node.Key, Permissions = permissions });
}
return Attempt.SucceedWithStatus<IEnumerable<NodePermissions>, UserOperationStatus>(UserOperationStatus.Success, results);
}
private Attempt<Dictionary<Guid, int>?> CreateIdKeyMap(IEnumerable<Guid> nodeKeys, UmbracoObjectTypes objectType)
{
// We'll return this as a dictionary we can link the id and key again later.
Dictionary<Guid, int> idKeys = new();
foreach (Guid key in nodeKeys)
{
Attempt<int> idAttempt = _entityService.GetId(key, objectType);
if (idAttempt.Success is false)
{
return Attempt.Fail<Dictionary<Guid, int>?>(null);
}
idKeys[key] = idAttempt.Result;
}
return Attempt.Succeed<Dictionary<Guid, int>?>(idKeys);
}
/// <summary>
/// Get explicitly assigned permissions for a user and optional node ids
/// </summary>

View File

@@ -330,4 +330,16 @@ public class BackOfficeUserManager : UmbracoUserManager<BackOfficeIdentityUser,
return Attempt.SucceedWithStatus(UserOperationStatus.Success, token);
}
public async Task<Attempt<ICollection<IIdentityUserLogin>, UserOperationStatus>> GetLoginsAsync(IUser user)
{
BackOfficeIdentityUser? identityUser = await FindByIdAsync(user.Id.ToString());
if (identityUser is null)
{
return Attempt.FailWithStatus<ICollection<IIdentityUserLogin>, UserOperationStatus>(UserOperationStatus.UserNotFound, Array.Empty<IIdentityUserLogin>());
}
return Attempt.SucceedWithStatus(UserOperationStatus.Success, identityUser.Logins);
}
}