Browse Source

Better Separation of Concerns.

pull/336/head
Sebastian Stehle 8 years ago
parent
commit
86d7b4c8fe
  1. 5
      src/Squidex.Domain.Users.MongoDb/Infrastructure/MongoPersistedGrantStore.cs
  2. 27
      src/Squidex.Domain.Users.MongoDb/MongoRole.cs
  3. 62
      src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs
  4. 150
      src/Squidex.Domain.Users.MongoDb/MongoUser.cs
  5. 33
      src/Squidex.Domain.Users.MongoDb/MongoUserClaim.cs
  6. 42
      src/Squidex.Domain.Users.MongoDb/MongoUserLogin.cs
  7. 269
      src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs
  8. 26
      src/Squidex.Domain.Users.MongoDb/MongoUserToken.cs
  9. 50
      src/Squidex.Domain.Users/DefaultUserResolver.cs
  10. 14
      src/Squidex.Domain.Users/IRole.cs
  11. 14
      src/Squidex.Domain.Users/IRoleFactory.cs
  12. 6
      src/Squidex.Domain.Users/IUserFactory.cs
  13. 5
      src/Squidex.Domain.Users/PwnedPasswordValidator.cs
  14. 1
      src/Squidex.Domain.Users/Squidex.Domain.Users.csproj
  15. 7
      src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs
  16. 178
      src/Squidex.Domain.Users/UserManagerExtensions.cs
  17. 69
      src/Squidex.Domain.Users/UserValues.cs
  18. 48
      src/Squidex.Domain.Users/UserWithClaims.cs
  19. 2
      src/Squidex.Shared/Identity/SquidexClaimTypes.cs
  20. 31
      src/Squidex.Shared/Users/ExternalLogin.cs
  21. 14
      src/Squidex.Shared/Users/IUser.cs
  22. 53
      src/Squidex.Shared/Users/UserExtensions.cs
  23. 7
      src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs
  24. 7
      src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs
  25. 11
      src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs
  26. 13
      src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs
  27. 9
      src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs
  28. 76
      src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs
  29. 5
      src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs
  30. 6
      src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs
  31. 49
      src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs
  32. 6
      src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs
  33. 2
      src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml
  34. 2
      src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml
  35. 8
      src/Squidex/Config/Domain/InfrastructureServices.cs
  36. 6
      src/Squidex/Config/Domain/StoreServices.cs
  37. 3
      src/Squidex/Config/Domain/SubscriptionServices.cs
  38. 3
      src/Squidex/WebStartup.cs
  39. 2
      src/Squidex/app/theme/_static.scss

5
src/Squidex.Domain.Users.MongoDb/Infrastructure/MongoPersistedGrantStore.cs

@ -10,7 +10,9 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using IdentityServer4.Models; using IdentityServer4.Models;
using IdentityServer4.Stores; using IdentityServer4.Stores;
using MongoDB.Bson;
using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
@ -23,7 +25,8 @@ namespace Squidex.Domain.Users.MongoDb.Infrastructure
BsonClassMap.RegisterClassMap<PersistedGrant>(map => BsonClassMap.RegisterClassMap<PersistedGrant>(map =>
{ {
map.AutoMap(); map.AutoMap();
map.MapIdProperty(x => x.Key);
map.MapIdProperty(x => x.Key).SetSerializer(new StringSerializer(BsonType.ObjectId));
}); });
} }

27
src/Squidex.Domain.Users.MongoDb/MongoRole.cs

@ -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; }
}
}

62
src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs

@ -8,14 +8,28 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Users.MongoDb namespace Squidex.Domain.Users.MongoDb
{ {
public sealed class MongoRoleStore : MongoRepositoryBase<MongoRole>, IRoleStore<IRole>, IRoleFactory public sealed class MongoRoleStore : MongoRepositoryBase<IdentityRole>, IRoleStore<IdentityRole>
{ {
static MongoRoleStore()
{
BsonClassMap.RegisterClassMap<IdentityRole<string>>(cm =>
{
cm.AutoMap();
cm.MapMember(x => x.Id).SetSerializer(new StringSerializer(BsonType.ObjectId));
cm.UnmapMember(x => x.ConcurrencyStamp);
});
}
public MongoRoleStore(IMongoDatabase database) public MongoRoleStore(IMongoDatabase database)
: base(database) : base(database)
{ {
@ -26,10 +40,10 @@ namespace Squidex.Domain.Users.MongoDb
return "Identity_Roles"; return "Identity_Roles";
} }
protected override Task SetupCollectionAsync(IMongoCollection<MongoRole> collection, CancellationToken ct = default(CancellationToken)) protected override Task SetupCollectionAsync(IMongoCollection<IdentityRole> collection, CancellationToken ct = default(CancellationToken))
{ {
return collection.Indexes.CreateOneAsync( return collection.Indexes.CreateOneAsync(
new CreateIndexModel<MongoRole>(Index.Ascending(x => x.NormalizedName), new CreateIndexOptions { Unique = true }), cancellationToken: ct); new CreateIndexModel<IdentityRole>(Index.Ascending(x => x.NormalizedName), new CreateIndexOptions { Unique = true }), cancellationToken: ct);
} }
protected override MongoCollectionSettings CollectionSettings() 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<IRole> FindByIdAsync(string roleId, CancellationToken cancellationToken) public async Task<IdentityRole> FindByIdAsync(string roleId, CancellationToken cancellationToken)
{ {
return await Collection.Find(x => x.Id == roleId).FirstOrDefaultAsync(cancellationToken); return await Collection.Find(x => x.Id == roleId).FirstOrDefaultAsync(cancellationToken);
} }
public async Task<IRole> FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken) public async Task<IdentityRole> FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken)
{ {
return await Collection.Find(x => x.NormalizedName == normalizedRoleName).FirstOrDefaultAsync(cancellationToken); return await Collection.Find(x => x.NormalizedName == normalizedRoleName).FirstOrDefaultAsync(cancellationToken);
} }
public async Task<IdentityResult> CreateAsync(IRole role, CancellationToken cancellationToken) public async Task<IdentityResult> CreateAsync(IdentityRole role, CancellationToken cancellationToken)
{ {
await Collection.InsertOneAsync((MongoRole)role, null, cancellationToken); await Collection.InsertOneAsync(role, null, cancellationToken);
return IdentityResult.Success; return IdentityResult.Success;
} }
public async Task<IdentityResult> UpdateAsync(IRole role, CancellationToken cancellationToken) public async Task<IdentityResult> 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; return IdentityResult.Success;
} }
public async Task<IdentityResult> DeleteAsync(IRole role, CancellationToken cancellationToken) public async Task<IdentityResult> 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; return IdentityResult.Success;
} }
public Task<string> GetRoleIdAsync(IRole role, CancellationToken cancellationToken) public Task<string> GetRoleIdAsync(IdentityRole role, CancellationToken cancellationToken)
{ {
return Task.FromResult(((MongoRole)role).Id); return Task.FromResult(role.Id);
} }
public Task<string> GetRoleNameAsync(IRole role, CancellationToken cancellationToken) public Task<string> GetRoleNameAsync(IdentityRole role, CancellationToken cancellationToken)
{ {
return Task.FromResult(((MongoRole)role).Name); return Task.FromResult(role.Name);
} }
public Task<string> GetNormalizedRoleNameAsync(IRole role, CancellationToken cancellationToken) public Task<string> 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; 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; return TaskHelper.Done;
} }
} }
} }

150
src/Squidex.Domain.Users.MongoDb/MongoUser.cs

