Add member "kind" - and refactor user "type" to "kind" for consistency (#16979)

* Rename UserType to UserKind

* Add MemberKind to tell API members from regular ones

* Remove user kind from invite user endpoint

---------

Co-authored-by: Mads Rasmussen <madsr@hey.com>
This commit is contained in:
Kenn Jacobsen
2024-09-03 10:43:09 +02:00
committed by GitHub
parent 2a6b376f0d
commit 874055eeab
28 changed files with 129 additions and 73 deletions

View File

@@ -10,7 +10,7 @@ internal static class MemberBuilderExtensions
{
internal static IUmbracoBuilder AddMember(this IUmbracoBuilder builder)
{
builder.Services.AddTransient<IMemberPresentationFactory, MemberPresentationFactory>();
builder.Services.AddSingleton<IMemberPresentationFactory, MemberPresentationFactory>();
builder.Services.AddTransient<IMemberEditingPresentationFactory, MemberEditingPresentationFactory>();
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>().Add<MemberMapDefinition>();

View File

@@ -1,7 +1,9 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
using Microsoft.Extensions.Options;
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.Configuration.Models;
using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
@@ -19,19 +21,23 @@ internal sealed class MemberPresentationFactory : IMemberPresentationFactory
private readonly IMemberTypeService _memberTypeService;
private readonly ITwoFactorLoginService _twoFactorLoginService;
private readonly IMemberGroupService _memberGroupService;
private readonly DeliveryApiSettings _deliveryApiSettings;
private IEnumerable<Guid>? _clientCredentialsMemberKeys;
public MemberPresentationFactory(
IUmbracoMapper umbracoMapper,
IMemberService memberService,
IMemberTypeService memberTypeService,
ITwoFactorLoginService twoFactorLoginService,
IMemberGroupService memberGroupService)
IMemberGroupService memberGroupService,
IOptions<DeliveryApiSettings> deliveryApiSettings)
{
_umbracoMapper = umbracoMapper;
_memberService = memberService;
_memberTypeService = memberTypeService;
_twoFactorLoginService = twoFactorLoginService;
_memberGroupService = memberGroupService;
_deliveryApiSettings = deliveryApiSettings.Value;
}
public async Task<MemberResponseModel> CreateResponseModelAsync(IMember member, IUser currentUser)
@@ -39,6 +45,7 @@ internal sealed class MemberPresentationFactory : IMemberPresentationFactory
MemberResponseModel responseModel = _umbracoMapper.Map<MemberResponseModel>(member)!;
responseModel.IsTwoFactorEnabled = await _twoFactorLoginService.IsTwoFactorEnabledAsync(member.Key);
responseModel.Kind = GetMemberKind(member.Key);
IEnumerable<string> roles = _memberService.GetAllRoles(member.Username);
// Get the member groups per role, so we can return the group keys
@@ -71,7 +78,8 @@ internal sealed class MemberPresentationFactory : IMemberPresentationFactory
{
Id = entity.Key,
MemberType = _umbracoMapper.Map<MemberTypeReferenceResponseModel>(entity)!,
Variants = CreateVariantsItemResponseModels(entity)
Variants = CreateVariantsItemResponseModels(entity),
Kind = GetMemberKind(entity.Key)
};
private static IEnumerable<VariantItemResponseModel> CreateVariantsItemResponseModels(ITreeEntity entity)
@@ -108,4 +116,24 @@ internal sealed class MemberPresentationFactory : IMemberPresentationFactory
return responseModel;
}
private MemberKind GetMemberKind(Guid key)
{
if (_clientCredentialsMemberKeys is null)
{
IEnumerable<string> clientCredentialsMemberUserNames = _deliveryApiSettings
.MemberAuthorization?
.ClientCredentialsFlow?
.AssociatedMembers
.Select(m => m.UserName).ToArray()
?? [];
_clientCredentialsMemberKeys = clientCredentialsMemberUserNames
.Select(_memberService.GetByUsername)
.WhereNotNull()
.Select(m => m.Key).ToArray();
}
return _clientCredentialsMemberKeys.Contains(key) ? MemberKind.Api : MemberKind.Default;
}
}

View File

