From 86d7b4c8fe414c0c0666affa09e42ffb6c52602b Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 17 Nov 2018 20:25:10 +0100 Subject: [PATCH] Better Separation of Concerns. --- .../MongoPersistedGrantStore.cs | 5 +- src/Squidex.Domain.Users.MongoDb/MongoRole.cs | 27 -- .../MongoRoleStore.cs | 62 ++-- src/Squidex.Domain.Users.MongoDb/MongoUser.cs | 150 ++-------- .../MongoUserClaim.cs | 33 --- .../MongoUserLogin.cs | 42 --- .../MongoUserStore.cs | 269 ++++++++++++------ .../MongoUserToken.cs | 26 -- .../DefaultUserResolver.cs | 50 ++++ src/Squidex.Domain.Users/IRole.cs | 14 - src/Squidex.Domain.Users/IRoleFactory.cs | 14 - src/Squidex.Domain.Users/IUserFactory.cs | 6 +- .../PwnedPasswordValidator.cs | 5 +- .../Squidex.Domain.Users.csproj | 1 + .../UserClaimsPrincipalFactoryWithEmail.cs | 7 +- .../UserManagerExtensions.cs | 178 +++++++++--- src/Squidex.Domain.Users/UserValues.cs | 69 +++++ src/Squidex.Domain.Users/UserWithClaims.cs | 48 ++++ .../Identity/SquidexClaimTypes.cs | 2 + src/Squidex.Shared/Users/ExternalLogin.cs | 31 -- src/Squidex.Shared/Users/IUser.cs | 14 - src/Squidex.Shared/Users/UserExtensions.cs | 53 +--- .../Controllers/Users/Models/CreateUserDto.cs | 7 + .../Controllers/Users/Models/UpdateUserDto.cs | 7 + .../Users/UserManagementController.cs | 11 +- .../Config/IdentityServerExtensions.cs | 13 +- .../Config/IdentityServerServices.cs | 9 +- .../Controllers/Account/AccountController.cs | 76 ++--- .../IdentityServer/Controllers/Extensions.cs | 5 +- .../Controllers/Profile/ChangeProfileModel.cs | 6 + .../Controllers/Profile/ProfileController.cs | 49 ++-- .../Controllers/Profile/ProfileVM.cs | 6 +- .../Views/Account/Consent.cshtml | 2 +- .../Views/Profile/Profile.cshtml | 2 +- .../Config/Domain/InfrastructureServices.cs | 8 + src/Squidex/Config/Domain/StoreServices.cs | 6 +- .../Config/Domain/SubscriptionServices.cs | 3 - src/Squidex/WebStartup.cs | 3 - src/Squidex/app/theme/_static.scss | 2 +- 39 files changed, 690 insertions(+), 631 deletions(-) delete mode 100644 src/Squidex.Domain.Users.MongoDb/MongoRole.cs delete mode 100644 src/Squidex.Domain.Users.MongoDb/MongoUserClaim.cs delete mode 100644 src/Squidex.Domain.Users.MongoDb/MongoUserLogin.cs delete mode 100644 src/Squidex.Domain.Users.MongoDb/MongoUserToken.cs create mode 100644 src/Squidex.Domain.Users/DefaultUserResolver.cs delete mode 100644 src/Squidex.Domain.Users/IRole.cs delete mode 100644 src/Squidex.Domain.Users/IRoleFactory.cs create mode 100644 src/Squidex.Domain.Users/UserValues.cs create mode 100644 src/Squidex.Domain.Users/UserWithClaims.cs delete mode 100644 src/Squidex.Shared/Users/ExternalLogin.cs diff --git a/src/Squidex.Domain.Users.MongoDb/Infrastructure/MongoPersistedGrantStore.cs b/src/Squidex.Domain.Users.MongoDb/Infrastructure/MongoPersistedGrantStore.cs index abf05e1ac..3a56c5593 100644 --- a/src/Squidex.Domain.Users.MongoDb/Infrastructure/MongoPersistedGrantStore.cs +++ b/src/Squidex.Domain.Users.MongoDb/Infrastructure/MongoPersistedGrantStore.cs @@ -10,7 +10,9 @@ using System.Threading; using System.Threading.Tasks; using IdentityServer4.Models; using IdentityServer4.Stores; +using MongoDB.Bson; using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; using MongoDB.Driver; using Squidex.Infrastructure.MongoDb; @@ -23,7 +25,8 @@ namespace Squidex.Domain.Users.MongoDb.Infrastructure BsonClassMap.RegisterClassMap(map => { map.AutoMap(); - map.MapIdProperty(x => x.Key); + + map.MapIdProperty(x => x.Key).SetSerializer(new StringSerializer(BsonType.ObjectId)); }); } diff --git a/src/Squidex.Domain.Users.MongoDb/MongoRole.cs b/src/Squidex.Domain.Users.MongoDb/MongoRole.cs deleted file mode 100644 index c49d2e1b7..000000000 --- a/src/Squidex.Domain.Users.MongoDb/MongoRole.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; - -namespace Squidex.Domain.Users.MongoDb -{ - public sealed class MongoRole : IRole - { - [BsonRepresentation(BsonType.ObjectId)] - [BsonElement] - public string Id { get; set; } - - [BsonRequired] - [BsonElement] - public string Name { get; set; } - - [BsonRequired] - [BsonElement] - public string NormalizedName { get; set; } - } -} diff --git a/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs b/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs index 8043b89e2..f4588a9b7 100644 --- a/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs +++ b/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs @@ -8,14 +8,28 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; using MongoDB.Driver; using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.Tasks; namespace Squidex.Domain.Users.MongoDb { - public sealed class MongoRoleStore : MongoRepositoryBase, IRoleStore, IRoleFactory + public sealed class MongoRoleStore : MongoRepositoryBase, IRoleStore { + static MongoRoleStore() + { + BsonClassMap.RegisterClassMap>(cm => + { + cm.AutoMap(); + + cm.MapMember(x => x.Id).SetSerializer(new StringSerializer(BsonType.ObjectId)); + cm.UnmapMember(x => x.ConcurrencyStamp); + }); + } + public MongoRoleStore(IMongoDatabase database) : base(database) { @@ -26,10 +40,10 @@ namespace Squidex.Domain.Users.MongoDb return "Identity_Roles"; } - protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default(CancellationToken)) + protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default(CancellationToken)) { return collection.Indexes.CreateOneAsync( - new CreateIndexModel(Index.Ascending(x => x.NormalizedName), new CreateIndexOptions { Unique = true }), cancellationToken: ct); + new CreateIndexModel(Index.Ascending(x => x.NormalizedName), new CreateIndexOptions { Unique = true }), cancellationToken: ct); } protected override MongoCollectionSettings CollectionSettings() @@ -41,69 +55,69 @@ namespace Squidex.Domain.Users.MongoDb { } - public IRole Create(string name) + public IdentityRole Create(string name) { - return new MongoRole { Name = name }; + return new IdentityRole { Name = name }; } - public async Task FindByIdAsync(string roleId, CancellationToken cancellationToken) + public async Task FindByIdAsync(string roleId, CancellationToken cancellationToken) { return await Collection.Find(x => x.Id == roleId).FirstOrDefaultAsync(cancellationToken); } - public async Task FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken) + public async Task FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken) { return await Collection.Find(x => x.NormalizedName == normalizedRoleName).FirstOrDefaultAsync(cancellationToken); } - public async Task CreateAsync(IRole role, CancellationToken cancellationToken) + public async Task CreateAsync(IdentityRole role, CancellationToken cancellationToken) { - await Collection.InsertOneAsync((MongoRole)role, null, cancellationToken); + await Collection.InsertOneAsync(role, null, cancellationToken); return IdentityResult.Success; } - public async Task UpdateAsync(IRole role, CancellationToken cancellationToken) + public async Task UpdateAsync(IdentityRole role, CancellationToken cancellationToken) { - await Collection.ReplaceOneAsync(x => x.Id == ((MongoRole)role).Id, (MongoRole)role, null, cancellationToken); + await Collection.ReplaceOneAsync(x => x.Id == role.Id, role, null, cancellationToken); return IdentityResult.Success; } - public async Task DeleteAsync(IRole role, CancellationToken cancellationToken) + public async Task DeleteAsync(IdentityRole role, CancellationToken cancellationToken) { - await Collection.DeleteOneAsync(x => x.Id == ((MongoRole)role).Id, null, cancellationToken); + await Collection.DeleteOneAsync(x => x.Id == role.Id, null, cancellationToken); return IdentityResult.Success; } - public Task GetRoleIdAsync(IRole role, CancellationToken cancellationToken) + public Task GetRoleIdAsync(IdentityRole role, CancellationToken cancellationToken) { - return Task.FromResult(((MongoRole)role).Id); + return Task.FromResult(role.Id); } - public Task GetRoleNameAsync(IRole role, CancellationToken cancellationToken) + public Task GetRoleNameAsync(IdentityRole role, CancellationToken cancellationToken) { - return Task.FromResult(((MongoRole)role).Name); + return Task.FromResult(role.Name); } - public Task GetNormalizedRoleNameAsync(IRole role, CancellationToken cancellationToken) + public Task GetNormalizedRoleNameAsync(IdentityRole role, CancellationToken cancellationToken) { - return Task.FromResult(((MongoRole)role).NormalizedName); + return Task.FromResult(role.NormalizedName); } - public Task SetRoleNameAsync(IRole role, string roleName, CancellationToken cancellationToken) + public Task SetRoleNameAsync(IdentityRole role, string roleName, CancellationToken cancellationToken) { - ((MongoRole)role).Name = roleName; + role.Name = roleName; return TaskHelper.Done; } - public Task SetNormalizedRoleNameAsync(IRole role, string normalizedName, CancellationToken cancellationToken) + public Task SetNormalizedRoleNameAsync(IdentityRole role, string normalizedName, CancellationToken cancellationToken) { - ((MongoRole)role).NormalizedName = normalizedName; + role.NormalizedName = normalizedName; return TaskHelper.Done; } } -} +} \ No newline at end of file diff --git a/src/Squidex.Domain.Users.MongoDb/MongoUser.cs b/src/Squidex.Domain.Users.MongoDb/MongoUser.cs index a88bf56ad..71df0aa59 100644 --- a/src/Squidex.Domain.Users.MongoDb/MongoUser.cs +++ b/src/Squidex.Domain.Users.MongoDb/MongoUser.cs @@ -5,196 +5,100 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using Microsoft.AspNetCore.Identity; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; using Squidex.Infrastructure; -using Squidex.Shared.Users; namespace Squidex.Domain.Users.MongoDb { - public sealed class MongoUser : IUser + public sealed class MongoUser : IdentityUser { - [BsonRepresentation(BsonType.ObjectId)] - [BsonElement] - public string Id { get; set; } + public List Claims { get; set; } = new List(); - [BsonIgnoreIfNull] - [BsonElement] - public string SecurityStamp { get; set; } + public List Tokens { get; set; } = new List(); - [BsonRequired] - [BsonElement] - public string UserName { get; set; } + public List Logins { get; set; } = new List(); - [BsonRequired] - [BsonElement] - public string NormalizedUserName { get; set; } + public HashSet Roles { get; set; } = new HashSet(); - [BsonRequired] - [BsonElement] - public string Email { get; set; } - - [BsonRequired] - [BsonElement] - public string NormalizedEmail { get; set; } - - [BsonIgnoreIfNull] - [BsonElement] - public string PhoneNumber { get; set; } - - [BsonIgnoreIfNull] - [BsonElement] - public string PasswordHash { get; set; } - - [BsonRequired] - [BsonElement] - public bool EmailConfirmed { get; set; } - - [BsonIgnoreIfDefault] - [BsonElement] - public bool PhoneNumberConfirmed { get; set; } - - [BsonIgnoreIfDefault] - [BsonElement] - public bool TwoFactorEnabled { get; set; } - - [BsonIgnoreIfDefault] - [BsonElement] - public bool LockoutEnabled { get; set; } - - [BsonIgnoreIfNull] - [BsonElement] - public DateTime? LockoutEndDateUtc { get; set; } - - [BsonIgnoreIfDefault] - [BsonElement] - public int AccessFailedCount { get; set; } - - [BsonRequired] - [BsonElement] - public List Roles { get; set; } = new List(); - - [BsonRequired] - [BsonElement] - public List Claims { get; set; } = new List(); - - [BsonRequired] - [BsonElement] - public List Tokens { get; set; } = new List(); - - [BsonRequired] - [BsonElement] - public List Logins { get; set; } = new List(); - - public bool IsLocked - { - get { return LockoutEndDateUtc != null && LockoutEndDateUtc.Value > DateTime.UtcNow; } - } - - IReadOnlyList IUser.Claims - { - get { return Claims.Select(x => new Claim(x.Type, x.Value)).ToList(); } - } - - IReadOnlyList IUser.Logins - { - get { return Logins.Select(x => new ExternalLogin(x.LoginProvider, x.ProviderKey, x.ProviderDisplayName)).ToList(); } - } - - public MongoUser() + internal IdentityUserToken FindTokenAsync(string loginProvider, string name) { - Id = ObjectId.GenerateNewId().ToString(); + return Tokens.FirstOrDefault(x => x.LoginProvider == loginProvider && x.Name == name); } - public void SetEmail(string email) + internal void AddLogin(UserLoginInfo login) { - Email = UserName = email; + Logins.Add(new UserLoginInfo(login.LoginProvider, login.ProviderKey, login.ProviderDisplayName)); } - public void AddRole(string role) + internal void AddRole(string role) { Roles.Add(role); } - public void RemoveRole(string role) + internal void RemoveRole(string role) { Roles.Remove(role); } - public void AddLogin(UserLoginInfo login) - { - Logins.Add(login); - } - - public void RemoveLogin(string loginProvider, string providerKey) + internal void RemoveLogin(string loginProvider, string providerKey) { Logins.RemoveAll(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey); } - public void RemoveClaims(string type) - { - Claims.RemoveAll(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)); - } - - public void AddClaim(Claim claim) + internal void AddClaim(Claim claim) { Claims.Add(claim); } - public void AddClaims(IEnumerable claims) + internal void AddClaims(IEnumerable claims) { claims.Foreach(AddClaim); } - public void RemoveClaim(Claim claim) + internal void RemoveClaim(Claim claim) { Claims.RemoveAll(c => c.Type == claim.Type && c.Value == claim.Value); } - public void RemoveClaims(IEnumerable claims) + internal void RemoveClaims(IEnumerable claims) { claims.Foreach(RemoveClaim); } - public string GetToken(string loginProider, string name) + internal string GetToken(string loginProvider, string name) { - return Tokens.FirstOrDefault(t => t.LoginProvider == loginProider && t.Name == name)?.Value; + return Tokens.FirstOrDefault(t => t.LoginProvider == loginProvider && t.Name == name)?.Value; } - public void AddToken(string loginProvider, string name, string value) + internal void AddToken(string loginProvider, string name, string value) { - Tokens.Add(new MongoUserToken { LoginProvider = loginProvider, Name = name, Value = value }); + Tokens.Add(new UserTokenInfo { LoginProvider = loginProvider, Name = name, Value = value }); } - public void RemoveToken(string loginProvider, string name) + internal void RemoveToken(string loginProvider, string name) { Tokens.RemoveAll(t => t.LoginProvider == loginProvider && t.Name == name); } - public void SetClaim(string type, string value) - { - RemoveClaims(type); - - AddClaim(new Claim(type, value)); - } - - public void ReplaceClaim(Claim existingClaim, Claim newClaim) + internal void ReplaceClaim(Claim existingClaim, Claim newClaim) { RemoveClaim(existingClaim); AddClaim(newClaim); } - public void SetToken(string loginProider, string name, string value) + internal void SetToken(string loginProider, string name, string value) { RemoveToken(loginProider, name); AddToken(loginProider, name, value); } } + + public sealed class UserTokenInfo : IdentityUserToken + { + } } diff --git a/src/Squidex.Domain.Users.MongoDb/MongoUserClaim.cs b/src/Squidex.Domain.Users.MongoDb/MongoUserClaim.cs deleted file mode 100644 index b99421f3d..000000000 --- a/src/Squidex.Domain.Users.MongoDb/MongoUserClaim.cs +++ /dev/null @@ -1,33 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Security.Claims; -using MongoDB.Bson.Serialization.Attributes; - -namespace Squidex.Domain.Users.MongoDb -{ - public sealed class MongoUserClaim - { - [BsonRequired] - [BsonElement] - public string Type { get; set; } - - [BsonRequired] - [BsonElement] - public string Value { get; set; } - - public static implicit operator MongoUserClaim(Claim claim) - { - return new MongoUserClaim { Type = claim.Type, Value = claim.Value }; - } - - public static implicit operator Claim(MongoUserClaim userClaim) - { - return new Claim(userClaim.Type, userClaim.Value); - } - } -} diff --git a/src/Squidex.Domain.Users.MongoDb/MongoUserLogin.cs b/src/Squidex.Domain.Users.MongoDb/MongoUserLogin.cs deleted file mode 100644 index c26abecaa..000000000 --- a/src/Squidex.Domain.Users.MongoDb/MongoUserLogin.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Identity; -using MongoDB.Bson.Serialization.Attributes; - -namespace Squidex.Domain.Users.MongoDb -{ - public sealed class MongoUserLogin - { - [BsonRequired] - [BsonElement] - public string LoginProvider { get; set; } - - [BsonRequired] - [BsonElement] - public string ProviderDisplayName { get; set; } - - [BsonRequired] - [BsonElement] - public string ProviderKey { get; set; } - - public static implicit operator MongoUserLogin(UserLoginInfo login) - { - return new MongoUserLogin - { - LoginProvider = login.LoginProvider, - ProviderKey = login.ProviderKey, - ProviderDisplayName = login.ProviderDisplayName - }; - } - - public static implicit operator UserLoginInfo(MongoUserLogin userLogin) - { - return new UserLoginInfo(userLogin.LoginProvider, userLogin.ProviderKey, userLogin.ProviderDisplayName); - } - } -} diff --git a/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs b/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs index ec3cbde8d..62604760d 100644 --- a/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs +++ b/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs @@ -13,29 +13,96 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; using MongoDB.Driver; using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.Tasks; -using Squidex.Shared.Users; namespace Squidex.Domain.Users.MongoDb { public sealed class MongoUserStore : MongoRepositoryBase, - IUserPasswordStore, - IUserRoleStore, - IUserLoginStore, - IUserSecurityStampStore, - IUserEmailStore, - IUserClaimStore, - IUserPhoneNumberStore, - IUserTwoFactorStore, - IUserLockoutStore, - IUserAuthenticationTokenStore, + IUserAuthenticationTokenStore, + IUserAuthenticatorKeyStore, + IUserClaimStore, + IUserEmailStore, IUserFactory, - IUserResolver, - IQueryableUserStore + IUserLockoutStore, + IUserLoginStore, + IUserPasswordStore, + IUserPhoneNumberStore, + IUserSecurityStampStore, + IUserTwoFactorStore, + IUserTwoFactorRecoveryCodeStore, + IQueryableUserStore { + private const string InternalLoginProvider = "[AspNetUserStore]"; + private const string AuthenticatorKeyTokenName = "AuthenticatorKey"; + private const string RecoveryCodeTokenName = "RecoveryCodes"; + + static MongoUserStore() + { + BsonClassMap.RegisterClassMap(cm => + { + cm.MapConstructor(typeof(Claim).GetConstructors() + .First(x => + { + var parameters = x.GetParameters(); + + return parameters.Length == 2 && + parameters[0].Name == "type" && + parameters[0].ParameterType == typeof(string) && + parameters[1].Name == "value" && + parameters[1].ParameterType == typeof(string); + })) + .SetArguments(new[] + { + nameof(Claim.Type), + nameof(Claim.Value) + }); + + cm.MapMember(x => x.Type); + cm.MapMember(x => x.Value); + }); + + BsonClassMap.RegisterClassMap(cm => + { + cm.MapConstructor(typeof(UserLoginInfo).GetConstructors().First()) + .SetArguments(new[] + { + nameof(UserLoginInfo.LoginProvider), + nameof(UserLoginInfo.ProviderKey), + nameof(UserLoginInfo.ProviderDisplayName) + }); + + cm.AutoMap(); + }); + + BsonClassMap.RegisterClassMap>(cm => + { + cm.AutoMap(); + + cm.UnmapMember(x => x.UserId); + }); + + BsonClassMap.RegisterClassMap>(cm => + { + cm.AutoMap(); + + cm.MapMember(x => x.Id).SetSerializer(new StringSerializer(BsonType.ObjectId)); + cm.MapMember(x => x.AccessFailedCount).SetIgnoreIfDefault(true); + cm.MapMember(x => x.EmailConfirmed).SetIgnoreIfDefault(true); + cm.MapMember(x => x.LockoutEnd).SetElementName("LockoutEndDateUtc").SetIgnoreIfNull(true); + cm.MapMember(x => x.LockoutEnabled).SetIgnoreIfDefault(true); + cm.MapMember(x => x.PasswordHash).SetIgnoreIfNull(true); + cm.MapMember(x => x.PhoneNumber).SetIgnoreIfNull(true); + cm.MapMember(x => x.PhoneNumberConfirmed).SetIgnoreIfDefault(true); + cm.MapMember(x => x.SecurityStamp).SetIgnoreIfNull(true); + cm.MapMember(x => x.TwoFactorEnabled).SetIgnoreIfDefault(true); + }); + } + public MongoUserStore(IMongoDatabase database) : base(database) { @@ -66,352 +133,374 @@ namespace Squidex.Domain.Users.MongoDb { } - public IQueryable Users + public IQueryable Users { get { return Collection.AsQueryable(); } } - public IUser Create(string email) + public bool IsId(string id) + { + return ObjectId.TryParse(id, out var _); + } + + public IdentityUser Create(string email) { return new MongoUser { Email = email, UserName = email }; } - public async Task FindByIdAsync(string userId, CancellationToken cancellationToken) + public async Task FindByIdAsync(string userId, CancellationToken cancellationToken) { return await Collection.Find(x => x.Id == userId).FirstOrDefaultAsync(cancellationToken); } - public async Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken) + public async Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken) { return await Collection.Find(x => x.NormalizedEmail == normalizedEmail).FirstOrDefaultAsync(cancellationToken); } - public async Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) + public async Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) { return await Collection.Find(x => x.NormalizedEmail == normalizedUserName).FirstOrDefaultAsync(cancellationToken); } - public async Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) + public async Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) { return await Collection.Find(x => x.Logins.Any(y => y.LoginProvider == loginProvider && y.ProviderKey == providerKey)).FirstOrDefaultAsync(cancellationToken); } - public async Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken) + public async Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken) { - return (await Collection.Find(x => x.Claims.Any(y => y.Type == claim.Type && y.Value == claim.Value)).ToListAsync(cancellationToken)).OfType().ToList(); + return (await Collection.Find(x => x.Claims.Any(y => y.Type == claim.Type && y.Value == claim.Value)).ToListAsync(cancellationToken)).OfType().ToList(); } - public async Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken) + public async Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken) { - return (await Collection.Find(x => x.Roles.Contains(roleName)).ToListAsync(cancellationToken)).OfType().ToList(); + return (await Collection.Find(x => x.Roles.Contains(roleName)).ToListAsync(cancellationToken)).OfType().ToList(); } - public async Task CreateAsync(IUser user, CancellationToken cancellationToken) + public async Task CreateAsync(IdentityUser user, CancellationToken cancellationToken) { + user.Id = ObjectId.GenerateNewId().ToString(); + await Collection.InsertOneAsync((MongoUser)user, null, cancellationToken); return IdentityResult.Success; } - public async Task UpdateAsync(IUser user, CancellationToken cancellationToken) + public async Task UpdateAsync(IdentityUser user, CancellationToken cancellationToken) { await Collection.ReplaceOneAsync(x => x.Id == user.Id, (MongoUser)user, null, cancellationToken); return IdentityResult.Success; } - public async Task DeleteAsync(IUser user, CancellationToken cancellationToken) + public async Task DeleteAsync(IdentityUser user, CancellationToken cancellationToken) { await Collection.DeleteOneAsync(x => x.Id == user.Id, null, cancellationToken); return IdentityResult.Success; } - public Task GetUserIdAsync(IUser user, CancellationToken cancellationToken) + public Task GetUserIdAsync(IdentityUser user, CancellationToken cancellationToken) { return Task.FromResult(((MongoUser)user).Id); } - public Task GetUserNameAsync(IUser user, CancellationToken cancellationToken) + public Task GetUserNameAsync(IdentityUser user, CancellationToken cancellationToken) { return Task.FromResult(((MongoUser)user).UserName); } - public Task GetNormalizedUserNameAsync(IUser user, CancellationToken cancellationToken) + public Task GetNormalizedUserNameAsync(IdentityUser user, CancellationToken cancellationToken) { return Task.FromResult(((MongoUser)user).NormalizedUserName); } - public Task GetPasswordHashAsync(IUser user, CancellationToken cancellationToken) + public Task GetPasswordHashAsync(IdentityUser user, CancellationToken cancellationToken) { return Task.FromResult(((MongoUser)user).PasswordHash); } - public Task> GetRolesAsync(IUser user, CancellationToken cancellationToken) + public Task> GetRolesAsync(IdentityUser user, CancellationToken cancellationToken) { - return Task.FromResult>(((MongoUser)user).Roles); + return Task.FromResult>(((MongoUser)user).Roles.ToList()); } - public Task IsInRoleAsync(IUser user, string roleName, CancellationToken cancellationToken) + public Task IsInRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken) { return Task.FromResult(((MongoUser)user).Roles.Contains(roleName)); } - public Task> GetLoginsAsync(IUser user, CancellationToken cancellationToken) + public Task> GetLoginsAsync(IdentityUser user, CancellationToken cancellationToken) { - return Task.FromResult>(((MongoUser)user).Logins.Select(x => (UserLoginInfo)x).ToList()); + return Task.FromResult>(((MongoUser)user).Logins.Select(x => new UserLoginInfo(x.LoginProvider, x.ProviderKey, x.ProviderDisplayName)).ToList()); } - public Task GetSecurityStampAsync(IUser user, CancellationToken cancellationToken) + public Task GetSecurityStampAsync(IdentityUser user, CancellationToken cancellationToken) { return Task.FromResult(((MongoUser)user).SecurityStamp); } - public Task GetEmailAsync(IUser user, CancellationToken cancellationToken) + public Task GetEmailAsync(IdentityUser user, CancellationToken cancellationToken) { return Task.FromResult(((MongoUser)user).Email); } - public Task GetEmailConfirmedAsync(IUser user, CancellationToken cancellationToken) + public Task GetEmailConfirmedAsync(IdentityUser user, CancellationToken cancellationToken) { return Task.FromResult(((MongoUser)user).EmailConfirmed); } - public Task GetNormalizedEmailAsync(IUser user, CancellationToken cancellationToken) + public Task GetNormalizedEmailAsync(IdentityUser user, CancellationToken cancellationToken) { return Task.FromResult(((MongoUser)user).NormalizedEmail); } - public Task> GetClaimsAsync(IUser user, CancellationToken cancellationToken) + public Task> GetClaimsAsync(IdentityUser user, CancellationToken cancellationToken) { - return Task.FromResult>(((MongoUser)user).Claims.Select(x => (Claim)x).ToList()); + return Task.FromResult>(((MongoUser)user).Claims); } - public Task GetPhoneNumberAsync(IUser user, CancellationToken cancellationToken) + public Task GetPhoneNumberAsync(IdentityUser user, CancellationToken cancellationToken) { return Task.FromResult(((MongoUser)user).PhoneNumber); } - public Task GetPhoneNumberConfirmedAsync(IUser user, CancellationToken cancellationToken) + public Task GetPhoneNumberConfirmedAsync(IdentityUser user, CancellationToken cancellationToken) { return Task.FromResult(((MongoUser)user).PhoneNumberConfirmed); } - public Task GetTwoFactorEnabledAsync(IUser user, CancellationToken cancellationToken) + public Task GetTwoFactorEnabledAsync(IdentityUser user, CancellationToken cancellationToken) { return Task.FromResult(((MongoUser)user).TwoFactorEnabled); } - public Task GetLockoutEndDateAsync(IUser user, CancellationToken cancellationToken) + public Task GetLockoutEndDateAsync(IdentityUser user, CancellationToken cancellationToken) { - return Task.FromResult(((MongoUser)user).LockoutEndDateUtc); + return Task.FromResult(((MongoUser)user).LockoutEnd); } - public Task GetAccessFailedCountAsync(IUser user, CancellationToken cancellationToken) + public Task GetAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken) { return Task.FromResult(((MongoUser)user).AccessFailedCount); } - public Task GetLockoutEnabledAsync(IUser user, CancellationToken cancellationToken) + public Task GetLockoutEnabledAsync(IdentityUser user, CancellationToken cancellationToken) { return Task.FromResult(((MongoUser)user).LockoutEnabled); } - public Task GetTokenAsync(IUser user, string loginProvider, string name, CancellationToken cancellationToken) + public Task GetTokenAsync(IdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) { return Task.FromResult(((MongoUser)user).GetToken(loginProvider, name)); } - public Task HasPasswordAsync(IUser user, CancellationToken cancellationToken) + public Task GetAuthenticatorKeyAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).GetToken(InternalLoginProvider, AuthenticatorKeyTokenName)); + } + + public Task HasPasswordAsync(IdentityUser user, CancellationToken cancellationToken) { return Task.FromResult(!string.IsNullOrWhiteSpace(((MongoUser)user).PasswordHash)); } - public Task SetUserNameAsync(IUser user, string userName, CancellationToken cancellationToken) + public Task CountCodesAsync(IdentityUser user, CancellationToken cancellationToken) + { + return Task.FromResult(((MongoUser)user).GetToken(InternalLoginProvider, RecoveryCodeTokenName)?.Split(';')?.Length ?? 0); + } + + public Task SetUserNameAsync(IdentityUser user, string userName, CancellationToken cancellationToken) { ((MongoUser)user).UserName = userName; return TaskHelper.Done; } - public Task SetNormalizedUserNameAsync(IUser user, string normalizedName, CancellationToken cancellationToken) + public Task SetNormalizedUserNameAsync(IdentityUser user, string normalizedName, CancellationToken cancellationToken) { ((MongoUser)user).NormalizedUserName = normalizedName; return TaskHelper.Done; } - public Task SetPasswordHashAsync(IUser user, string passwordHash, CancellationToken cancellationToken) + public Task SetPasswordHashAsync(IdentityUser user, string passwordHash, CancellationToken cancellationToken) { ((MongoUser)user).PasswordHash = passwordHash; return TaskHelper.Done; } - public Task AddToRoleAsync(IUser user, string roleName, CancellationToken cancellationToken) + public Task AddToRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken) { ((MongoUser)user).AddRole(roleName); return TaskHelper.Done; } - public Task RemoveFromRoleAsync(IUser user, string roleName, CancellationToken cancellationToken) + public Task RemoveFromRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken) { ((MongoUser)user).RemoveRole(roleName); return TaskHelper.Done; } - public Task AddLoginAsync(IUser user, UserLoginInfo login, CancellationToken cancellationToken) + public Task AddLoginAsync(IdentityUser user, UserLoginInfo login, CancellationToken cancellationToken) { ((MongoUser)user).AddLogin(login); return TaskHelper.Done; } - public Task RemoveLoginAsync(IUser user, string loginProvider, string providerKey, CancellationToken cancellationToken) + public Task RemoveLoginAsync(IdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken) { ((MongoUser)user).RemoveLogin(loginProvider, providerKey); return TaskHelper.Done; } - public Task SetSecurityStampAsync(IUser user, string stamp, CancellationToken cancellationToken) + public Task SetSecurityStampAsync(IdentityUser user, string stamp, CancellationToken cancellationToken) { ((MongoUser)user).SecurityStamp = stamp; return TaskHelper.Done; } - public Task SetEmailAsync(IUser user, string email, CancellationToken cancellationToken) + public Task SetEmailAsync(IdentityUser user, string email, CancellationToken cancellationToken) { ((MongoUser)user).Email = email; return TaskHelper.Done; } - public Task SetEmailConfirmedAsync(IUser user, bool confirmed, CancellationToken cancellationToken) + public Task SetEmailConfirmedAsync(IdentityUser user, bool confirmed, CancellationToken cancellationToken) { ((MongoUser)user).EmailConfirmed = confirmed; return TaskHelper.Done; } - public Task SetNormalizedEmailAsync(IUser user, string normalizedEmail, CancellationToken cancellationToken) + public Task SetNormalizedEmailAsync(IdentityUser user, string normalizedEmail, CancellationToken cancellationToken) { ((MongoUser)user).NormalizedEmail = normalizedEmail; return TaskHelper.Done; } - public Task AddClaimsAsync(IUser user, IEnumerable claims, CancellationToken cancellationToken) + public Task AddClaimsAsync(IdentityUser user, IEnumerable claims, CancellationToken cancellationToken) { ((MongoUser)user).AddClaims(claims); return TaskHelper.Done; } - public Task ReplaceClaimAsync(IUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken) + public Task ReplaceClaimAsync(IdentityUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken) { ((MongoUser)user).ReplaceClaim(claim, newClaim); return TaskHelper.Done; } - public Task RemoveClaimsAsync(IUser user, IEnumerable claims, CancellationToken cancellationToken) + public Task RemoveClaimsAsync(IdentityUser user, IEnumerable claims, CancellationToken cancellationToken) { ((MongoUser)user).RemoveClaims(claims); return TaskHelper.Done; } - public Task SetPhoneNumberAsync(IUser user, string phoneNumber, CancellationToken cancellationToken) + public Task SetPhoneNumberAsync(IdentityUser user, string phoneNumber, CancellationToken cancellationToken) { ((MongoUser)user).PhoneNumber = phoneNumber; return TaskHelper.Done; } - public Task SetPhoneNumberConfirmedAsync(IUser user, bool confirmed, CancellationToken cancellationToken) + public Task SetPhoneNumberConfirmedAsync(IdentityUser user, bool confirmed, CancellationToken cancellationToken) { ((MongoUser)user).PhoneNumberConfirmed = confirmed; return TaskHelper.Done; } - public Task SetTwoFactorEnabledAsync(IUser user, bool enabled, CancellationToken cancellationToken) + public Task SetTwoFactorEnabledAsync(IdentityUser user, bool enabled, CancellationToken cancellationToken) { ((MongoUser)user).TwoFactorEnabled = enabled; return TaskHelper.Done; } - public Task SetLockoutEndDateAsync(IUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken) + public Task SetLockoutEndDateAsync(IdentityUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken) { - ((MongoUser)user).LockoutEndDateUtc = lockoutEnd?.UtcDateTime; + ((MongoUser)user).LockoutEnd = lockoutEnd?.UtcDateTime; return TaskHelper.Done; } - public Task IncrementAccessFailedCountAsync(IUser user, CancellationToken cancellationToken) + public Task IncrementAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken) { ((MongoUser)user).AccessFailedCount++; return Task.FromResult(((MongoUser)user).AccessFailedCount); } - public Task ResetAccessFailedCountAsync(IUser user, CancellationToken cancellationToken) + public Task ResetAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken) { ((MongoUser)user).AccessFailedCount = 0; return TaskHelper.Done; } - public Task SetLockoutEnabledAsync(IUser user, bool enabled, CancellationToken cancellationToken) + public Task SetLockoutEnabledAsync(IdentityUser user, bool enabled, CancellationToken cancellationToken) { ((MongoUser)user).LockoutEnabled = enabled; return TaskHelper.Done; } - public Task SetTokenAsync(IUser user, string loginProvider, string name, string value, CancellationToken cancellationToken) + public Task SetTokenAsync(IdentityUser user, string loginProvider, string name, string value, CancellationToken cancellationToken) { ((MongoUser)user).SetToken(loginProvider, name, value); return TaskHelper.Done; } - public Task RemoveTokenAsync(IUser user, string loginProvider, string name, CancellationToken cancellationToken) + public Task RemoveTokenAsync(IdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) { ((MongoUser)user).RemoveToken(loginProvider, name); return TaskHelper.Done; } - public async Task FindByIdOrEmailAsync(string id) + public Task SetAuthenticatorKeyAsync(IdentityUser user, string key, CancellationToken cancellationToken) { - if (ObjectId.TryParse(id, out _)) - { - return await Collection.Find(x => x.Id == id).FirstOrDefaultAsync(); - } - else - { - return await Collection.Find(x => x.NormalizedEmail == id.ToUpperInvariant()).FirstOrDefaultAsync(); - } + ((MongoUser)user).SetToken(InternalLoginProvider, AuthenticatorKeyTokenName, key); + + return TaskHelper.Done; + } + + public Task ReplaceCodesAsync(IdentityUser user, IEnumerable recoveryCodes, CancellationToken cancellationToken) + { + ((MongoUser)user).SetToken(InternalLoginProvider, RecoveryCodeTokenName, string.Join(";", recoveryCodes)); + + return TaskHelper.Done; } - public Task> QueryByEmailAsync(string email) + public Task RedeemCodeAsync(IdentityUser user, string code, CancellationToken cancellationToken) { - var result = Users; + var mergedCodes = ((MongoUser)user).GetToken(InternalLoginProvider, RecoveryCodeTokenName) ?? string.Empty; - if (!string.IsNullOrWhiteSpace(email)) + var splitCodes = mergedCodes.Split(';'); + if (splitCodes.Contains(code)) { - var normalizedEmail = email.ToUpperInvariant(); + var updatedCodes = new List(splitCodes.Where(s => s != code)); + + ((MongoUser)user).SetToken(InternalLoginProvider, RecoveryCodeTokenName, string.Join(";", updatedCodes)); - result = result.Where(x => x.NormalizedEmail.Contains(normalizedEmail)); + return TaskHelper.True; } - return Task.FromResult(result.Select(x => x).ToList()); + return TaskHelper.False; } } -} +} \ No newline at end of file diff --git a/src/Squidex.Domain.Users.MongoDb/MongoUserToken.cs b/src/Squidex.Domain.Users.MongoDb/MongoUserToken.cs deleted file mode 100644 index 15a0b4ebd..000000000 --- a/src/Squidex.Domain.Users.MongoDb/MongoUserToken.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using MongoDB.Bson.Serialization.Attributes; - -namespace Squidex.Domain.Users.MongoDb -{ - public sealed class MongoUserToken - { - [BsonRequired] - [BsonElement] - public string LoginProvider { get; set; } - - [BsonIgnoreIfDefault] - [BsonElement] - public string Name { get; set; } - - [BsonRequired] - [BsonElement] - public string Value { get; set; } - } -} diff --git a/src/Squidex.Domain.Users/DefaultUserResolver.cs b/src/Squidex.Domain.Users/DefaultUserResolver.cs new file mode 100644 index 000000000..0b5d2cbe0 --- /dev/null +++ b/src/Squidex.Domain.Users/DefaultUserResolver.cs @@ -0,0 +1,50 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Squidex.Infrastructure; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Users +{ + public sealed class DefaultUserResolver : IUserResolver + { + private readonly UserManager userManager; + private readonly IUserFactory userFactory; + + public DefaultUserResolver(UserManager userManager, IUserFactory userFactory) + { + Guard.NotNull(userManager, nameof(userManager)); + Guard.NotNull(userFactory, nameof(userFactory)); + + this.userManager = userManager; + this.userFactory = userFactory; + } + + public async Task FindByIdOrEmailAsync(string idOrEmail) + { + if (userFactory.IsId(idOrEmail)) + { + return await userManager.FindByIdWithClaimsAsync(idOrEmail); + } + else + { + return await userManager.FindByEmailWithClaimsAsyncAsync(idOrEmail); + } + } + + public async Task> QueryByEmailAsync(string email) + { + var result = await userManager.QueryByEmailAsync(email); + + return result.OfType().ToList(); + } + } +} diff --git a/src/Squidex.Domain.Users/IRole.cs b/src/Squidex.Domain.Users/IRole.cs deleted file mode 100644 index a4663ed93..000000000 --- a/src/Squidex.Domain.Users/IRole.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Users -{ - public interface IRole - { - string Name { get; } - } -} diff --git a/src/Squidex.Domain.Users/IRoleFactory.cs b/src/Squidex.Domain.Users/IRoleFactory.cs deleted file mode 100644 index 499c13b35..000000000 --- a/src/Squidex.Domain.Users/IRoleFactory.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Users -{ - public interface IRoleFactory - { - IRole Create(string name); - } -} diff --git a/src/Squidex.Domain.Users/IUserFactory.cs b/src/Squidex.Domain.Users/IUserFactory.cs index 36a5a3305..cb9ad46d8 100644 --- a/src/Squidex.Domain.Users/IUserFactory.cs +++ b/src/Squidex.Domain.Users/IUserFactory.cs @@ -5,12 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Squidex.Shared.Users; +using Microsoft.AspNetCore.Identity; namespace Squidex.Domain.Users { public interface IUserFactory { - IUser Create(string email); + IdentityUser Create(string email); + + bool IsId(string id); } } diff --git a/src/Squidex.Domain.Users/PwnedPasswordValidator.cs b/src/Squidex.Domain.Users/PwnedPasswordValidator.cs index 44087694c..08a2f8320 100644 --- a/src/Squidex.Domain.Users/PwnedPasswordValidator.cs +++ b/src/Squidex.Domain.Users/PwnedPasswordValidator.cs @@ -11,11 +11,10 @@ using Microsoft.AspNetCore.Identity; using SharpPwned.NET; using Squidex.Infrastructure; using Squidex.Infrastructure.Log; -using Squidex.Shared.Users; namespace Squidex.Domain.Users { - public sealed class PwnedPasswordValidator : IPasswordValidator + public sealed class PwnedPasswordValidator : IPasswordValidator { private const string ErrorCode = "PwnedError"; private const string ErrorText = "This password has previously appeared in a data breach and should never be used. If you've ever used it anywhere before, change it!"; @@ -31,7 +30,7 @@ namespace Squidex.Domain.Users this.log = log; } - public async Task ValidateAsync(UserManager manager, IUser user, string password) + public async Task ValidateAsync(UserManager manager, IdentityUser user, string password) { try { diff --git a/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj b/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj index 410d36a0c..84aa8e93f 100644 --- a/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj +++ b/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs b/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs index e8f32d741..f0efcdfd9 100644 --- a/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs +++ b/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs @@ -11,18 +11,17 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using Squidex.Infrastructure.Security; -using Squidex.Shared.Users; namespace Squidex.Domain.Users { - public sealed class UserClaimsPrincipalFactoryWithEmail : UserClaimsPrincipalFactory + public sealed class UserClaimsPrincipalFactoryWithEmail : UserClaimsPrincipalFactory { - public UserClaimsPrincipalFactoryWithEmail(UserManager userManager, RoleManager roleManager, IOptions optionsAccessor) + public UserClaimsPrincipalFactoryWithEmail(UserManager userManager, RoleManager roleManager, IOptions optionsAccessor) : base(userManager, roleManager, optionsAccessor) { } - public override async Task CreateAsync(IUser user) + public override async Task CreateAsync(IdentityUser user) { var principal = await base.CreateAsync(user); diff --git a/src/Squidex.Domain.Users/UserManagerExtensions.cs b/src/Squidex.Domain.Users/UserManagerExtensions.cs index 5ed5b7f0d..0314c9d00 100644 --- a/src/Squidex.Domain.Users/UserManagerExtensions.cs +++ b/src/Squidex.Domain.Users/UserManagerExtensions.cs @@ -8,31 +8,88 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Squidex.Infrastructure; -using Squidex.Infrastructure.Security; -using Squidex.Shared.Users; namespace Squidex.Domain.Users { public static class UserManagerExtensions { - public static Task> QueryByEmailAsync(this UserManager userManager, string email = null, int take = 10, int skip = 0) + public static async Task GetUserWithClaimsAsync(this UserManager userManager, ClaimsPrincipal principal) { - var users = QueryUsers(userManager, email).Skip(skip).Take(take).ToList(); + if (principal == null) + { + return null; + } + + var user = await userManager.FindByIdWithClaimsAsync(userManager.GetUserId(principal)); + + return user; + } + + public static async Task ResolveUserAsync(this UserManager userManager, IdentityUser user) + { + if (user == null) + { + return null; + } + + var claims = await userManager.GetClaimsAsync(user); + + return new UserWithClaims(user, claims); + } + + public static async Task FindByIdWithClaimsAsync(this UserManager userManager, string id) + { + if (id == null) + { + return null; + } + + var user = await userManager.FindByIdAsync(id); + + return await userManager.ResolveUserAsync(user); + } + + public static async Task FindByEmailWithClaimsAsyncAsync(this UserManager userManager, string email) + { + if (email == null) + { + return null; + } - return Task.FromResult>(users); + var user = await userManager.FindByEmailAsync(email); + + return await userManager.ResolveUserAsync(user); } - public static Task CountByEmailAsync(this UserManager userManager, string email = null) + public static Task CountByEmailAsync(this UserManager userManager, string email = null) { var count = QueryUsers(userManager, email).LongCount(); return Task.FromResult(count); } - private static IQueryable QueryUsers(UserManager userManager, string email = null) + public static async Task> QueryByEmailAsync(this UserManager userManager, string email = null, int take = 10, int skip = 0) + { + var users = QueryUsers(userManager, email).Skip(skip).Take(take).ToList(); + + var result = await userManager.ResolveUsersAsync(users); + + return result.ToList(); + } + + public static Task ResolveUsersAsync(this UserManager userManager, IEnumerable users) + { + return Task.WhenAll(users.Select(async user => + { + return await userManager.ResolveUserAsync(user); + })); + } + + public static IQueryable QueryUsers(UserManager userManager, string email = null) { var result = userManager.Users; @@ -46,25 +103,24 @@ namespace Squidex.Domain.Users return result; } - public static async Task CreateAsync(this UserManager userManager, IUserFactory factory, string email, string displayName, string password, PermissionSet permissions = null) + public static async Task CreateAsync(this UserManager userManager, IUserFactory factory, UserValues values) { - var user = factory.Create(email); + var user = factory.Create(values.Email); try { - user.SetDisplayName(displayName); - user.SetPictureUrlFromGravatar(email); + await DoChecked(() => userManager.CreateAsync(user), "Cannot create user."); - if (permissions != null) + var claims = values.ToClaims().ToList(); + + if (claims.Count > 0) { - user.SetPermissions(permissions); + await DoChecked(() => userManager.AddClaimsAsync(user, claims), "Cannot add user."); } - await DoChecked(() => userManager.CreateAsync(user), "Cannot create user."); - - if (!string.IsNullOrWhiteSpace(password)) + if (!string.IsNullOrWhiteSpace(values.Password)) { - await DoChecked(() => userManager.AddPasswordAsync(user, password), "Cannot create user."); + await DoChecked(() => userManager.AddPasswordAsync(user, values.Password), "Cannot create user."); } } catch @@ -77,68 +133,73 @@ namespace Squidex.Domain.Users return user; } - public static Task UpdateAsync(this UserManager userManager, IUser user, string email, string displayName, bool hidden) - { - user.SetHidden(hidden); - user.SetEmail(email); - user.SetDisplayName(displayName); - - return userManager.UpdateAsync(user); - } - - public static async Task UpdateAsync(this UserManager userManager, string id, string email, string displayName, string password, PermissionSet permissions = null) + public static async Task UpdateAsync(this UserManager userManager, string id, UserValues values) { var user = await userManager.FindByIdAsync(id); if (user == null) { - throw new DomainObjectNotFoundException(id, typeof(IUser)); + throw new DomainObjectNotFoundException(id, typeof(IdentityUser)); } - if (!string.IsNullOrWhiteSpace(email)) + await UpdateAsync(userManager, user, values); + } + + public static async Task UpdateSafeAsync(this UserManager userManager, IdentityUser user, UserValues values) + { + try + { + await userManager.UpdateAsync(user, values); + + return IdentityResult.Success; + } + catch (ValidationException ex) { - await DoChecked(() => userManager.SetEmailAsync(user, email), "Cannot update email."); - await DoChecked(() => userManager.SetUserNameAsync(user, email), "Cannot update email."); + return IdentityResult.Failed(ex.Errors.Select(x => new IdentityError { Description = x.Message }).ToArray()); } + } - if (!string.IsNullOrWhiteSpace(displayName)) + public static async Task UpdateAsync(this UserManager userManager, IdentityUser user, UserValues values) + { + if (user == null) { - user.SetDisplayName(displayName); + throw new DomainObjectNotFoundException("Id", typeof(IdentityUser)); } - if (permissions != null) + if (!string.IsNullOrWhiteSpace(values.Email) && values.Email != user.Email) { - user.SetPermissions(permissions); + await DoChecked(() => userManager.SetEmailAsync(user, values.Email), "Cannot update email."); + await DoChecked(() => userManager.SetUserNameAsync(user, values.Email), "Cannot update email."); } - await DoChecked(() => userManager.UpdateAsync(user), "Cannot update user."); + await DoChecked(() => userManager.SyncClaimsAsync(user, values.ToClaims().ToList()), "Cannot update user."); - if (!string.IsNullOrWhiteSpace(password)) + if (!string.IsNullOrWhiteSpace(values.Password)) { - await DoChecked(() => userManager.RemovePasswordAsync(user), "Cannot update user."); - await DoChecked(() => userManager.AddPasswordAsync(user, password), "Cannot update user."); + await DoChecked(() => userManager.RemovePasswordAsync(user), "Cannot replace password."); + await DoChecked(() => userManager.AddPasswordAsync(user, values.Password), "Cannot replace password."); } } - public static async Task LockAsync(this UserManager userManager, string id) + public static async Task LockAsync(this UserManager userManager, string id) { var user = await userManager.FindByIdAsync(id); if (user == null) { - throw new DomainObjectNotFoundException(id, typeof(IUser)); + throw new DomainObjectNotFoundException(id, typeof(IdentityUser)); } await DoChecked(() => userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddYears(100)), "Cannot lock user."); } - public static async Task UnlockAsync(this UserManager userManager, string id) + public static async Task UnlockAsync(this UserManager userManager, string id) { var user = await userManager.FindByIdAsync(id); if (user == null) { - throw new DomainObjectNotFoundException(id, typeof(IUser)); + throw new DomainObjectNotFoundException(id, typeof(IdentityUser)); } await DoChecked(() => userManager.SetLockoutEndDateAsync(user, null), "Cannot unlock user."); @@ -153,5 +214,36 @@ namespace Squidex.Domain.Users throw new ValidationException(message, result.Errors.Select(x => new ValidationError(x.Description)).ToArray()); } } + + public static async Task SyncClaimsAsync(this UserManager userManager, IdentityUser user, IEnumerable claims) + { + if (claims.Any()) + { + var oldClaims = await userManager.GetClaimsAsync(user); + var oldClaimsToRemove = new List(); + + foreach (var oldClaim in oldClaims) + { + if (claims.Any(x => x.Type == oldClaim.Type)) + { + oldClaimsToRemove.Add(oldClaim); + } + } + + if (oldClaimsToRemove.Count > 0) + { + var result = await userManager.RemoveClaimsAsync(user, oldClaimsToRemove); + + if (!result.Succeeded) + { + return result; + } + } + + return await userManager.AddClaimsAsync(user, claims); + } + + return IdentityResult.Success; + } } } diff --git a/src/Squidex.Domain.Users/UserValues.cs b/src/Squidex.Domain.Users/UserValues.cs new file mode 100644 index 000000000..e3b588925 --- /dev/null +++ b/src/Squidex.Domain.Users/UserValues.cs @@ -0,0 +1,69 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Security.Claims; +using Squidex.Infrastructure.Security; +using Squidex.Shared.Identity; + +namespace Squidex.Domain.Users +{ + public sealed class UserValues + { + public string DisplayName { get; set; } + + public string PictureUrl { get; set; } + + public string Password { get; set; } + + public string Email { get; set; } + + public bool? Consent { get; set; } + + public bool? ConsentForEmails { get; set; } + + public bool? Hidden { get; set; } + + public PermissionSet Permissions { get; set; } + + public IEnumerable ToClaims() + { + if (!string.IsNullOrWhiteSpace(DisplayName)) + { + yield return new Claim(SquidexClaimTypes.DisplayName, DisplayName); + } + + if (!string.IsNullOrWhiteSpace(PictureUrl)) + { + yield return new Claim(SquidexClaimTypes.PictureUrl, PictureUrl); + } + + if (Hidden.HasValue) + { + yield return new Claim(SquidexClaimTypes.Consent, Hidden.ToString()); + } + + if (Consent.HasValue) + { + yield return new Claim(SquidexClaimTypes.Consent, Consent.ToString()); + } + + if (ConsentForEmails.HasValue) + { + yield return new Claim(SquidexClaimTypes.ConsentForEmails, ConsentForEmails.ToString()); + } + + if (Permissions != null) + { + foreach (var permission in Permissions) + { + yield return new Claim(SquidexClaimTypes.Permissions, permission.Id); + } + } + } + } +} diff --git a/src/Squidex.Domain.Users/UserWithClaims.cs b/src/Squidex.Domain.Users/UserWithClaims.cs new file mode 100644 index 000000000..3231f5a67 --- /dev/null +++ b/src/Squidex.Domain.Users/UserWithClaims.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Microsoft.AspNetCore.Identity; +using Squidex.Infrastructure; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Users +{ + public sealed class UserWithClaims : IUser + { + public IdentityUser Identity { get; } + + public List Claims { get; } + + public string Id + { + get { return Identity.Id; } + } + + public string Email + { + get { return Identity.Email; } + } + + IReadOnlyList IUser.Claims + { + get { return Claims; } + } + + public UserWithClaims(IdentityUser user, IEnumerable claims) + { + Guard.NotNull(user, nameof(user)); + Guard.NotNull(claims, nameof(claims)); + + Identity = user; + + Claims = claims.ToList(); + } + } +} diff --git a/src/Squidex.Shared/Identity/SquidexClaimTypes.cs b/src/Squidex.Shared/Identity/SquidexClaimTypes.cs index b0dc53541..bd32adb0d 100644 --- a/src/Squidex.Shared/Identity/SquidexClaimTypes.cs +++ b/src/Squidex.Shared/Identity/SquidexClaimTypes.cs @@ -22,5 +22,7 @@ namespace Squidex.Shared.Identity public static readonly string Permissions = "urn:squidex:permissions"; public static readonly string Prefix = "urn:squidex:"; + + public static readonly string PictureUrlStore = "store"; } } diff --git a/src/Squidex.Shared/Users/ExternalLogin.cs b/src/Squidex.Shared/Users/ExternalLogin.cs deleted file mode 100644 index 4e25c2194..000000000 --- a/src/Squidex.Shared/Users/ExternalLogin.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Shared.Users -{ - public sealed class ExternalLogin - { - public string LoginProvider { get; } - - public string ProviderKey { get; } - - public string ProviderDisplayName { get; } - - public ExternalLogin(string loginProvider, string providerKey, string providerDisplayName) - { - LoginProvider = loginProvider; - - ProviderKey = providerKey; - ProviderDisplayName = providerDisplayName; - - if (string.IsNullOrWhiteSpace(ProviderDisplayName)) - { - ProviderDisplayName = loginProvider; - } - } - } -} diff --git a/src/Squidex.Shared/Users/IUser.cs b/src/Squidex.Shared/Users/IUser.cs index 1e9eaae8c..b863ff171 100644 --- a/src/Squidex.Shared/Users/IUser.cs +++ b/src/Squidex.Shared/Users/IUser.cs @@ -12,24 +12,10 @@ namespace Squidex.Shared.Users { public interface IUser { - bool IsLocked { get; } - string Id { get; } string Email { get; } - string NormalizedEmail { get; } - IReadOnlyList Claims { get; } - - IReadOnlyList Logins { get; } - - void RemoveClaims(string type); - - void SetEmail(string email); - - void SetClaim(string type, string value); - - void AddClaim(Claim claim); } } diff --git a/src/Squidex.Shared/Users/UserExtensions.cs b/src/Squidex.Shared/Users/UserExtensions.cs index c0479e457..52584f671 100644 --- a/src/Squidex.Shared/Users/UserExtensions.cs +++ b/src/Squidex.Shared/Users/UserExtensions.cs @@ -7,8 +7,6 @@ using System; using System.Linq; -using System.Security.Claims; -using Squidex.Infrastructure; using Squidex.Infrastructure.Security; using Squidex.Shared.Identity; @@ -16,49 +14,9 @@ namespace Squidex.Shared.Users { public static class UserExtensions { - public static void SetDisplayName(this IUser user, string displayName) - { - user.SetClaim(SquidexClaimTypes.DisplayName, displayName); - } - - public static void SetPictureUrl(this IUser user, string pictureUrl) - { - user.SetClaim(SquidexClaimTypes.PictureUrl, pictureUrl); - } - - public static void SetPictureUrlToStore(this IUser user) - { - user.SetClaim(SquidexClaimTypes.PictureUrl, "store"); - } - - public static void SetPictureUrlFromGravatar(this IUser user, string email) - { - user.SetClaim(SquidexClaimTypes.PictureUrl, GravatarHelper.CreatePictureUrl(email)); - } - - public static void SetHidden(this IUser user, bool value) - { - user.SetClaim(SquidexClaimTypes.Hidden, value.ToString()); - } - - public static void SetConsent(this IUser user) - { - user.SetClaim(SquidexClaimTypes.Consent, "true"); - } - - public static void SetConsentForEmails(this IUser user, bool value) - { - user.SetClaim(SquidexClaimTypes.ConsentForEmails, value.ToString()); - } - - public static void SetPermissions(this IUser user, PermissionSet permissions) + public static PermissionSet Permissions(this IUser user) { - user.RemoveClaims(SquidexClaimTypes.Permissions); - - foreach (var permission in permissions) - { - user.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission.Id)); - } + return new PermissionSet(user.GetClaimValues(SquidexClaimTypes.Permissions).Select(x => new Permission(x))); } public static bool IsHidden(this IUser user) @@ -88,7 +46,7 @@ namespace Squidex.Shared.Users public static bool IsPictureUrlStored(this IUser user) { - return user.HasClaimValue(SquidexClaimTypes.PictureUrl, "store"); + return user.HasClaimValue(SquidexClaimTypes.PictureUrl, SquidexClaimTypes.PictureUrlStore); } public static string PictureUrl(this IUser user) @@ -101,11 +59,6 @@ namespace Squidex.Shared.Users return user.GetClaimValue(SquidexClaimTypes.DisplayName); } - public static PermissionSet Permissions(this IUser user) - { - return new PermissionSet(user.GetClaimValues(SquidexClaimTypes.Permissions).Select(x => new Permission(x))); - } - public static string GetClaimValue(this IUser user, string type) { return user.Claims.FirstOrDefault(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase))?.Value; diff --git a/src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs b/src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs index 81c479ac3..b59f83b53 100644 --- a/src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs @@ -6,6 +6,8 @@ // ========================================================================== using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Users; +using Squidex.Infrastructure.Security; namespace Squidex.Areas.Api.Controllers.Users.Models { @@ -35,5 +37,10 @@ namespace Squidex.Areas.Api.Controllers.Users.Models /// [Required] public string[] Permissions { get; set; } + + public UserValues ToValues() + { + return new UserValues { Email = Email, DisplayName = DisplayName, Password = Password, Permissions = new PermissionSet(Permissions) }; + } } } diff --git a/src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs b/src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs index 4e6ee87a3..77b41f567 100644 --- a/src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs @@ -6,6 +6,8 @@ // ========================================================================== using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Users; +using Squidex.Infrastructure.Security; namespace Squidex.Areas.Api.Controllers.Users.Models { @@ -34,5 +36,10 @@ namespace Squidex.Areas.Api.Controllers.Users.Models /// [Required] public string[] Permissions { get; set; } + + public UserValues ToValues() + { + return new UserValues { Email = Email, DisplayName = DisplayName, Password = Password, Permissions = new PermissionSet(Permissions) }; + } } } diff --git a/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs b/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs index be6acdd89..ef7e8cb65 100644 --- a/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs @@ -17,17 +17,16 @@ using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Security; using Squidex.Pipeline; using Squidex.Shared; -using Squidex.Shared.Users; namespace Squidex.Areas.Api.Controllers.Users { [ApiModelValidation(true)] public sealed class UserManagementController : ApiController { - private readonly UserManager userManager; + private readonly UserManager userManager; private readonly IUserFactory userFactory; - public UserManagementController(ICommandBus commandBus, UserManager userManager, IUserFactory userFactory) + public UserManagementController(ICommandBus commandBus, UserManager userManager, IUserFactory userFactory) : base(commandBus) { this.userManager = userManager; @@ -58,7 +57,7 @@ namespace Squidex.Areas.Api.Controllers.Users [ApiPermission(Permissions.AdminUsersRead)] public async Task GetUser(string id) { - var entity = await userManager.FindByIdAsync(id); + var entity = await userManager.FindByIdWithClaimsAsync(id); if (entity == null) { @@ -75,7 +74,7 @@ namespace Squidex.Areas.Api.Controllers.Users [ApiPermission(Permissions.AdminUsersCreate)] public async Task PostUser([FromBody] CreateUserDto request) { - var user = await userManager.CreateAsync(userFactory, request.Email, request.DisplayName, request.Password, new PermissionSet(request.Permissions)); + var user = await userManager.CreateAsync(userFactory, request.ToValues()); var response = new UserCreatedDto { Id = user.Id }; @@ -87,7 +86,7 @@ namespace Squidex.Areas.Api.Controllers.Users [ApiPermission(Permissions.AdminUsersUpdate)] public async Task PutUser(string id, [FromBody] UpdateUserDto request) { - await userManager.UpdateAsync(id, request.Email, request.DisplayName, request.Password, new PermissionSet(request.Permissions)); + await userManager.UpdateAsync(id, request.ToValues()); return NoContent(); } diff --git a/src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs b/src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs index c6b8f2668..f7bc37b51 100644 --- a/src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs +++ b/src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs @@ -17,7 +17,6 @@ using Squidex.Domain.Users; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Security; using Squidex.Shared; -using Squidex.Shared.Users; namespace Squidex.Areas.IdentityServer.Config { @@ -34,7 +33,7 @@ namespace Squidex.Areas.IdentityServer.Config { var options = services.GetService>().Value; - var userManager = services.GetService>(); + var userManager = services.GetService>(); var userFactory = services.GetService(); var log = services.GetService(); @@ -50,9 +49,15 @@ namespace Squidex.Areas.IdentityServer.Config { try { - var permissions = new PermissionSet(Permissions.Admin); + var values = new UserValues + { + Email = adminEmail, + Password = adminPass, + Permissions = new PermissionSet(Permissions.Admin), + DisplayName = adminEmail + }; - await userManager.CreateAsync(userFactory, adminEmail, adminEmail, adminPass, permissions); + await userManager.CreateAsync(userFactory, values); } catch (Exception ex) { diff --git a/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs b/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs index 32daaf5cf..f5025efd6 100644 --- a/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs +++ b/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs @@ -20,7 +20,6 @@ using Microsoft.Extensions.Options; using Squidex.Config; using Squidex.Domain.Users; using Squidex.Shared.Identity; -using Squidex.Shared.Users; namespace Squidex.Areas.IdentityServer.Config { @@ -55,11 +54,11 @@ namespace Squidex.Areas.IdentityServer.Config services.AddSingleton(GetApiResources()); services.AddSingleton(GetIdentityResources()); - services.AddIdentity() + services.AddIdentity() .AddDefaultTokenProviders(); - services.AddSingleton, + services.AddSingleton, PwnedPasswordValidator>(); - services.AddSingleton, + services.AddSingleton, UserClaimsPrincipalFactoryWithEmail>(); services.AddSingleton(); @@ -70,7 +69,7 @@ namespace Squidex.Areas.IdentityServer.Config { options.UserInteraction.ErrorUrl = "/error/"; }) - .AddAspNetIdentity() + .AddAspNetIdentity() .AddInMemoryApiResources(GetApiResources()) .AddInMemoryIdentityResources(GetIdentityResources()) .AddSigningCredential(certificate); diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs b/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs index ea658852f..890ebefab 100644 --- a/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using System.Security; @@ -30,20 +31,22 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account { public sealed class AccountController : IdentityServerController { - private readonly SignInManager signInManager; - private readonly UserManager userManager; + private readonly SignInManager signInManager; + private readonly UserManager userManager; private readonly IUserFactory userFactory; private readonly IUserEvents userEvents; + private readonly IUserResolver userResolver; private readonly IOptions identityOptions; private readonly IOptions urlOptions; private readonly ISemanticLog log; private readonly IIdentityServerInteractionService interactions; public AccountController( - SignInManager signInManager, - UserManager userManager, + SignInManager signInManager, + UserManager userManager, IUserFactory userFactory, IUserEvents userEvents, + IUserResolver userResolver, IOptions identityOptions, IOptions urlOptions, ISemanticLog log, @@ -52,6 +55,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account this.log = log; this.urlOptions = urlOptions; this.userEvents = userEvents; + this.userResolver = userResolver; this.userManager = userManager; this.userFactory = userFactory; this.interactions = interactions; @@ -115,12 +119,15 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account return View(vm); } - var user = await userManager.GetUserAsync(User); + var user = await userManager.GetUserWithClaimsAsync(User); - user.SetConsentForEmails(model.ConsentToAutomatedEmails); - user.SetConsent(); + var update = new UserValues + { + Consent = true, + ConsentForEmails = model.ConsentToAutomatedEmails + }; - await userManager.UpdateAsync(user); + await userManager.UpdateAsync(user.Id, update); userEvents.OnConsentGiven(user); @@ -232,36 +239,37 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account var isLoggedIn = result.Succeeded; - IUser user = null; + UserWithClaims user = null; if (!isLoggedIn) { var email = externalLogin.Principal.FindFirst(ClaimTypes.Email).Value; - user = await userManager.FindByEmailAsync(email); + user = await userManager.FindByEmailWithClaimsAsyncAsync(email); if (user != null) { isLoggedIn = await AddLoginAsync(user, externalLogin) && + await AddClaimsAsync(user, externalLogin, email) && await LoginAsync(externalLogin); } else { - user = CreateUser(externalLogin, email); + user = new UserWithClaims(userFactory.Create(email), new List()); var isFirst = userManager.Users.LongCount() == 0; isLoggedIn = await AddUserAsync(user) && await AddLoginAsync(user, externalLogin) && - await MakeAdminAsync(user, isFirst) && + await AddClaimsAsync(user, externalLogin, email, isFirst) && await LockAsync(user, isFirst) && await LoginAsync(externalLogin); userEvents.OnUserRegistered(user); - if (user.IsLocked) + if (await userManager.IsLockedOutAsync(user.Identity)) { return View(nameof(LockedOut)); } @@ -282,14 +290,14 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account } } - private Task AddLoginAsync(IUser user, UserLoginInfo externalLogin) + private Task AddLoginAsync(UserWithClaims user, UserLoginInfo externalLogin) { - return MakeIdentityOperation(() => userManager.AddLoginAsync(user, externalLogin)); + return MakeIdentityOperation(() => userManager.AddLoginAsync(user.Identity, externalLogin)); } - private Task AddUserAsync(IUser user) + private Task AddUserAsync(UserWithClaims user) { - return MakeIdentityOperation(() => userManager.CreateAsync(user)); + return MakeIdentityOperation(() => userManager.CreateAsync(user.Identity)); } private async Task LoginAsync(UserLoginInfo externalLogin) @@ -299,46 +307,48 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account return result.Succeeded; } - private Task LockAsync(IUser user, bool isFirst) + private Task LockAsync(UserWithClaims user, bool isFirst) { if (isFirst || !identityOptions.Value.LockAutomatically) { return TaskHelper.True; } - return MakeIdentityOperation(() => userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddYears(100))); + return MakeIdentityOperation(() => userManager.SetLockoutEndDateAsync(user.Identity, DateTimeOffset.UtcNow.AddYears(100))); } - private Task MakeAdminAsync(IUser user, bool isFirst) + private Task AddClaimsAsync(UserWithClaims user, ExternalLoginInfo externalLogin, string email, bool isFirst = false) { - if (!isFirst) - { - return TaskHelper.True; - } + var newClaims = new List(); - return MakeIdentityOperation(() => userManager.AddClaimAsync(user, new Claim(SquidexClaimTypes.Permissions, Permissions.Admin))); - } + void AddClaim(Claim claim) + { + newClaims.Add(claim); - private IUser CreateUser(ExternalLoginInfo externalLogin, string email) - { - var user = userFactory.Create(email); + user.Claims.Add(claim); + } foreach (var squidexClaim in externalLogin.Principal.GetSquidexClaims()) { - user.AddClaim(squidexClaim); + AddClaim(squidexClaim); } if (!user.HasPictureUrl()) { - user.SetPictureUrl(GravatarHelper.CreatePictureUrl(email)); + AddClaim(new Claim(SquidexClaimTypes.PictureUrl, GravatarHelper.CreatePictureUrl(email))); } if (!user.HasDisplayName()) { - user.SetDisplayName(email); + AddClaim(new Claim(SquidexClaimTypes.DisplayName, email)); + } + + if (isFirst) + { + AddClaim(new Claim(SquidexClaimTypes.Permissions, Permissions.Admin)); } - return user; + return MakeIdentityOperation(() => userManager.SyncClaimsAsync(user.Identity, newClaims)); } private IActionResult RedirectToLogoutUrl(LogoutRequest context) diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs b/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs index 29d80c305..de5b21262 100644 --- a/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs @@ -11,13 +11,12 @@ using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Identity; -using Squidex.Shared.Users; namespace Squidex.Areas.IdentityServer.Controllers { public static class Extensions { - public static async Task GetExternalLoginInfoWithDisplayNameAsync(this SignInManager signInManager, string expectedXsrf = null) + public static async Task GetExternalLoginInfoWithDisplayNameAsync(this SignInManager signInManager, string expectedXsrf = null) { var externalLogin = await signInManager.GetExternalLoginInfoAsync(expectedXsrf); @@ -26,7 +25,7 @@ namespace Squidex.Areas.IdentityServer.Controllers return externalLogin; } - public static async Task> GetExternalProvidersAsync(this SignInManager signInManager) + public static async Task> GetExternalProvidersAsync(this SignInManager signInManager) { var externalSchemes = await signInManager.GetExternalAuthenticationSchemesAsync(); var externalProviders = diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs index e1eb3c23e..c97af286b 100644 --- a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Users; namespace Squidex.Areas.IdentityServer.Controllers.Profile { @@ -19,5 +20,10 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile public string DisplayName { get; set; } public bool IsHidden { get; set; } + + public UserValues ToValues() + { + return new UserValues { Email = Email, DisplayName = DisplayName, Hidden = IsHidden }; + } } } diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs index ed77e990a..c727806c6 100644 --- a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs @@ -20,6 +20,7 @@ using Squidex.Config; using Squidex.Domain.Users; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Reflection; +using Squidex.Shared.Identity; using Squidex.Shared.Users; namespace Squidex.Areas.IdentityServer.Controllers.Profile @@ -27,15 +28,15 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile [Authorize] public sealed class ProfileController : IdentityServerController { - private readonly SignInManager signInManager; - private readonly UserManager userManager; + private readonly SignInManager signInManager; + private readonly UserManager userManager; private readonly IUserPictureStore userPictureStore; private readonly IAssetThumbnailGenerator assetThumbnailGenerator; private readonly IOptions identityOptions; public ProfileController( - SignInManager signInManager, - UserManager userManager, + SignInManager signInManager, + UserManager userManager, IUserPictureStore userPictureStore, IAssetThumbnailGenerator assetThumbnailGenerator, IOptions identityOptions) @@ -51,7 +52,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile [Route("/account/profile/")] public async Task Profile(string successMessage = null) { - var user = await userManager.GetUserAsync(User); + var user = await userManager.GetUserWithClaimsAsync(User); return View(await GetProfileVM(user, successMessage: successMessage)); } @@ -81,7 +82,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile [Route("/account/profile/update/")] public Task UpdateProfile(ChangeProfileModel model) { - return MakeChangeAsync(user => userManager.UpdateAsync(user, model.Email, model.DisplayName, model.IsHidden), + return MakeChangeAsync(user => userManager.UpdateSafeAsync(user.Identity, model.ToValues()), "Account updated successfully."); } @@ -89,7 +90,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile [Route("/account/profile/login-remove/")] public Task RemoveLogin(RemoveLoginModel model) { - return MakeChangeAsync(user => userManager.RemoveLoginAsync(user, model.LoginProvider, model.ProviderKey), + return MakeChangeAsync(user => userManager.RemoveLoginAsync(user.Identity, model.LoginProvider, model.ProviderKey), "Login provider removed successfully."); } @@ -97,7 +98,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile [Route("/account/profile/password-set/")] public Task SetPassword(SetPasswordModel model) { - return MakeChangeAsync(user => userManager.AddPasswordAsync(user, model.Password), + return MakeChangeAsync(user => userManager.AddPasswordAsync(user.Identity, model.Password), "Password set successfully."); } @@ -105,7 +106,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile [Route("/account/profile/password-change/")] public Task ChangePassword(ChangePasswordModel model) { - return MakeChangeAsync(user => userManager.ChangePasswordAsync(user, model.OldPassword, model.Password), + return MakeChangeAsync(user => userManager.ChangePasswordAsync(user.Identity, model.OldPassword, model.Password), "Password changed successfully."); } @@ -117,14 +118,14 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile "Picture uploaded successfully."); } - private async Task AddLoginAsync(IUser user) + private async Task AddLoginAsync(UserWithClaims user) { var externalLogin = await signInManager.GetExternalLoginInfoWithDisplayNameAsync(userManager.GetUserId(User)); - return await userManager.AddLoginAsync(user, externalLogin); + return await userManager.AddLoginAsync(user.Identity, externalLogin); } - private async Task UpdatePictureAsync(List file, IUser user) + private async Task UpdatePictureAsync(List file, UserWithClaims user) { if (file.Count != 1) { @@ -145,14 +146,12 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile await userPictureStore.UploadAsync(user.Id, thumbnailStream); - user.SetPictureUrlToStore(); - - return await userManager.UpdateAsync(user); + return await userManager.UpdateSafeAsync(user.Identity, new UserValues { PictureUrl = SquidexClaimTypes.PictureUrlStore }); } - private async Task MakeChangeAsync(Func> action, string successMessage, ChangeProfileModel model = null) + private async Task MakeChangeAsync(Func> action, string successMessage, ChangeProfileModel model = null) { - var user = await userManager.GetUserAsync(User); + var user = await userManager.GetUserWithClaimsAsync(User); if (!ModelState.IsValid) { @@ -166,7 +165,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile if (result.Succeeded) { - await signInManager.SignInAsync(user, true); + await signInManager.SignInAsync(user.Identity, true); return RedirectToAction(nameof(Profile), new { successMessage }); } @@ -181,20 +180,24 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile return View(nameof(Profile), await GetProfileVM(user, model, errorMessage)); } - private async Task GetProfileVM(IUser user, ChangeProfileModel model = null, string errorMessage = null, string successMessage = null) + private async Task GetProfileVM(UserWithClaims user, ChangeProfileModel model = null, string errorMessage = null, string successMessage = null) { - var externalProviders = await signInManager.GetExternalProvidersAsync(); + var taskForProviders = signInManager.GetExternalProvidersAsync(); + var taskForPassword = userManager.HasPasswordAsync(user.Identity); + var taskForLogins = userManager.GetLoginsAsync(user.Identity); + + await Task.WhenAll(taskForProviders, taskForPassword, taskForLogins); var result = new ProfileVM { Id = user.Id, Email = user.Email, ErrorMessage = errorMessage, - ExternalLogins = user.Logins, - ExternalProviders = externalProviders, + ExternalLogins = taskForLogins.Result, + ExternalProviders = taskForProviders.Result, DisplayName = user.DisplayName(), IsHidden = user.IsHidden(), - HasPassword = await userManager.HasPasswordAsync(user), + HasPassword = taskForPassword.Result, HasPasswordAuth = identityOptions.Value.AllowPasswordAuth, SuccessMessage = successMessage }; diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs index 0ac09080c..e65519f73 100644 --- a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs @@ -6,7 +6,7 @@ // ========================================================================== using System.Collections.Generic; -using Squidex.Shared.Users; +using Microsoft.AspNetCore.Identity; namespace Squidex.Areas.IdentityServer.Controllers.Profile { @@ -28,8 +28,8 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile public bool HasPasswordAuth { get; set; } - public IReadOnlyList ExternalLogins { get; set; } + public IList ExternalLogins { get; set; } - public IReadOnlyList ExternalProviders { get; set; } + public IList ExternalProviders { get; set; } } } diff --git a/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml b/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml index 955648686..82e9e0613 100644 --- a/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml +++ b/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml @@ -28,7 +28,7 @@
- I understand and agree that Squidex sends e-mails to inform me about new features, breaking changes and down times. + I understand and agree that Squidex sends Emails to inform me about new features, breaking changes and down times.
diff --git a/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml b/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml index 02c1d8f54..7008a7165 100644 --- a/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml +++ b/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml @@ -121,7 +121,7 @@ { var schema = provider.AuthenticationScheme.ToLowerInvariant(); - } diff --git a/src/Squidex/Config/Domain/InfrastructureServices.cs b/src/Squidex/Config/Domain/InfrastructureServices.cs index 61d963821..ba5a5be18 100644 --- a/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -11,10 +11,12 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using NodaTime; +using Squidex.Domain.Users; using Squidex.Infrastructure; using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Diagnostics; using Squidex.Infrastructure.UsageTracking; +using Squidex.Shared.Users; #pragma warning disable RECS0092 // Convert field to readonly @@ -48,6 +50,12 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + services.AddTransient(typeof(Lazy<>), typeof(Lazier<>)); } } diff --git a/src/Squidex/Config/Domain/StoreServices.cs b/src/Squidex/Config/Domain/StoreServices.cs index 0d4a01be2..8dec518d6 100644 --- a/src/Squidex/Config/Domain/StoreServices.cs +++ b/src/Squidex/Config/Domain/StoreServices.cs @@ -85,14 +85,12 @@ namespace Squidex.Config.Domain .As(); services.AddSingletonAs() - .As>() + .As>() .As() - .As() .As(); services.AddSingletonAs() - .As>() - .As() + .As>() .As(); services.AddSingletonAs() diff --git a/src/Squidex/Config/Domain/SubscriptionServices.cs b/src/Squidex/Config/Domain/SubscriptionServices.cs index c38eab838..a9205dbeb 100644 --- a/src/Squidex/Config/Domain/SubscriptionServices.cs +++ b/src/Squidex/Config/Domain/SubscriptionServices.cs @@ -27,9 +27,6 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddSingletonAs() - .As(); - services.AddSingletonAs() .As(); } diff --git a/src/Squidex/WebStartup.cs b/src/Squidex/WebStartup.cs index 034293428..ff233f5f7 100644 --- a/src/Squidex/WebStartup.cs +++ b/src/Squidex/WebStartup.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Ben.Diagnostics; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -42,8 +41,6 @@ namespace Squidex { app.ApplicationServices.LogConfiguration(); - app.UseBlockingDetection(); - app.UseMyHealthCheck(); app.UseMyRobotsTxt(); app.UseMyTracking(); diff --git a/src/Squidex/app/theme/_static.scss b/src/Squidex/app/theme/_static.scss index b5d375f7f..a89226607 100644 --- a/src/Squidex/app/theme/_static.scss +++ b/src/Squidex/app/theme/_static.scss @@ -109,7 +109,7 @@ noscript { &-icon { display: inline-block; - font-size: 1.25rem; + font-size: 1.5rem; font-weight: normal; vertical-align: middle; width: 1.6rem;