@ -5,196 +5,100 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Shared.Users;
namespace Squidex.Domain.Users.MongoDb namespace Squidex.Domain.Users.MongoDb
{ {
public sealed class MongoUser : IUser public sealed class MongoUser : IdentityUser
{ {
[BsonRepresentation(BsonType.ObjectId)] public List<Claim> Claims { get; set; } = new List<Claim>();
[BsonElement]
public string Id { get; set; }
[BsonIgnoreIfNull] public List<UserTokenInfo> Tokens { get; set; } = new List<UserTokenInfo>();
[BsonElement]
public string SecurityStamp { get; set; }
[BsonRequired] public List<UserLoginInfo> Logins { get; set; } = new List<UserLoginInfo>();
[BsonElement]
public string UserName { get; set; }
[BsonRequired] public HashSet<string> Roles { get; set; } = new HashSet<string>();
[BsonElement]
public string NormalizedUserName { get; set; }
[BsonRequired] internal IdentityUserToken<string> FindTokenAsync(string loginProvider, string name)
[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<string> Roles { get; set; } = new List<string>();
[BsonRequired]
[BsonElement]
public List<MongoUserClaim> Claims { get; set; } = new List<MongoUserClaim>();
[BsonRequired]
[BsonElement]
public List<MongoUserToken> Tokens { get; set; } = new List<MongoUserToken>();
[BsonRequired]
[BsonElement]
public List<MongoUserLogin> Logins { get; set; } = new List<MongoUserLogin>();
public bool IsLocked
{
get { return LockoutEndDateUtc != null && LockoutEndDateUtc.Value > DateTime.UtcNow; }
}
IReadOnlyList<Claim> IUser.Claims
{
get { return Claims.Select(x => new Claim(x.Type, x.Value)).ToList(); }
}
IReadOnlyList<ExternalLogin> IUser.Logins
{
get { return Logins.Select(x => new ExternalLogin(x.LoginProvider, x.ProviderKey, x.ProviderDisplayName)).ToList(); }
}
public MongoUser()
{ {
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); Roles.Add(role);
} }
public void RemoveRole(string role) internal void RemoveRole(string role)
{ {
Roles.Remove(role); Roles.Remove(role);
} }
public void AddLogin(UserLoginInfo login) internal void RemoveLogin(string loginProvider, string providerKey)
{
Logins.Add(login);
}
public void RemoveLogin(string loginProvider, string providerKey)
{ {
Logins.RemoveAll(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey); Logins.RemoveAll(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey);
} }
public void RemoveClaims(string type) internal void AddClaim(Claim claim)
{
Claims.RemoveAll(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase));
}
public void AddClaim(Claim claim)
{ {
Claims.Add(claim); Claims.Add(claim);
} }
public void AddClaims(IEnumerable<Claim> claims) internal void AddClaims(IEnumerable<Claim> claims)
{ {
claims.Foreach(AddClaim); claims.Foreach(AddClaim);
} }
public void RemoveClaim(Claim claim) internal void RemoveClaim(Claim claim)
{ {
Claims.RemoveAll(c => c.Type == claim.Type && c.Value == claim.Value); Claims.RemoveAll(c => c.Type == claim.Type && c.Value == claim.Value);
} }
public void RemoveClaims(IEnumerable<Claim> claims) internal void RemoveClaims(IEnumerable<Claim> claims)
{ {
claims.Foreach(RemoveClaim); 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); Tokens.RemoveAll(t => t.LoginProvider == loginProvider && t.Name == name);
} }
public void SetClaim(string type, string value) internal void ReplaceClaim(Claim existingClaim, Claim newClaim)
{
RemoveClaims(type);
AddClaim(new Claim(type, value));
}
public void ReplaceClaim(Claim existingClaim, Claim newClaim)
{ {
RemoveClaim(existingClaim); RemoveClaim(existingClaim);
AddClaim(newClaim); AddClaim(newClaim);
} }
public void SetToken(string loginProider, string name, string value) internal void SetToken(string loginProider, string name, string value)
{ {
RemoveToken(loginProider, name); RemoveToken(loginProider, name);
AddToken(loginProider, name, value); AddToken(loginProider, name, value);
} }
} }
public sealed class UserTokenInfo : IdentityUserToken<string>
{
}
} }

33
src/Squidex.Domain.Users.MongoDb/MongoUserClaim.cs

@ -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);
}
}
}

42
src/Squidex.Domain.Users.MongoDb/MongoUserLogin.cs

@ -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);
}
}
}

269
src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs

@ -13,29 +13,96 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
using Squidex.Shared.Users;
namespace Squidex.Domain.Users.MongoDb namespace Squidex.Domain.Users.MongoDb
{ {
public sealed class MongoUserStore : public sealed class MongoUserStore :
MongoRepositoryBase<MongoUser>, MongoRepositoryBase<MongoUser>,
IUserPasswordStore<IUser>, IUserAuthenticationTokenStore<IdentityUser>,
IUserRoleStore<IUser>, IUserAuthenticatorKeyStore<IdentityUser>,
IUserLoginStore<IUser>, IUserClaimStore<IdentityUser>,
IUserSecurityStampStore<IUser>, IUserEmailStore<IdentityUser>,
IUserEmailStore<IUser>,
IUserClaimStore<IUser>,
IUserPhoneNumberStore<IUser>,
IUserTwoFactorStore<IUser>,
IUserLockoutStore<IUser>,
IUserAuthenticationTokenStore<IUser>,
IUserFactory, IUserFactory,
IUserResolver, IUserLockoutStore<IdentityUser>,
IQueryableUserStore<IUser> IUserLoginStore<IdentityUser>,
IUserPasswordStore<IdentityUser>,
IUserPhoneNumberStore<IdentityUser>,
IUserSecurityStampStore<IdentityUser>,
IUserTwoFactorStore<IdentityUser>,
IUserTwoFactorRecoveryCodeStore<IdentityUser>,
IQueryableUserStore<IdentityUser>
{ {
private const string InternalLoginProvider = "[AspNetUserStore]";
private const string AuthenticatorKeyTokenName = "AuthenticatorKey";
private const string RecoveryCodeTokenName = "RecoveryCodes";
static MongoUserStore()
{
BsonClassMap.RegisterClassMap<Claim>(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<UserLoginInfo>(cm =>
{
cm.MapConstructor(typeof(UserLoginInfo).GetConstructors().First())
.SetArguments(new[]
{
nameof(UserLoginInfo.LoginProvider),
nameof(UserLoginInfo.ProviderKey),
nameof(UserLoginInfo.ProviderDisplayName)
});
cm.AutoMap();
});
BsonClassMap.RegisterClassMap<IdentityUserToken<string>>(cm =>
{
cm.AutoMap();
cm.UnmapMember(x => x.UserId);
});
BsonClassMap.RegisterClassMap<IdentityUser<string>>(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) public MongoUserStore(IMongoDatabase database)
: base(database) : base(database)
{ {
@ -66,352 +133,374 @@ namespace Squidex.Domain.Users.MongoDb
{ {
} }
public IQueryable<IUser> Users public IQueryable<IdentityUser> Users
{ {
get { return Collection.AsQueryable(); } 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 }; return new MongoUser { Email = email, UserName = email };
} }
public async Task<IUser> FindByIdAsync(string userId, CancellationToken cancellationToken) public async Task<IdentityUser> FindByIdAsync(string userId, CancellationToken cancellationToken)
{ {
return await Collection.Find(x => x.Id == userId).FirstOrDefaultAsync(cancellationToken); return await Collection.Find(x => x.Id == userId).FirstOrDefaultAsync(cancellationToken);
} }
public async Task<IUser> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken) public async Task<IdentityUser> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken)
{ {
return await Collection.Find(x => x.NormalizedEmail == normalizedEmail).FirstOrDefaultAsync(cancellationToken); return await Collection.Find(x => x.NormalizedEmail == normalizedEmail).FirstOrDefaultAsync(cancellationToken);
} }
public async Task<IUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) public async Task<IdentityUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
{ {
return await Collection.Find(x => x.NormalizedEmail == normalizedUserName).FirstOrDefaultAsync(cancellationToken); return await Collection.Find(x => x.NormalizedEmail == normalizedUserName).FirstOrDefaultAsync(cancellationToken);
} }
public async Task<IUser> FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) public async Task<IdentityUser> FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken)
{ {
return await Collection.Find(x => x.Logins.Any(y => y.LoginProvider == loginProvider && y.ProviderKey == providerKey)).FirstOrDefaultAsync(cancellationToken); return await Collection.Find(x => x.Logins.Any(y => y.LoginProvider == loginProvider && y.ProviderKey == providerKey)).FirstOrDefaultAsync(cancellationToken);
} }
public async Task<IList<IUser>> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken) public async Task<IList<IdentityUser>> 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<IUser>().ToList(); return (await Collection.Find(x => x.Claims.Any(y => y.Type == claim.Type && y.Value == claim.Value)).ToListAsync(cancellationToken)).OfType<IdentityUser>().ToList();
} }
public async Task<IList<IUser>> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken) public async Task<IList<IdentityUser>> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken)
{ {
return (await Collection.Find(x => x.Roles.Contains(roleName)).ToListAsync(cancellationToken)).OfType<IUser>().ToList(); return (await Collection.Find(x => x.Roles.Contains(roleName)).ToListAsync(cancellationToken)).OfType<IdentityUser>().ToList();
} }
public async Task<IdentityResult> CreateAsync(IUser user, CancellationToken cancellationToken) public async Task<IdentityResult> CreateAsync(IdentityUser user, CancellationToken cancellationToken)
{ {
user.Id = ObjectId.GenerateNewId().ToString();
await Collection.InsertOneAsync((MongoUser)user, null, cancellationToken); await Collection.InsertOneAsync((MongoUser)user, null, cancellationToken);
return IdentityResult.Success; return IdentityResult.Success;
} }
public async Task<IdentityResult> UpdateAsync(IUser user, CancellationToken cancellationToken) public async Task<IdentityResult> UpdateAsync(IdentityUser user, CancellationToken cancellationToken)
{ {
await Collection.ReplaceOneAsync(x => x.Id == user.Id, (MongoUser)user, null, cancellationToken); await Collection.ReplaceOneAsync(x => x.Id == user.Id, (MongoUser)user, null, cancellationToken);
return IdentityResult.Success; return IdentityResult.Success;
} }
public async Task<IdentityResult> DeleteAsync(IUser user, CancellationToken cancellationToken) public async Task<IdentityResult> DeleteAsync(IdentityUser user, CancellationToken cancellationToken)
{ {
await Collection.DeleteOneAsync(x => x.Id == user.Id, null, cancellationToken); await Collection.DeleteOneAsync(x => x.Id == user.Id, null, cancellationToken);
return IdentityResult.Success; return IdentityResult.Success;
} }
public Task<string> GetUserIdAsync(IUser user, CancellationToken cancellationToken) public Task<string> GetUserIdAsync(IdentityUser user, CancellationToken cancellationToken)
{ {
return Task.FromResult(((MongoUser)user).Id); return Task.FromResult(((MongoUser)user).Id);
} }
public Task<string> GetUserNameAsync(IUser user, CancellationToken cancellationToken) public Task<string> GetUserNameAsync(IdentityUser user, CancellationToken cancellationToken)
{ {
return Task.FromResult(((MongoUser)user).UserName); return Task.FromResult(((MongoUser)user).UserName);
} }
public Task<string> GetNormalizedUserNameAsync(IUser user, CancellationToken cancellationToken) public Task<string> GetNormalizedUserNameAsync(IdentityUser user, CancellationToken cancellationToken)
{ {
return Task.FromResult(((MongoUser)user).NormalizedUserName); return Task.FromResult(((MongoUser)user).NormalizedUserName);
} }
public Task<string> GetPasswordHashAsync(IUser user, CancellationToken cancellationToken) public Task<string> GetPasswordHashAsync(IdentityUser user, CancellationToken cancellationToken)
{ {
return Task.FromResult(((MongoUser)user).PasswordHash); return Task.FromResult(((MongoUser)user).PasswordHash);
} }
public Task<IList<string>> GetRolesAsync(IUser user, CancellationToken cancellationToken) public Task<IList<string>> GetRolesAsync(IdentityUser user, CancellationToken cancellationToken)
{ {
return Task.FromResult<IList<string>>(((MongoUser)user).Roles); return Task.FromResult<IList<string>>(((MongoUser)user).Roles.ToList());
} }
public Task<bool> IsInRoleAsync(IUser user, string roleName, CancellationToken cancellationToken) public Task<bool> IsInRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken)
{ {
return Task.FromResult(((MongoUser)user).Roles.Contains(roleName)); return Task.FromResult(((MongoUser)user).Roles.Contains(roleName));
} }
public Task<IList<UserLoginInfo>> GetLoginsAsync(IUser user, CancellationToken cancellationToken) public Task<IList<UserLoginInfo>> GetLoginsAsync(IdentityUser user, CancellationToken cancellationToken)
{ {
return Task.FromResult<IList<UserLoginInfo>>(((MongoUser)user).Logins.Select(x => (UserLoginInfo)x).ToList()); return Task.FromResult<IList<UserLoginInfo>>(((MongoUser)user).Logins.Select(x => new UserLoginInfo(x.LoginProvider, x.ProviderKey, x.ProviderDisplayName)).ToList());
} }
public Task<string> GetSecurityStampAsync(IUser user, CancellationToken cancellationToken) public Task<string> GetSecurityStampAsync(IdentityUser user, CancellationToken cancellationToken)
{ {
return Task.FromResult(((MongoUser)user).SecurityStamp); return Task.FromResult(((MongoUser)user).SecurityStamp);
} }
public Task<string> GetEmailAsync(IUser user, CancellationToken cancellationToken) public Task<string> GetEmailAsync(IdentityUser user, CancellationToken cancellationToken)
{ {
return Task.FromResult(((MongoUser)user).Email); return Task.FromResult(((MongoUser)user).Email);
} }
public Task<bool> GetEmailConfirmedAsync(IUser user, CancellationToken cancellationToken) public Task<bool> GetEmailConfirmedAsync(IdentityUser user, CancellationToken cancellationToken)
{ {
return Task.FromResult(((MongoUser)user).EmailConfirmed); return Task.FromResult(((MongoUser)user).EmailConfirmed);
} }
public Task<string> GetNormalizedEmailAsync(IUser user, CancellationToken cancellationToken) public Task<string> GetNormalizedEmailAsync(IdentityUser user, CancellationToken cancellationToken)
{ {
return Task.FromResult(((MongoUser)user).NormalizedEmail); return Task.FromResult(((MongoUser)user).NormalizedEmail);
} }
public Task<IList<Claim>> GetClaimsAsync(IUser user, CancellationToken cancellationToken) public Task<IList<Claim>> GetClaimsAsync(IdentityUser user, CancellationToken cancellationToken)
{ {
return Task.FromResult<IList<Claim>>(((MongoUser)user).Claims.Select(x => (Claim)x).ToList()); return Task.FromResult<IList<Claim>>(((MongoUser)user).Claims);
} }
public Task<string> GetPhoneNumberAsync(IUser user, CancellationToken cancellationToken) public Task<string> GetPhoneNumberAsync(IdentityUser user, CancellationToken cancellationToken)
{ {
return Task.FromResult(((MongoUser)user).PhoneNumber); return Task.FromResult(((MongoUser)user).PhoneNumber);
} }
public Task<bool> GetPhoneNumberConfirmedAsync(IUser user, CancellationToken cancellationToken) public Task<bool> GetPhoneNumberConfirmedAsync(IdentityUser user, CancellationToken cancellationToken)
{ {
return Task.FromResult(((MongoUser)user).PhoneNumberConfirmed); return Task.FromResult(((MongoUser)user).PhoneNumberConfirmed);
} }
public Task<bool> GetTwoFactorEnabledAsync(IUser user, CancellationToken cancellationToken) public Task<bool> GetTwoFactorEnabledAsync(IdentityUser user, CancellationToken cancellationToken)
{ {
return Task.FromResult(((MongoUser)user).TwoFactorEnabled); return Task.FromResult(((MongoUser)user).TwoFactorEnabled);
} }
public Task<DateTimeOffset?> GetLockoutEndDateAsync(IUser user, CancellationToken cancellationToken) public Task<DateTimeOffset?> GetLockoutEndDateAsync(IdentityUser user, CancellationToken cancellationToken)
{ {
return Task.FromResult<DateTimeOffset?>(((MongoUser)user).LockoutEndDateUtc); return Task.FromResult<DateTimeOffset?>(((MongoUser)user).LockoutEnd);
} }
public Task<int> GetAccessFailedCountAsync(IUser user, CancellationToken cancellationToken) public Task<int> GetAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken)
{ {
return Task.FromResult(((MongoUser)user).AccessFailedCount); return Task.FromResult(((MongoUser)user).AccessFailedCount);
} }
public Task<bool> GetLockoutEnabledAsync(IUser user, CancellationToken cancellationToken) public Task<bool> GetLockoutEnabledAsync(IdentityUser user, CancellationToken cancellationToken)
{ {
return Task.FromResult(((MongoUser)user).LockoutEnabled); return Task.FromResult(((MongoUser)user).LockoutEnabled);
} }
public Task<string> GetTokenAsync(IUser user, string loginProvider, string name, CancellationToken cancellationToken) public Task<string> GetTokenAsync(IdentityUser user, string loginProvider, string name, CancellationToken cancellationToken)
{ {
return Task.FromResult(((MongoUser)user).GetToken(loginProvider, name)); return Task.FromResult(((MongoUser)user).GetToken(loginProvider, name));
} }
public Task<bool> HasPasswordAsync(IUser user, CancellationToken cancellationToken) public Task<string> GetAuthenticatorKeyAsync(IdentityUser user, CancellationToken cancellationToken)
{
return Task.FromResult(((MongoUser)user).GetToken(InternalLoginProvider, AuthenticatorKeyTokenName));
}
public Task<bool> HasPasswordAsync(IdentityUser user, CancellationToken cancellationToken)
{ {
return Task.FromResult(!string.IsNullOrWhiteSpace(((MongoUser)user).PasswordHash)); return Task.FromResult(!string.IsNullOrWhiteSpace(((MongoUser)user).PasswordHash));
} }
public Task SetUserNameAsync(IUser user, string userName, CancellationToken cancellationToken) public Task<int> 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; ((MongoUser)user).UserName = userName;
return TaskHelper.Done; 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; ((MongoUser)user).NormalizedUserName = normalizedName;
return TaskHelper.Done; 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; ((MongoUser)user).PasswordHash = passwordHash;
return TaskHelper.Done; 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); ((MongoUser)user).AddRole(roleName);
return TaskHelper.Done; 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); ((MongoUser)user).RemoveRole(roleName);
return TaskHelper.Done; 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); ((MongoUser)user).AddLogin(login);
return TaskHelper.Done; 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); ((MongoUser)user).RemoveLogin(loginProvider, providerKey);
return TaskHelper.Done; 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; ((MongoUser)user).SecurityStamp = stamp;
return TaskHelper.Done; 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; ((MongoUser)user).Email = email;
return TaskHelper.Done; 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; ((MongoUser)user).EmailConfirmed = confirmed;
return TaskHelper.Done; 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; ((MongoUser)user).NormalizedEmail = normalizedEmail;
return TaskHelper.Done; return TaskHelper.Done;
} }
public Task AddClaimsAsync(IUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken) public Task AddClaimsAsync(IdentityUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken)
{ {
((MongoUser)user).AddClaims(claims); ((MongoUser)user).AddClaims(claims);
return TaskHelper.Done; 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); ((MongoUser)user).ReplaceClaim(claim, newClaim);
return TaskHelper.Done; return TaskHelper.Done;
} }
public Task RemoveClaimsAsync(IUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken) public Task RemoveClaimsAsync(IdentityUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken)
{ {
((MongoUser)user).RemoveClaims(claims); ((MongoUser)user).RemoveClaims(claims);
return TaskHelper.Done; 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; ((MongoUser)user).PhoneNumber = phoneNumber;
return TaskHelper.Done; 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; ((MongoUser)user).PhoneNumberConfirmed = confirmed;
return TaskHelper.Done; 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; ((MongoUser)user).TwoFactorEnabled = enabled;
return TaskHelper.Done; 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; return TaskHelper.Done;
} }
public Task<int> IncrementAccessFailedCountAsync(IUser user, CancellationToken cancellationToken) public Task<int> IncrementAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken)
{ {
((MongoUser)user).AccessFailedCount++; ((MongoUser)user).AccessFailedCount++;
return Task.FromResult(((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; ((MongoUser)user).AccessFailedCount = 0;
return TaskHelper.Done; 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; ((MongoUser)user).LockoutEnabled = enabled;
return TaskHelper.Done; 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); ((MongoUser)user).SetToken(loginProvider, name, value);
return TaskHelper.Done; 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); ((MongoUser)user).RemoveToken(loginProvider, name);
return TaskHelper.Done; return TaskHelper.Done;
} }
public async Task<IUser> FindByIdOrEmailAsync(string id) public Task SetAuthenticatorKeyAsync(IdentityUser user, string key, CancellationToken cancellationToken)
{ {
if (ObjectId.TryParse(id, out _)) ((MongoUser)user).SetToken(InternalLoginProvider, AuthenticatorKeyTokenName, key);
{
return await Collection.Find(x => x.Id == id).FirstOrDefaultAsync(); return TaskHelper.Done;
} }
else
{ public Task ReplaceCodesAsync(IdentityUser user, IEnumerable<string> recoveryCodes, CancellationToken cancellationToken)
return await Collection.Find(x => x.NormalizedEmail == id.ToUpperInvariant()).FirstOrDefaultAsync(); {
} ((MongoUser)user).SetToken(InternalLoginProvider, RecoveryCodeTokenName, string.Join(";", recoveryCodes));
return TaskHelper.Done;
} }
public Task<List<IUser>> QueryByEmailAsync(string email) public Task<bool> 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<string>(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;
} }
} }
} }