@@ -80,7 +80,7 @@ public class UserPresentationFactory : IUserPresentationFactory
LastLockoutDate = user.LastLockoutDate,
LastPasswordChangeDate = user.LastPasswordChangeDate,
IsAdmin = user.IsAdmin(),
Type = user.Type
Kind = user.Kind
};
return responseModel;
@@ -93,7 +93,7 @@ public class UserPresentationFactory : IUserPresentationFactory
Name = user.Name ?? user.Username,
AvatarUrls = user.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator)
.Select(url => _absoluteUrlBuilder.ToAbsoluteUrl(url).ToString()),
Type = user.Type
Kind = user.Kind
};
public async Task<UserCreateModel> CreateCreationModelAsync(CreateUserRequestModel requestModel)
@@ -105,7 +105,7 @@ public class UserPresentationFactory : IUserPresentationFactory
Name = requestModel.Name,
UserName = requestModel.UserName,
UserGroupKeys = requestModel.UserGroupIds.Select(x => x.Id).ToHashSet(),
Type = requestModel.Type
Kind = requestModel.Kind
};
return await Task.FromResult(createModel);

View File

@@ -17,7 +17,7 @@ public class MemberMapDefinition : ContentMapDefinition<IMember, MemberValueMode
public void DefineMaps(IUmbracoMapper mapper)
=> mapper.Define<IMember, MemberResponseModel>((_, _) => new MemberResponseModel(), Map);
// Umbraco.Code.MapAll -IsTwoFactorEnabled -Groups
// Umbraco.Code.MapAll -IsTwoFactorEnabled -Groups -Kind
private void Map(IMember source, MemberResponseModel target, MapperContext context)
{
target.Id = source.Key;

View File

@@ -35493,8 +35493,8 @@
"CreateUserRequestModel": {
"required": [
"email",
"kind",
"name",
"type",
"userGroupIds",
"userName"
],
@@ -35525,8 +35525,8 @@
"format": "uuid",
"nullable": true
},
"type": {
"$ref": "#/components/schemas/UserTypeModel"
"kind": {
"$ref": "#/components/schemas/UserKindModel"
}
},
"additionalProperties": false
@@ -38244,7 +38244,6 @@
"required": [
"email",
"name",
"type",
"userGroupIds",
"userName"
],
@@ -38275,9 +38274,6 @@
"format": "uuid",
"nullable": true
},
"type": {
"$ref": "#/components/schemas/UserTypeModel"
},
"message": {
"type": "string",
"nullable": true
@@ -39415,6 +39411,7 @@
"MemberItemResponseModel": {
"required": [
"id",
"kind",
"memberType",
"variants"
],
@@ -39440,10 +39437,20 @@
}
]
}
},
"kind": {
"$ref": "#/components/schemas/MemberKindModel"
}
},
"additionalProperties": false
},
"MemberKindModel": {
"enum": [
"Default",
"Api"
],
"type": "string"
},
"MemberResponseModel": {
"required": [
"email",
@@ -39453,6 +39460,7 @@
"isApproved",
"isLockedOut",
"isTwoFactorEnabled",
"kind",
"memberType",
"username",
"values",
@@ -39531,6 +39539,9 @@
"type": "string",
"format": "uuid"
}
},
"kind": {
"$ref": "#/components/schemas/MemberKindModel"
}
},
"additionalProperties": false
@@ -45082,8 +45093,8 @@
"required": [
"avatarUrls",
"id",
"name",
"type"
"kind",
"name"
],
"type": "object",
"properties": {
@@ -45100,12 +45111,19 @@
"type": "string"
}
},
"type": {
"$ref": "#/components/schemas/UserTypeModel"
"kind": {
"$ref": "#/components/schemas/UserKindModel"
}
},
"additionalProperties": false
},
"UserKindModel": {
"enum": [
"Default",
"Api"
],
"type": "string"
},
"UserOrderModel": {
"enum": [
"UserName",
@@ -45172,10 +45190,10 @@
"hasMediaRootAccess",
"id",
"isAdmin",
"kind",
"mediaStartNodeIds",
"name",
"state",
"type",
"updateDate",
"userGroupIds",
"userName"
@@ -45277,8 +45295,8 @@
"isAdmin": {
"type": "boolean"
},
"type": {
"$ref": "#/components/schemas/UserTypeModel"
"kind": {
"$ref": "#/components/schemas/UserKindModel"
}
},
"additionalProperties": false
@@ -45339,13 +45357,6 @@
},
"additionalProperties": false
},
"UserTypeModel": {
"enum": [
"Default",
"Api"
],
"type": "string"
},
"VariantItemResponseModel": {
"required": [
"name"
@@ -45566,4 +45577,4 @@
}
}
}
}
}