26
src/Squidex.Domain.Users.MongoDb/MongoUserToken.cs

@ -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; }
}
}

50
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<IdentityUser> userManager;
private readonly IUserFactory userFactory;
public DefaultUserResolver(UserManager<IdentityUser> userManager, IUserFactory userFactory)
{
Guard.NotNull(userManager, nameof(userManager));
Guard.NotNull(userFactory, nameof(userFactory));
this.userManager = userManager;
this.userFactory = userFactory;
}
public async Task<IUser> FindByIdOrEmailAsync(string idOrEmail)
{
if (userFactory.IsId(idOrEmail))
{
return await userManager.FindByIdWithClaimsAsync(idOrEmail);
}
else
{
return await userManager.FindByEmailWithClaimsAsyncAsync(idOrEmail);
}
}
public async Task<List<IUser>> QueryByEmailAsync(string email)
{
var result = await userManager.QueryByEmailAsync(email);
return result.OfType<IUser>().ToList();
}
}
}

14
src/Squidex.Domain.Users/IRole.cs

@ -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; }
}
}

14
src/Squidex.Domain.Users/IRoleFactory.cs

@ -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);
}
}

6
src/Squidex.Domain.Users/IUserFactory.cs

@ -5,12 +5,14 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Shared.Users; using Microsoft.AspNetCore.Identity;
namespace Squidex.Domain.Users namespace Squidex.Domain.Users
{ {
public interface IUserFactory public interface IUserFactory
{ {
IUser Create(string email); IdentityUser Create(string email);
bool IsId(string id);
} }
} }

5
src/Squidex.Domain.Users/PwnedPasswordValidator.cs

@ -11,11 +11,10 @@ using Microsoft.AspNetCore.Identity;
using SharpPwned.NET; using SharpPwned.NET;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Shared.Users;
namespace Squidex.Domain.Users namespace Squidex.Domain.Users
{ {
public sealed class PwnedPasswordValidator : IPasswordValidator<IUser> public sealed class PwnedPasswordValidator : IPasswordValidator<IdentityUser>
{ {
private const string ErrorCode = "PwnedError"; 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!"; 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; this.log = log;
} }
public async Task<IdentityResult> ValidateAsync(UserManager<IUser> manager, IUser user, string password) public async Task<IdentityResult> ValidateAsync(UserManager<IdentityUser> manager, IdentityUser user, string password)
{ {
try try
{ {

1
src/Squidex.Domain.Users/Squidex.Domain.Users.csproj

@ -12,6 +12,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.1.6" /> <PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.1.6" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="2.1.6" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.5.0" /> <PackageReference Include="Microsoft.Win32.Registry" Version="4.5.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="SharpPwned.NET" Version="1.0.8" /> <PackageReference Include="SharpPwned.NET" Version="1.0.8" />

7
src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs

@ -11,18 +11,17 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Shared.Users;
namespace Squidex.Domain.Users namespace Squidex.Domain.Users
{ {
public sealed class UserClaimsPrincipalFactoryWithEmail : UserClaimsPrincipalFactory<IUser, IRole> public sealed class UserClaimsPrincipalFactoryWithEmail : UserClaimsPrincipalFactory<IdentityUser, IdentityRole>
{ {
public UserClaimsPrincipalFactoryWithEmail(UserManager<IUser> userManager, RoleManager<IRole> roleManager, IOptions<IdentityOptions> optionsAccessor) public UserClaimsPrincipalFactoryWithEmail(UserManager<IdentityUser> userManager, RoleManager<IdentityRole> roleManager, IOptions<IdentityOptions> optionsAccessor)
: base(userManager, roleManager, optionsAccessor) : base(userManager, roleManager, optionsAccessor)
{ {
} }
public override async Task<ClaimsPrincipal> CreateAsync(IUser user) public override async Task<ClaimsPrincipal> CreateAsync(IdentityUser user)
{ {
var principal = await base.CreateAsync(user); var principal = await base.CreateAsync(user);

178
src/Squidex.Domain.Users/UserManagerExtensions.cs

@ -8,31 +8,88 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using Squidex.Shared.Users;
namespace Squidex.Domain.Users namespace Squidex.Domain.Users
{ {
public static class UserManagerExtensions public static class UserManagerExtensions
{ {
public static Task<IReadOnlyList<IUser>> QueryByEmailAsync(this UserManager<IUser> userManager, string email = null, int take = 10, int skip = 0) public static async Task<UserWithClaims> GetUserWithClaimsAsync(this UserManager<IdentityUser> 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<UserWithClaims> ResolveUserAsync(this UserManager<IdentityUser> userManager, IdentityUser user)
{
if (user == null)
{
return null;
}
var claims = await userManager.GetClaimsAsync(user);
return new UserWithClaims(user, claims);
}
public static async Task<UserWithClaims> FindByIdWithClaimsAsync(this UserManager<IdentityUser> userManager, string id)
{
if (id == null)
{
return null;
}
var user = await userManager.FindByIdAsync(id);
return await userManager.ResolveUserAsync(user);
}
public static async Task<UserWithClaims> FindByEmailWithClaimsAsyncAsync(this UserManager<IdentityUser> userManager, string email)
{
if (email == null)
{
return null;
}
return Task.FromResult<IReadOnlyList<IUser>>(users); var user = await userManager.FindByEmailAsync(email);
return await userManager.ResolveUserAsync(user);
} }
public static Task<long> CountByEmailAsync(this UserManager<IUser> userManager, string email = null) public static Task<long> CountByEmailAsync(this UserManager<IdentityUser> userManager, string email = null)
{ {
var count = QueryUsers(userManager, email).LongCount(); var count = QueryUsers(userManager, email).LongCount();
return Task.FromResult(count); return Task.FromResult(count);
} }
private static IQueryable<IUser> QueryUsers(UserManager<IUser> userManager, string email = null) public static async Task<List<UserWithClaims>> QueryByEmailAsync(this UserManager<IdentityUser> 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<UserWithClaims[]> ResolveUsersAsync(this UserManager<IdentityUser> userManager, IEnumerable<IdentityUser> users)
{
return Task.WhenAll(users.Select(async user =>
{
return await userManager.ResolveUserAsync(user);
}));
}
public static IQueryable<IdentityUser> QueryUsers(UserManager<IdentityUser> userManager, string email = null)
{ {
var result = userManager.Users; var result = userManager.Users;
@ -46,25 +103,24 @@ namespace Squidex.Domain.Users
return result; return result;
} }
public static async Task<IUser> CreateAsync(this UserManager<IUser> userManager, IUserFactory factory, string email, string displayName, string password, PermissionSet permissions = null) public static async Task<IdentityUser> CreateAsync(this UserManager<IdentityUser> userManager, IUserFactory factory, UserValues values)
{ {
var user = factory.Create(email); var user = factory.Create(values.Email);
try try
{ {
user.SetDisplayName(displayName); await DoChecked(() => userManager.CreateAsync(user), "Cannot create user.");
user.SetPictureUrlFromGravatar(email);
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(values.Password))
if (!string.IsNullOrWhiteSpace(password))
{ {
await DoChecked(() => userManager.AddPasswordAsync(user, password), "Cannot create user."); await DoChecked(() => userManager.AddPasswordAsync(user, values.Password), "Cannot create user.");
} }
} }
catch catch
@ -77,68 +133,73 @@ namespace Squidex.Domain.Users
return user; return user;
} }
public static Task<IdentityResult> UpdateAsync(this UserManager<IUser> userManager, IUser user, string email, string displayName, bool hidden) public static async Task UpdateAsync(this UserManager<IdentityUser> userManager, string id, UserValues values)
{
user.SetHidden(hidden);
user.SetEmail(email);
user.SetDisplayName(displayName);
return userManager.UpdateAsync(user);
}
public static async Task UpdateAsync(this UserManager<IUser> userManager, string id, string email, string displayName, string password, PermissionSet permissions = null)
{ {
var user = await userManager.FindByIdAsync(id); var user = await userManager.FindByIdAsync(id);
if (user == null) 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<IdentityResult> UpdateSafeAsync(this UserManager<IdentityUser> 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."); return IdentityResult.Failed(ex.Errors.Select(x => new IdentityError { Description = x.Message }).ToArray());
await DoChecked(() => userManager.SetUserNameAsync(user, email), "Cannot update email.");
} }
}
if (!string.IsNullOrWhiteSpace(displayName)) public static async Task UpdateAsync(this UserManager<IdentityUser> 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.RemovePasswordAsync(user), "Cannot replace password.");
await DoChecked(() => userManager.AddPasswordAsync(user, password), "Cannot update user."); await DoChecked(() => userManager.AddPasswordAsync(user, values.Password), "Cannot replace password.");
} }
} }
public static async Task LockAsync(this UserManager<IUser> userManager, string id) public static async Task LockAsync(this UserManager<IdentityUser> userManager, string id)
{ {
var user = await userManager.FindByIdAsync(id); var user = await userManager.FindByIdAsync(id);
if (user == null) 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."); await DoChecked(() => userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddYears(100)), "Cannot lock user.");
} }
public static async Task UnlockAsync(this UserManager<IUser> userManager, string id) public static async Task UnlockAsync(this UserManager<IdentityUser> userManager, string id)
{ {
var user = await userManager.FindByIdAsync(id); var user = await userManager.FindByIdAsync(id);
if (user == null) if (user == null)
{ {
throw new DomainObjectNotFoundException(id, typeof(IUser)); throw new DomainObjectNotFoundException(id, typeof(IdentityUser));
} }
await DoChecked(() => userManager.SetLockoutEndDateAsync(user, null), "Cannot unlock user."); 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()); throw new ValidationException(message, result.Errors.Select(x => new ValidationError(x.Description)).ToArray());
} }
} }
public static async Task<IdentityResult> SyncClaimsAsync(this UserManager<IdentityUser> userManager, IdentityUser user, IEnumerable<Claim> claims)
{
if (claims.Any())
{
var oldClaims = await userManager.GetClaimsAsync(user);
var oldClaimsToRemove = new List<Claim>();
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;
}
} }
} }

69
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<Claim> 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);
}
}
}
}
}

48
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<Claim> Claims { get; }
public string Id
{
get { return Identity.Id; }
}
public string Email
{
get { return Identity.Email; }
}
IReadOnlyList<Claim> IUser.Claims
{
get { return Claims; }
}
public UserWithClaims(IdentityUser user, IEnumerable<Claim> claims)
{
Guard.NotNull(user, nameof(user));
Guard.NotNull(claims, nameof(claims));
Identity = user;
Claims = claims.ToList();
}
}
}

2
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 Permissions = "urn:squidex:permissions";
public static readonly string Prefix = "urn:squidex:"; public static readonly string Prefix = "urn:squidex:";
public static readonly string PictureUrlStore = "store";
} }
} }

31
src/Squidex.Shared/Users/ExternalLogin.cs

@ -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;
}
}
}
}

14
src/Squidex.Shared/Users/IUser.cs

@ -12,24 +12,10 @@ namespace Squidex.Shared.Users
{ {
public interface IUser public interface IUser
{ {
bool IsLocked { get; }
string Id { get; } string Id { get; }
string Email { get; } string Email { get; }
string NormalizedEmail { get; }
IReadOnlyList<Claim> Claims { get; } IReadOnlyList<Claim> Claims { get; }
IReadOnlyList<ExternalLogin> Logins { get; }
void RemoveClaims(string type);
void SetEmail(string email);
void SetClaim(string type, string value);
void AddClaim(Claim claim);
} }
} }

53
src/Squidex.Shared/Users/UserExtensions.cs

@ -7,8 +7,6 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Security.Claims;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Shared.Identity; using Squidex.Shared.Identity;
@ -16,49 +14,9 @@ namespace Squidex.Shared.Users
{ {
public static class UserExtensions public static class UserExtensions
{ {
public static void SetDisplayName(this IUser user, string displayName) public static PermissionSet Permissions(this IUser user)
{
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)
{ {
user.RemoveClaims(SquidexClaimTypes.Permissions); return new PermissionSet(user.GetClaimValues(SquidexClaimTypes.Permissions).Select(x => new Permission(x)));
foreach (var permission in permissions)
{
user.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission.Id));
}
} }
public static bool IsHidden(this IUser user) public static bool IsHidden(this IUser user)
@ -88,7 +46,7 @@ namespace Squidex.Shared.Users
public static bool IsPictureUrlStored(this IUser user) 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) public static string PictureUrl(this IUser user)
@ -101,11 +59,6 @@ namespace Squidex.Shared.Users
return user.GetClaimValue(SquidexClaimTypes.DisplayName); 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) public static string GetClaimValue(this IUser user, string type)
{ {
return user.Claims.FirstOrDefault(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase))?.Value; return user.Claims.FirstOrDefault(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase))?.Value;

7
src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs

@ -6,6 +6,8 @@
// ========================================================================== // ==========================================================================
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Users;
using Squidex.Infrastructure.Security;
namespace Squidex.Areas.Api.Controllers.Users.Models namespace Squidex.Areas.Api.Controllers.Users.Models
{ {
@ -35,5 +37,10 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
/// </summary> /// </summary>
[Required] [Required]
public string[] Permissions { get; set; } public string[] Permissions { get; set; }
public UserValues ToValues()
{
return new UserValues { Email = Email, DisplayName = DisplayName, Password = Password, Permissions = new PermissionSet(Permissions) };
}
} }
} }