View File

@@ -1,6 +1,7 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
using Umbraco.Cms.Api.Management.ViewModels.Item;
using Umbraco.Cms.Api.Management.ViewModels.MemberType;
using Umbraco.Cms.Core.Models.Membership;
namespace Umbraco.Cms.Api.Management.ViewModels.Member.Item;
@@ -9,4 +10,6 @@ public class MemberItemResponseModel : ItemResponseModelBase
public MemberTypeReferenceResponseModel MemberType { get; set; } = new();
public IEnumerable<VariantItemResponseModel> Variants { get; set; } = Enumerable.Empty<VariantItemResponseModel>();
public MemberKind Kind { get; set; }
}

View File

@@ -1,5 +1,6 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
using Umbraco.Cms.Api.Management.ViewModels.MemberType;
using Umbraco.Cms.Core.Models.Membership;
namespace Umbraco.Cms.Api.Management.ViewModels.Member;
@@ -26,4 +27,6 @@ public class MemberResponseModel : ContentResponseModelBase<MemberValueModel, Me
public DateTimeOffset? LastPasswordChangeDate { get; set; }
public IEnumerable<Guid> Groups { get; set; } = [];
public MemberKind Kind { get; set; }
}

View File

@@ -2,9 +2,7 @@
namespace Umbraco.Cms.Api.Management.ViewModels.User;
public class CreateUserRequestModel : UserPresentationBase
public class CreateUserRequestModel : CreateUserRequestModelBase
{
public Guid? Id { get; set; }
public UserType Type { get; set; }
public UserKind Kind { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.User;
public class CreateUserRequestModelBase : UserPresentationBase
{
public Guid? Id { get; set; }
}

View File

@@ -1,6 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.User;
public class InviteUserRequestModel : CreateUserRequestModel
public class InviteUserRequestModel : CreateUserRequestModelBase
{
public string? Message { get; set; }
}

View File

@@ -7,5 +7,5 @@ public class UserItemResponseModel : NamedItemResponseModelBase
{
public IEnumerable<string> AvatarUrls { get; set; } = Enumerable.Empty<string>();
public UserType Type { get; set; }
public UserKind Kind { get; set; }
}

View File

@@ -34,5 +34,5 @@ public class UserResponseModel : UserPresentationBase
public bool IsAdmin { get; set; }
public UserType Type { get; set; }
public UserKind Kind { get; set; }
}

View File

@@ -42,7 +42,7 @@ public interface IUser : IMembershipUser, IRememberBeingDirty
/// <summary>
/// The type of user.
/// </summary>
UserType Type { get; set; }
UserKind Kind { get; set; }
void RemoveGroup(string group);

View File

@@ -0,0 +1,7 @@
namespace Umbraco.Cms.Core.Models.Membership;
public enum MemberKind
{
Default = 0,
Api
}

View File

@@ -41,7 +41,7 @@ public class User : EntityBase, IUser, IProfile
private HashSet<IReadOnlyUserGroup> _userGroups;
private string _username;
private UserType _type;
private UserKind _kind;
/// <summary>
/// Constructor for creating a new/empty user
@@ -359,10 +359,10 @@ public class User : EntityBase, IUser, IProfile
}
[DataMember]
public UserType Type
public UserKind Kind
{
get => _type;
set => SetPropertyValueAndDetectChanges(value, ref _type, nameof(Type));
get => _kind;
set => SetPropertyValueAndDetectChanges(value, ref _kind, nameof(Kind));
}
/// <summary>

View File

@@ -1,6 +1,6 @@
namespace Umbraco.Cms.Core.Models.Membership;
public enum UserType
public enum UserKind
{
Default = 0,
Api

View File

@@ -12,7 +12,7 @@ public class UserCreateModel
public string Name { get; set; } = string.Empty;
public UserType Type { get; set; }
public UserKind Kind { get; set; }
public ISet<Guid> UserGroupKeys { get; set; } = new HashSet<Guid>();
}

View File