7
src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs

@ -6,6 +6,8 @@
// ========================================================================== // ==========================================================================
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Users;
using Squidex.Infrastructure.Security;
namespace Squidex.Areas.Api.Controllers.Users.Models namespace Squidex.Areas.Api.Controllers.Users.Models
{ {
@ -34,5 +36,10 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
/// </summary> /// </summary>
[Required] [Required]
public string[] Permissions { get; set; } public string[] Permissions { get; set; }
public UserValues ToValues()
{
return new UserValues { Email = Email, DisplayName = DisplayName, Password = Password, Permissions = new PermissionSet(Permissions) };
}
} }
} }

11
src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs

@ -17,17 +17,16 @@ using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Shared; using Squidex.Shared;
using Squidex.Shared.Users;
namespace Squidex.Areas.Api.Controllers.Users namespace Squidex.Areas.Api.Controllers.Users
{ {
[ApiModelValidation(true)] [ApiModelValidation(true)]
public sealed class UserManagementController : ApiController public sealed class UserManagementController : ApiController
{ {
private readonly UserManager<IUser> userManager; private readonly UserManager<IdentityUser> userManager;
private readonly IUserFactory userFactory; private readonly IUserFactory userFactory;
public UserManagementController(ICommandBus commandBus, UserManager<IUser> userManager, IUserFactory userFactory) public UserManagementController(ICommandBus commandBus, UserManager<IdentityUser> userManager, IUserFactory userFactory)
: base(commandBus) : base(commandBus)
{ {
this.userManager = userManager; this.userManager = userManager;
@ -58,7 +57,7 @@ namespace Squidex.Areas.Api.Controllers.Users
[ApiPermission(Permissions.AdminUsersRead)] [ApiPermission(Permissions.AdminUsersRead)]
public async Task<IActionResult> GetUser(string id) public async Task<IActionResult> GetUser(string id)
{ {
var entity = await userManager.FindByIdAsync(id); var entity = await userManager.FindByIdWithClaimsAsync(id);
if (entity == null) if (entity == null)
{ {
@ -75,7 +74,7 @@ namespace Squidex.Areas.Api.Controllers.Users
[ApiPermission(Permissions.AdminUsersCreate)] [ApiPermission(Permissions.AdminUsersCreate)]
public async Task<IActionResult> PostUser([FromBody] CreateUserDto request) public async Task<IActionResult> 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 }; var response = new UserCreatedDto { Id = user.Id };
@ -87,7 +86,7 @@ namespace Squidex.Areas.Api.Controllers.Users
[ApiPermission(Permissions.AdminUsersUpdate)] [ApiPermission(Permissions.AdminUsersUpdate)]
public async Task<IActionResult> PutUser(string id, [FromBody] UpdateUserDto request) public async Task<IActionResult> 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(); return NoContent();
} }

13
src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs

@ -17,7 +17,6 @@ using Squidex.Domain.Users;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Shared; using Squidex.Shared;
using Squidex.Shared.Users;
namespace Squidex.Areas.IdentityServer.Config namespace Squidex.Areas.IdentityServer.Config
{ {
@ -34,7 +33,7 @@ namespace Squidex.Areas.IdentityServer.Config
{ {
var options = services.GetService<IOptions<MyIdentityOptions>>().Value; var options = services.GetService<IOptions<MyIdentityOptions>>().Value;
var userManager = services.GetService<UserManager<IUser>>(); var userManager = services.GetService<UserManager<IdentityUser>>();
var userFactory = services.GetService<IUserFactory>(); var userFactory = services.GetService<IUserFactory>();
var log = services.GetService<ISemanticLog>(); var log = services.GetService<ISemanticLog>();
@ -50,9 +49,15 @@ namespace Squidex.Areas.IdentityServer.Config
{ {
try 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) catch (Exception ex)
{ {

9
src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs

@ -20,7 +20,6 @@ using Microsoft.Extensions.Options;
using Squidex.Config; using Squidex.Config;
using Squidex.Domain.Users; using Squidex.Domain.Users;
using Squidex.Shared.Identity; using Squidex.Shared.Identity;
using Squidex.Shared.Users;
namespace Squidex.Areas.IdentityServer.Config namespace Squidex.Areas.IdentityServer.Config
{ {
@ -55,11 +54,11 @@ namespace Squidex.Areas.IdentityServer.Config
services.AddSingleton(GetApiResources()); services.AddSingleton(GetApiResources());
services.AddSingleton(GetIdentityResources()); services.AddSingleton(GetIdentityResources());
services.AddIdentity<IUser, IRole>() services.AddIdentity<IdentityUser, IdentityRole>()
.AddDefaultTokenProviders(); .AddDefaultTokenProviders();
services.AddSingleton<IPasswordValidator<IUser>, services.AddSingleton<IPasswordValidator<IdentityUser>,
PwnedPasswordValidator>(); PwnedPasswordValidator>();
services.AddSingleton<IUserClaimsPrincipalFactory<IUser>, services.AddSingleton<IUserClaimsPrincipalFactory<IdentityUser>,
UserClaimsPrincipalFactoryWithEmail>(); UserClaimsPrincipalFactoryWithEmail>();
services.AddSingleton<IClientStore, services.AddSingleton<IClientStore,
LazyClientStore>(); LazyClientStore>();
@ -70,7 +69,7 @@ namespace Squidex.Areas.IdentityServer.Config
{ {
options.UserInteraction.ErrorUrl = "/error/"; options.UserInteraction.ErrorUrl = "/error/";
}) })
.AddAspNetIdentity<IUser>() .AddAspNetIdentity<IdentityUser>()
.AddInMemoryApiResources(GetApiResources()) .AddInMemoryApiResources(GetApiResources())
.AddInMemoryIdentityResources(GetIdentityResources()) .AddInMemoryIdentityResources(GetIdentityResources())
.AddSigningCredential(certificate); .AddSigningCredential(certificate);

76
src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Security; using System.Security;
@ -30,20 +31,22 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
{ {
public sealed class AccountController : IdentityServerController public sealed class AccountController : IdentityServerController
{ {
private readonly SignInManager<IUser> signInManager; private readonly SignInManager<IdentityUser> signInManager;
private readonly UserManager<IUser> userManager; private readonly UserManager<IdentityUser> userManager;
private readonly IUserFactory userFactory; private readonly IUserFactory userFactory;
private readonly IUserEvents userEvents; private readonly IUserEvents userEvents;
private readonly IUserResolver userResolver;
private readonly IOptions<MyIdentityOptions> identityOptions; private readonly IOptions<MyIdentityOptions> identityOptions;
private readonly IOptions<MyUrlsOptions> urlOptions; private readonly IOptions<MyUrlsOptions> urlOptions;
private readonly ISemanticLog log; private readonly ISemanticLog log;
private readonly IIdentityServerInteractionService interactions; private readonly IIdentityServerInteractionService interactions;
public AccountController( public AccountController(
SignInManager<IUser> signInManager, SignInManager<IdentityUser> signInManager,
UserManager<IUser> userManager, UserManager<IdentityUser> userManager,
IUserFactory userFactory, IUserFactory userFactory,
IUserEvents userEvents, IUserEvents userEvents,
IUserResolver userResolver,
IOptions<MyIdentityOptions> identityOptions, IOptions<MyIdentityOptions> identityOptions,
IOptions<MyUrlsOptions> urlOptions, IOptions<MyUrlsOptions> urlOptions,
ISemanticLog log, ISemanticLog log,
@ -52,6 +55,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
this.log = log; this.log = log;
this.urlOptions = urlOptions; this.urlOptions = urlOptions;
this.userEvents = userEvents; this.userEvents = userEvents;
this.userResolver = userResolver;
this.userManager = userManager; this.userManager = userManager;
this.userFactory = userFactory; this.userFactory = userFactory;
this.interactions = interactions; this.interactions = interactions;
@ -115,12 +119,15 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
return View(vm); return View(vm);
} }
var user = await userManager.GetUserAsync(User); var user = await userManager.GetUserWithClaimsAsync(User);
user.SetConsentForEmails(model.ConsentToAutomatedEmails); var update = new UserValues
user.SetConsent(); {
Consent = true,
ConsentForEmails = model.ConsentToAutomatedEmails
};
await userManager.UpdateAsync(user); await userManager.UpdateAsync(user.Id, update);
userEvents.OnConsentGiven(user); userEvents.OnConsentGiven(user);
@ -232,36 +239,37 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
var isLoggedIn = result.Succeeded; var isLoggedIn = result.Succeeded;
IUser user = null; UserWithClaims user = null;
if (!isLoggedIn) if (!isLoggedIn)
{ {
var email = externalLogin.Principal.FindFirst(ClaimTypes.Email).Value; var email = externalLogin.Principal.FindFirst(ClaimTypes.Email).Value;
user = await userManager.FindByEmailAsync(email); user = await userManager.FindByEmailWithClaimsAsyncAsync(email);
if (user != null) if (user != null)
{ {
isLoggedIn = isLoggedIn =
await AddLoginAsync(user, externalLogin) && await AddLoginAsync(user, externalLogin) &&
await AddClaimsAsync(user, externalLogin, email) &&
await LoginAsync(externalLogin); await LoginAsync(externalLogin);
} }
else else
{ {
user = CreateUser(externalLogin, email); user = new UserWithClaims(userFactory.Create(email), new List<Claim>());
var isFirst = userManager.Users.LongCount() == 0; var isFirst = userManager.Users.LongCount() == 0;
isLoggedIn = isLoggedIn =
await AddUserAsync(user) && await AddUserAsync(user) &&
await AddLoginAsync(user, externalLogin) && await AddLoginAsync(user, externalLogin) &&
await MakeAdminAsync(user, isFirst) && await AddClaimsAsync(user, externalLogin, email, isFirst) &&
await LockAsync(user, isFirst) && await LockAsync(user, isFirst) &&
await LoginAsync(externalLogin); await LoginAsync(externalLogin);
userEvents.OnUserRegistered(user); userEvents.OnUserRegistered(user);
if (user.IsLocked) if (await userManager.IsLockedOutAsync(user.Identity))
{ {
return View(nameof(LockedOut)); return View(nameof(LockedOut));
} }
@ -282,14 +290,14 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
} }
} }
private Task<bool> AddLoginAsync(IUser user, UserLoginInfo externalLogin) private Task<bool> AddLoginAsync(UserWithClaims user, UserLoginInfo externalLogin)
{ {
return MakeIdentityOperation(() => userManager.AddLoginAsync(user, externalLogin)); return MakeIdentityOperation(() => userManager.AddLoginAsync(user.Identity, externalLogin));
} }
private Task<bool> AddUserAsync(IUser user) private Task<bool> AddUserAsync(UserWithClaims user)
{ {
return MakeIdentityOperation(() => userManager.CreateAsync(user)); return MakeIdentityOperation(() => userManager.CreateAsync(user.Identity));
} }
private async Task<bool> LoginAsync(UserLoginInfo externalLogin) private async Task<bool> LoginAsync(UserLoginInfo externalLogin)
@ -299,46 +307,48 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
return result.Succeeded; return result.Succeeded;
} }
private Task<bool> LockAsync(IUser user, bool isFirst) private Task<bool> LockAsync(UserWithClaims user, bool isFirst)
{ {
if (isFirst || !identityOptions.Value.LockAutomatically) if (isFirst || !identityOptions.Value.LockAutomatically)
{ {
return TaskHelper.True; return TaskHelper.True;
} }
return MakeIdentityOperation(() => userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddYears(100))); return MakeIdentityOperation(() => userManager.SetLockoutEndDateAsync(user.Identity, DateTimeOffset.UtcNow.AddYears(100)));
} }
private Task<bool> MakeAdminAsync(IUser user, bool isFirst) private Task<bool> AddClaimsAsync(UserWithClaims user, ExternalLoginInfo externalLogin, string email, bool isFirst = false)
{ {
if (!isFirst) var newClaims = new List<Claim>();
{
return TaskHelper.True;
}
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) user.Claims.Add(claim);
{ }
var user = userFactory.Create(email);
foreach (var squidexClaim in externalLogin.Principal.GetSquidexClaims()) foreach (var squidexClaim in externalLogin.Principal.GetSquidexClaims())
{ {
user.AddClaim(squidexClaim); AddClaim(squidexClaim);
} }
if (!user.HasPictureUrl()) if (!user.HasPictureUrl())
{ {
user.SetPictureUrl(GravatarHelper.CreatePictureUrl(email)); AddClaim(new Claim(SquidexClaimTypes.PictureUrl, GravatarHelper.CreatePictureUrl(email)));
} }
if (!user.HasDisplayName()) 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) private IActionResult RedirectToLogoutUrl(LogoutRequest context)

5
src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs

@ -11,13 +11,12 @@ using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Squidex.Shared.Users;
namespace Squidex.Areas.IdentityServer.Controllers namespace Squidex.Areas.IdentityServer.Controllers
{ {
public static class Extensions public static class Extensions
{ {
public static async Task<ExternalLoginInfo> GetExternalLoginInfoWithDisplayNameAsync(this SignInManager<IUser> signInManager, string expectedXsrf = null) public static async Task<ExternalLoginInfo> GetExternalLoginInfoWithDisplayNameAsync(this SignInManager<IdentityUser> signInManager, string expectedXsrf = null)
{ {
var externalLogin = await signInManager.GetExternalLoginInfoAsync(expectedXsrf); var externalLogin = await signInManager.GetExternalLoginInfoAsync(expectedXsrf);
@ -26,7 +25,7 @@ namespace Squidex.Areas.IdentityServer.Controllers
return externalLogin; return externalLogin;
} }
public static async Task<List<ExternalProvider>> GetExternalProvidersAsync(this SignInManager<IUser> signInManager) public static async Task<List<ExternalProvider>> GetExternalProvidersAsync(this SignInManager<IdentityUser> signInManager)
{ {
var externalSchemes = await signInManager.GetExternalAuthenticationSchemesAsync(); var externalSchemes = await signInManager.GetExternalAuthenticationSchemesAsync();
var externalProviders = var externalProviders =

6
src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Users;
namespace Squidex.Areas.IdentityServer.Controllers.Profile namespace Squidex.Areas.IdentityServer.Controllers.Profile
{ {
@ -19,5 +20,10 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
public string DisplayName { get; set; } public string DisplayName { get; set; }
public bool IsHidden { get; set; } public bool IsHidden { get; set; }
public UserValues ToValues()
{
return new UserValues { Email = Email, DisplayName = DisplayName, Hidden = IsHidden };
}
} }
} }

49
src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs

@ -20,6 +20,7 @@ using Squidex.Config;
using Squidex.Domain.Users; using Squidex.Domain.Users;
using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Shared.Identity;
using Squidex.Shared.Users; using Squidex.Shared.Users;
namespace Squidex.Areas.IdentityServer.Controllers.Profile namespace Squidex.Areas.IdentityServer.Controllers.Profile
@ -27,15 +28,15 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Authorize] [Authorize]
public sealed class ProfileController : IdentityServerController public sealed class ProfileController : IdentityServerController
{ {
private readonly SignInManager<IUser> signInManager; private readonly SignInManager<IdentityUser> signInManager;
private readonly UserManager<IUser> userManager; private readonly UserManager<IdentityUser> userManager;
private readonly IUserPictureStore userPictureStore; private readonly IUserPictureStore userPictureStore;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator; private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
private readonly IOptions<MyIdentityOptions> identityOptions; private readonly IOptions<MyIdentityOptions> identityOptions;
public ProfileController( public ProfileController(
SignInManager<IUser> signInManager, SignInManager<IdentityUser> signInManager,
UserManager<IUser> userManager, UserManager<IdentityUser> userManager,
IUserPictureStore userPictureStore, IUserPictureStore userPictureStore,
IAssetThumbnailGenerator assetThumbnailGenerator, IAssetThumbnailGenerator assetThumbnailGenerator,
IOptions<MyIdentityOptions> identityOptions) IOptions<MyIdentityOptions> identityOptions)
@ -51,7 +52,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/")] [Route("/account/profile/")]
public async Task<IActionResult> Profile(string successMessage = null) public async Task<IActionResult> Profile(string successMessage = null)
{ {
var user = await userManager.GetUserAsync(User); var user = await userManager.GetUserWithClaimsAsync(User);
return View(await GetProfileVM(user, successMessage: successMessage)); return View(await GetProfileVM(user, successMessage: successMessage));
} }
@ -81,7 +82,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/update/")] [Route("/account/profile/update/")]
public Task<IActionResult> UpdateProfile(ChangeProfileModel model) public Task<IActionResult> 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."); "Account updated successfully.");
} }
@ -89,7 +90,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/login-remove/")] [Route("/account/profile/login-remove/")]
public Task<IActionResult> RemoveLogin(RemoveLoginModel model) public Task<IActionResult> 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."); "Login provider removed successfully.");
} }
@ -97,7 +98,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/password-set/")] [Route("/account/profile/password-set/")]
public Task<IActionResult> SetPassword(SetPasswordModel model) public Task<IActionResult> SetPassword(SetPasswordModel model)
{ {
return MakeChangeAsync(user => userManager.AddPasswordAsync(user, model.Password), return MakeChangeAsync(user => userManager.AddPasswordAsync(user.Identity, model.Password),
"Password set successfully."); "Password set successfully.");
} }
@ -105,7 +106,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/password-change/")] [Route("/account/profile/password-change/")]
public Task<IActionResult> ChangePassword(ChangePasswordModel model) public Task<IActionResult> 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."); "Password changed successfully.");
} }
@ -117,14 +118,14 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
"Picture uploaded successfully."); "Picture uploaded successfully.");
} }
private async Task<IdentityResult> AddLoginAsync(IUser user) private async Task<IdentityResult> AddLoginAsync(UserWithClaims user)
{ {
var externalLogin = await signInManager.GetExternalLoginInfoWithDisplayNameAsync(userManager.GetUserId(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<IdentityResult> UpdatePictureAsync(List<IFormFile> file, IUser user) private async Task<IdentityResult> UpdatePictureAsync(List<IFormFile> file, UserWithClaims user)
{ {
if (file.Count != 1) if (file.Count != 1)
{ {
@ -145,14 +146,12 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
await userPictureStore.UploadAsync(user.Id, thumbnailStream); await userPictureStore.UploadAsync(user.Id, thumbnailStream);
user.SetPictureUrlToStore(); return await userManager.UpdateSafeAsync(user.Identity, new UserValues { PictureUrl = SquidexClaimTypes.PictureUrlStore });
return await userManager.UpdateAsync(user);
} }
private async Task<IActionResult> MakeChangeAsync(Func<IUser, Task<IdentityResult>> action, string successMessage, ChangeProfileModel model = null) private async Task<IActionResult> MakeChangeAsync(Func<UserWithClaims, Task<IdentityResult>> action, string successMessage, ChangeProfileModel model = null)
{ {
var user = await userManager.GetUserAsync(User); var user = await userManager.GetUserWithClaimsAsync(User);
if (!ModelState.IsValid) if (!ModelState.IsValid)
{ {
@ -166,7 +165,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
if (result.Succeeded) if (result.Succeeded)
{ {
await signInManager.SignInAsync(user, true); await signInManager.SignInAsync(user.Identity, true);
return RedirectToAction(nameof(Profile), new { successMessage }); 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)); return View(nameof(Profile), await GetProfileVM(user, model, errorMessage));
} }
private async Task<ProfileVM> GetProfileVM(IUser user, ChangeProfileModel model = null, string errorMessage = null, string successMessage = null) private async Task<ProfileVM> 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 var result = new ProfileVM
{ {
Id = user.Id, Id = user.Id,
Email = user.Email, Email = user.Email,
ErrorMessage = errorMessage, ErrorMessage = errorMessage,
ExternalLogins = user.Logins, ExternalLogins = taskForLogins.Result,
ExternalProviders = externalProviders, ExternalProviders = taskForProviders.Result,
DisplayName = user.DisplayName(), DisplayName = user.DisplayName(),
IsHidden = user.IsHidden(), IsHidden = user.IsHidden(),
HasPassword = await userManager.HasPasswordAsync(user), HasPassword = taskForPassword.Result,
HasPasswordAuth = identityOptions.Value.AllowPasswordAuth, HasPasswordAuth = identityOptions.Value.AllowPasswordAuth,
SuccessMessage = successMessage SuccessMessage = successMessage
}; };

6
src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs

@ -6,7 +6,7 @@
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using System.Collections.Generic;
using Squidex.Shared.Users; using Microsoft.AspNetCore.Identity;
namespace Squidex.Areas.IdentityServer.Controllers.Profile namespace Squidex.Areas.IdentityServer.Controllers.Profile
{ {
@ -28,8 +28,8 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
public bool HasPasswordAuth { get; set; } public bool HasPasswordAuth { get; set; }
public IReadOnlyList<ExternalLogin> ExternalLogins { get; set; } public IList<UserLoginInfo> ExternalLogins { get; set; }
public IReadOnlyList<ExternalProvider> ExternalProviders { get; set; } public IList<ExternalProvider> ExternalProviders { get; set; }
} }
} }

2
src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml

@ -28,7 +28,7 @@
<input type="checkbox" id="consentToAutomatedEmails" name="consentToAutomatedEmails" value="True" /> <input type="checkbox" id="consentToAutomatedEmails" name="consentToAutomatedEmails" value="True" />
</div> </div>
<div class="col"> <div class="col">
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.
</div> </div>
</div> </div>
</div> </div>

2
src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml

@ -121,7 +121,7 @@
{ {
var schema = provider.AuthenticationScheme.ToLowerInvariant(); var schema = provider.AuthenticationScheme.ToLowerInvariant();
<button class="btn external-button-small btn btn-@schema" type="submit" name="provider" value="@provider.AuthenticationScheme"> <button class="btn external-button-small btn-@schema" type="submit" name="provider" value="@provider.AuthenticationScheme">
<i class="icon-@schema external-icon"></i> <i class="icon-@schema external-icon"></i>
</button> </button>
} }

8
src/Squidex/Config/Domain/InfrastructureServices.cs

@ -11,10 +11,12 @@ using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using NodaTime; using NodaTime;
using Squidex.Domain.Users;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Diagnostics; using Squidex.Infrastructure.Diagnostics;
using Squidex.Infrastructure.UsageTracking; using Squidex.Infrastructure.UsageTracking;
using Squidex.Shared.Users;
#pragma warning disable RECS0092 // Convert field to readonly #pragma warning disable RECS0092 // Convert field to readonly
@ -48,6 +50,12 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<ActionContextAccessor>() services.AddSingletonAs<ActionContextAccessor>()
.As<IActionContextAccessor>(); .As<IActionContextAccessor>();
services.AddSingletonAs<DefaultUserResolver>()
.As<IUserResolver>();
services.AddSingletonAs<AssetUserPictureStore>()
.As<IUserPictureStore>();
services.AddTransient(typeof(Lazy<>), typeof(Lazier<>)); services.AddTransient(typeof(Lazy<>), typeof(Lazier<>));
} }
} }

6
src/Squidex/Config/Domain/StoreServices.cs

@ -85,14 +85,12 @@ namespace Squidex.Config.Domain
.As<IInitializable>(); .As<IInitializable>();
services.AddSingletonAs<MongoUserStore>() services.AddSingletonAs<MongoUserStore>()
.As<IUserStore<IUser>>() .As<IUserStore<IdentityUser>>()
.As<IUserFactory>() .As<IUserFactory>()
.As<IUserResolver>()
.As<IInitializable>(); .As<IInitializable>();
services.AddSingletonAs<MongoRoleStore>() services.AddSingletonAs<MongoRoleStore>()
.As<IRoleStore<IRole>>() .As<IRoleStore<IdentityRole>>()
.As<IRoleFactory>()
.As<IInitializable>(); .As<IInitializable>();
services.AddSingletonAs<MongoHistoryEventRepository>() services.AddSingletonAs<MongoHistoryEventRepository>()

3
src/Squidex/Config/Domain/SubscriptionServices.cs

@ -27,9 +27,6 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<NoopAppPlanBillingManager>() services.AddSingletonAs<NoopAppPlanBillingManager>()
.As<IAppPlanBillingManager>(); .As<IAppPlanBillingManager>();
services.AddSingletonAs<AssetUserPictureStore>()
.As<IUserPictureStore>();
services.AddSingletonAs<NoopUserEvents>() services.AddSingletonAs<NoopUserEvents>()
.As<IUserEvents>(); .As<IUserEvents>();
} }

3
src/Squidex/WebStartup.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Ben.Diagnostics;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -42,8 +41,6 @@ namespace Squidex
{ {
app.ApplicationServices.LogConfiguration(); app.ApplicationServices.LogConfiguration();
app.UseBlockingDetection();
app.UseMyHealthCheck(); app.UseMyHealthCheck();
app.UseMyRobotsTxt(); app.UseMyRobotsTxt();
app.UseMyTracking(); app.UseMyTracking();

2
src/Squidex/app/theme/_static.scss

@ -109,7 +109,7 @@ noscript {
&-icon { &-icon {
display: inline-block; display: inline-block;
font-size: 1.25rem; font-size: 1.5rem;
font-weight: normal; font-weight: normal;
vertical-align: middle; vertical-align: middle;
width: 1.6rem; width: 1.6rem;

Loading…
Cancel
Save