@@ -1189,7 +1189,7 @@ internal class UserService : RepositoryService, IUserService
return Attempt.FailWithStatus(UserOperationStatus.UserNotFound, new PasswordChangedModel());
}
if (user.Type != UserType.Default)
if (user.Kind != UserKind.Default)
{
return Attempt.FailWithStatus(UserOperationStatus.InvalidUserType, new PasswordChangedModel());
}
@@ -2494,7 +2494,7 @@ internal class UserService : RepositoryService, IUserService
}
IUser? user = await GetAsync(userKey);
if (user is null || user.Type != UserType.Api)
if (user is null || user.Kind != UserKind.Api)
{
return UserClientCredentialsOperationStatus.InvalidUser;
}
@@ -2517,7 +2517,7 @@ internal class UserService : RepositoryService, IUserService
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
IUser? user = _userRepository.GetByClientId(clientId);
return Task.FromResult(user?.Type == UserType.Api ? user : null);
return Task.FromResult(user?.Kind == UserKind.Api ? user : null);
}
public async Task<IEnumerable<string>> GetClientIdsAsync(Guid userKey)

View File

@@ -94,6 +94,6 @@ public class UmbracoPlan : MigrationPlan
// To 15.0.0
To<V_15_0_0.AddUserClientId>("{7F4F31D8-DD71-4F0D-93FC-2690A924D84B}");
To<V_15_0_0.AddTypeToUser>("{1A8835EF-F8AB-4472-B4D8-D75B7C164022}");
To<V_15_0_0.AddKindToUser>("{1A8835EF-F8AB-4472-B4D8-D75B7C164022}");
}
}

View File

@@ -8,12 +8,12 @@ using Umbraco.Cms.Infrastructure.Scoping;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0;
[Obsolete("Remove in Umbraco 18.")]
public class AddTypeToUser : UnscopedMigrationBase
public class AddKindToUser : UnscopedMigrationBase
{
private const string NewColumnName = "type";
private const string NewColumnName = "kind";
private readonly IScopeProvider _scopeProvider;
public AddTypeToUser(IMigrationContext context, IScopeProvider scopeProvider)
public AddKindToUser(IMigrationContext context, IScopeProvider scopeProvider)
: base(context)
=> _scopeProvider = scopeProvider;
@@ -90,7 +90,7 @@ public class AddTypeToUser : UnscopedMigrationBase
CreateDate = x.CreateDate,
UpdateDate = x.UpdateDate,
Avatar = x.Avatar,
Type = 0
Kind = 0
});
Delete.Table(Constants.DatabaseSchema.Tables.User).Do();

View File

@@ -103,10 +103,10 @@ public class UserDto
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime UpdateDate { get; set; } = DateTime.Now;
[Column("type")]
[Column("kind")]
[NullSetting(NullSetting = NullSettings.NotNull)]
[Constraint(Default = 0)]
public short Type { get; set; }
public short Kind { get; set; }
/// <summary>
/// Will hold the media file system relative path of the users custom avatar if they uploaded one

View File

@@ -47,7 +47,7 @@ internal static class UserFactory
user.Avatar = dto.Avatar;
user.EmailConfirmedDate = dto.EmailConfirmedDate;
user.InvitedDate = dto.InvitedDate;
user.Type = (UserType)dto.Type;
user.Kind = (UserKind)dto.Kind;
// reset dirty initial properties (U4-1946)
user.ResetDirtyProperties(false);
@@ -83,7 +83,7 @@ internal static class UserFactory
Avatar = entity.Avatar,
EmailConfirmedDate = entity.EmailConfirmedDate,
InvitedDate = entity.InvitedDate,
Type = (short)entity.Type
Kind = (short)entity.Kind
};
if (entity.StartContentIds is not null)

View File

@@ -21,7 +21,7 @@ public class BackOfficeIdentityUser : UmbracoIdentityUser
private DateTime? _inviteDateUtc;
private int[] _startContentIds;
private int[] _startMediaIds;
private UserType _type;
private UserKind _kind;
/// <summary>
/// Initializes a new instance of the <see cref="BackOfficeIdentityUser" /> class.
@@ -116,10 +116,10 @@ public class BackOfficeIdentityUser : UmbracoIdentityUser
}
}
public UserType Type
public UserKind Kind
{
get => _type;
set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _type, nameof(Type));
get => _kind;
set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _kind, nameof(Kind));
}
/// <summary>
@@ -130,7 +130,7 @@ public class BackOfficeIdentityUser : UmbracoIdentityUser
/// <param name="email">This is allowed to be null (but would need to be filled in if trying to persist this instance)</param>
/// <param name="culture"></param>
/// <param name="name"></param>
public static BackOfficeIdentityUser CreateNew(GlobalSettings globalSettings, string? username, string email, string culture, string? name = null, Guid? id = null, UserType type = UserType.Default)
public static BackOfficeIdentityUser CreateNew(GlobalSettings globalSettings, string? username, string email, string culture, string? name = null, Guid? id = null, UserKind kind = UserKind.Default)
{
if (string.IsNullOrWhiteSpace(username))
{
@@ -156,7 +156,7 @@ public class BackOfficeIdentityUser : UmbracoIdentityUser
user.HasIdentity = false;
user._culture = culture;
user.Name = name;
user.Type = type;
user.Kind = kind;
user.EnableChangeTracking();
return user;
}

View File

@@ -141,7 +141,7 @@ public class BackOfficeUserStore :
StartMediaIds = user.StartMediaIds ?? new int[] { },
IsLockedOut = user.IsLockedOut,
Key = user.Key,
Type = user.Type
Kind = user.Kind
};

View File

@@ -96,7 +96,7 @@ public class IdentityMapDefinition : IMapDefinition
target.SecurityStamp = source.SecurityStamp;
DateTime? lockedOutUntil = source.LastLockoutDate?.AddMinutes(_securitySettings.UserDefaultLockoutTimeInMinutes);
target.LockoutEnd = source.IsLockedOut ? (lockedOutUntil ?? DateTime.MaxValue).ToUniversalTime() : null;
target.Type = source.Type;
target.Kind = source.Kind;
}
// Umbraco.Code.MapAll -Id -LockoutEnabled -PhoneNumber -PhoneNumberConfirmed -ConcurrencyStamp -NormalizedEmail -NormalizedUserName -Roles

View File

@@ -299,7 +299,7 @@ public class BackOfficeUserManager : UmbracoUserManager<BackOfficeIdentityUser,
_globalSettings.DefaultUILanguage,
createModel.Name,
createModel.Id,
createModel.Type);
createModel.Kind);
IdentityResult created = await CreateAsync(identityUser);

View File

@@ -49,7 +49,7 @@ public partial class UserServiceCrudTests
Email = "some@one",
Name = "Some One",
UserGroupKeys = new HashSet<Guid> { userGroup.Key },
Type = UserType.Api
Kind = UserKind.Api
};
var userKey = (await userService.CreateAsync(Constants.Security.SuperUserKey, creationModel, true)).Result.CreatedUser!.Key;

View File

@@ -50,12 +50,12 @@ public partial class UserServiceCrudTests
Assert.IsNotNull(createdUser);
Assert.AreEqual(username, createdUser.Username);
Assert.AreEqual(email, createdUser.Email);
Assert.AreEqual(UserType.Default, createdUser.Type);
Assert.AreEqual(UserKind.Default, createdUser.Kind);
}
[TestCase(UserType.Default)]
[TestCase(UserType.Api)]
public async Task Can_Create_All_User_Types(UserType type)
[TestCase(UserKind.Default)]
[TestCase(UserKind.Api)]
public async Task Can_Create_All_User_Types(UserKind kind)
{
var securitySettings = new SecuritySettings();
var userService = CreateUserService(securitySettings);
@@ -67,7 +67,7 @@ public partial class UserServiceCrudTests
Email = "api@local",
Name = "API user",
UserGroupKeys = new HashSet<Guid> { userGroup.Key },
Type = type
Kind = kind
};
var result = await userService.CreateAsync(Constants.Security.SuperUserKey, creationModel, true);
@@ -76,11 +76,11 @@ public partial class UserServiceCrudTests
Assert.AreEqual(UserOperationStatus.Success, result.Status);
var createdUser = result.Result.CreatedUser;
Assert.IsNotNull(createdUser);
Assert.AreEqual(type, createdUser.Type);
Assert.AreEqual(kind, createdUser.Kind);
var user = await userService.GetAsync(createdUser.Key);
Assert.NotNull(user);
Assert.AreEqual(type, user.Type);
Assert.AreEqual(kind, user.Kind);
}
[Test]