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 IdentityServer4.Models;
using IdentityServer4.Stores;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;
using MongoDB.Driver;
using Squidex.Infrastructure.MongoDb;
@ -23,7 +25,8 @@ namespace Squidex.Domain.Users.MongoDb.Infrastructure
BsonClassMap.RegisterClassMap<PersistedGrant>(map =>
{
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.Tasks;
using Microsoft.AspNetCore.Identity;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;
using MongoDB.Driver;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Users.MongoDb
{
public sealed class MongoRoleStore : MongoRepositoryBase<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)
: base(database)
{
@ -26,10 +40,10 @@ namespace Squidex.Domain.Users.MongoDb
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(
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()
@ -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);
}
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);
}
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;
}
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;
}
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;
}
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;
}
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;
}
}
}
}

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

@ -5,196 +5,100 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using Microsoft.AspNetCore.Identity;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using Squidex.Infrastructure;
using Squidex.Shared.Users;
namespace Squidex.Domain.Users.MongoDb
{
public sealed class MongoUser : IUser
public sealed class MongoUser : IdentityUser
{
[BsonRepresentation(BsonType.ObjectId)]
[BsonElement]
public string Id { get; set; }
public List<Claim> Claims { get; set; } = new List<Claim>();
[BsonIgnoreIfNull]
[BsonElement]
public string SecurityStamp { get; set; }
public List<UserTokenInfo> Tokens { get; set; } = new List<UserTokenInfo>();
[BsonRequired]
[BsonElement]
public string UserName { get; set; }
public List<UserLoginInfo> Logins { get; set; } = new List<UserLoginInfo>();
[BsonRequired]
[BsonElement]
public string NormalizedUserName { get; set; }
public HashSet<string> Roles { get; set; } = new HashSet<string>();
[BsonRequired]
[BsonElement]
public string Email { get; set; }
[BsonRequired]
[BsonElement]
public string NormalizedEmail { get; set; }
[BsonIgnoreIfNull]
[BsonElement]
public string PhoneNumber { get; set; }
[BsonIgnoreIfNull]
[BsonElement]
public string PasswordHash { get; set; }
[BsonRequired]
[BsonElement]
public bool EmailConfirmed { get; set; }
[BsonIgnoreIfDefault]
[BsonElement]
public bool PhoneNumberConfirmed { get; set; }
[BsonIgnoreIfDefault]
[BsonElement]
public bool TwoFactorEnabled { get; set; }
[BsonIgnoreIfDefault]
[BsonElement]
public bool LockoutEnabled { get; set; }
[BsonIgnoreIfNull]
[BsonElement]
public DateTime? LockoutEndDateUtc { get; set; }
[BsonIgnoreIfDefault]
[BsonElement]
public int AccessFailedCount { get; set; }
[BsonRequired]
[BsonElement]
public List<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()
internal IdentityUserToken<string> FindTokenAsync(string loginProvider, string name)
{
Id = ObjectId.GenerateNewId().ToString();
return Tokens.FirstOrDefault(x => x.LoginProvider == loginProvider && x.Name == name);
}
public void SetEmail(string email)
internal void AddLogin(UserLoginInfo login)
{
Email = UserName = email;
Logins.Add(new UserLoginInfo(login.LoginProvider, login.ProviderKey, login.ProviderDisplayName));
}
public void AddRole(string role)
internal void AddRole(string role)
{
Roles.Add(role);
}
public void RemoveRole(string role)
internal void RemoveRole(string role)
{
Roles.Remove(role);
}
public void AddLogin(UserLoginInfo login)
{
Logins.Add(login);
}
public void RemoveLogin(string loginProvider, string providerKey)
internal void RemoveLogin(string loginProvider, string providerKey)
{
Logins.RemoveAll(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey);
}
public void RemoveClaims(string type)
{
Claims.RemoveAll(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase));
}
public void AddClaim(Claim claim)
internal void AddClaim(Claim claim)
{
Claims.Add(claim);
}
public void AddClaims(IEnumerable<Claim> claims)
internal void AddClaims(IEnumerable<Claim> claims)
{
claims.Foreach(AddClaim);
}
public void RemoveClaim(Claim claim)
internal void RemoveClaim(Claim claim)
{
Claims.RemoveAll(c => c.Type == claim.Type && c.Value == claim.Value);
}
public void RemoveClaims(IEnumerable<Claim> claims)
internal void RemoveClaims(IEnumerable<Claim> claims)
{
claims.Foreach(RemoveClaim);
}
public string GetToken(string loginProider, string name)
internal string GetToken(string loginProvider, string name)
{
return Tokens.FirstOrDefault(t => t.LoginProvider == loginProider && t.Name == name)?.Value;
return Tokens.FirstOrDefault(t => t.LoginProvider == loginProvider && t.Name == name)?.Value;
}
public void AddToken(string loginProvider, string name, string value)
internal void AddToken(string loginProvider, string name, string value)
{
Tokens.Add(new MongoUserToken { LoginProvider = loginProvider, Name = name, Value = value });
Tokens.Add(new UserTokenInfo { LoginProvider = loginProvider, Name = name, Value = value });
}
public void RemoveToken(string loginProvider, string name)
internal void RemoveToken(string loginProvider, string name)
{
Tokens.RemoveAll(t => t.LoginProvider == loginProvider && t.Name == name);
}
public void SetClaim(string type, string value)
{
RemoveClaims(type);
AddClaim(new Claim(type, value));
}
public void ReplaceClaim(Claim existingClaim, Claim newClaim)
internal void ReplaceClaim(Claim existingClaim, Claim newClaim)
{
RemoveClaim(existingClaim);
AddClaim(newClaim);
}
public void SetToken(string loginProider, string name, string value)
internal void SetToken(string loginProider, string name, string value)
{
RemoveToken(loginProider, name);
AddToken(loginProider, name, value);
}
}
public sealed class UserTokenInfo : IdentityUserToken<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 Microsoft.AspNetCore.Identity;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;
using MongoDB.Driver;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Tasks;
using Squidex.Shared.Users;
namespace Squidex.Domain.Users.MongoDb
{
public sealed class MongoUserStore :
MongoRepositoryBase<MongoUser>,
IUserPasswordStore<IUser>,
IUserRoleStore<IUser>,
IUserLoginStore<IUser>,
IUserSecurityStampStore<IUser>,
IUserEmailStore<IUser>,
IUserClaimStore<IUser>,
IUserPhoneNumberStore<IUser>,
IUserTwoFactorStore<IUser>,
IUserLockoutStore<IUser>,
IUserAuthenticationTokenStore<IUser>,
IUserAuthenticationTokenStore<IdentityUser>,
IUserAuthenticatorKeyStore<IdentityUser>,
IUserClaimStore<IdentityUser>,
IUserEmailStore<IdentityUser>,
IUserFactory,
IUserResolver,
IQueryableUserStore<IUser>
IUserLockoutStore<IdentityUser>,
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)
: base(database)
{
@ -66,352 +133,374 @@ namespace Squidex.Domain.Users.MongoDb
{
}
public IQueryable<IUser> Users
public IQueryable<IdentityUser> Users
{
get { return Collection.AsQueryable(); }
}
public IUser Create(string email)
public bool IsId(string id)
{
return ObjectId.TryParse(id, out var _);
}
public IdentityUser Create(string email)
{
return new MongoUser { Email = email, UserName = email };
}
public async Task<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);
}
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);
}
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);
}
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);
}
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);
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);
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);
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);
}
public Task<string> GetUserNameAsync(IUser user, CancellationToken cancellationToken)
public Task<string> GetUserNameAsync(IdentityUser user, CancellationToken cancellationToken)
{
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);
}
public Task<string> GetPasswordHashAsync(IUser user, CancellationToken cancellationToken)
public Task<string> GetPasswordHashAsync(IdentityUser user, CancellationToken cancellationToken)
{
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));
}
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);
}
public Task<string> GetEmailAsync(IUser user, CancellationToken cancellationToken)
public Task<string> GetEmailAsync(IdentityUser user, CancellationToken cancellationToken)
{
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);
}
public Task<string> GetNormalizedEmailAsync(IUser user, CancellationToken cancellationToken)
public Task<string> GetNormalizedEmailAsync(IdentityUser user, CancellationToken cancellationToken)
{
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);
}
public Task<bool> GetPhoneNumberConfirmedAsync(IUser user, CancellationToken cancellationToken)
public Task<bool> GetPhoneNumberConfirmedAsync(IdentityUser user, CancellationToken cancellationToken)
{
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);
}
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);
}
public Task<bool> GetLockoutEnabledAsync(IUser user, CancellationToken cancellationToken)
public Task<bool> GetLockoutEnabledAsync(IdentityUser user, CancellationToken cancellationToken)
{
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));
}
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));
}
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;
return TaskHelper.Done;
}
public Task SetNormalizedUserNameAsync(IUser user, string normalizedName, CancellationToken cancellationToken)
public Task SetNormalizedUserNameAsync(IdentityUser user, string normalizedName, CancellationToken cancellationToken)
{
((MongoUser)user).NormalizedUserName = normalizedName;
return TaskHelper.Done;
}
public Task SetPasswordHashAsync(IUser user, string passwordHash, CancellationToken cancellationToken)
public Task SetPasswordHashAsync(IdentityUser user, string passwordHash, CancellationToken cancellationToken)
{
((MongoUser)user).PasswordHash = passwordHash;
return TaskHelper.Done;
}
public Task AddToRoleAsync(IUser user, string roleName, CancellationToken cancellationToken)
public Task AddToRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken)
{
((MongoUser)user).AddRole(roleName);
return TaskHelper.Done;
}
public Task RemoveFromRoleAsync(IUser user, string roleName, CancellationToken cancellationToken)
public Task RemoveFromRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken)
{
((MongoUser)user).RemoveRole(roleName);
return TaskHelper.Done;
}
public Task AddLoginAsync(IUser user, UserLoginInfo login, CancellationToken cancellationToken)
public Task AddLoginAsync(IdentityUser user, UserLoginInfo login, CancellationToken cancellationToken)
{
((MongoUser)user).AddLogin(login);
return TaskHelper.Done;
}
public Task RemoveLoginAsync(IUser user, string loginProvider, string providerKey, CancellationToken cancellationToken)
public Task RemoveLoginAsync(IdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken)
{
((MongoUser)user).RemoveLogin(loginProvider, providerKey);
return TaskHelper.Done;
}
public Task SetSecurityStampAsync(IUser user, string stamp, CancellationToken cancellationToken)
public Task SetSecurityStampAsync(IdentityUser user, string stamp, CancellationToken cancellationToken)
{
((MongoUser)user).SecurityStamp = stamp;
return TaskHelper.Done;
}
public Task SetEmailAsync(IUser user, string email, CancellationToken cancellationToken)
public Task SetEmailAsync(IdentityUser user, string email, CancellationToken cancellationToken)
{
((MongoUser)user).Email = email;
return TaskHelper.Done;
}
public Task SetEmailConfirmedAsync(IUser user, bool confirmed, CancellationToken cancellationToken)
public Task SetEmailConfirmedAsync(IdentityUser user, bool confirmed, CancellationToken cancellationToken)
{
((MongoUser)user).EmailConfirmed = confirmed;
return TaskHelper.Done;
}
public Task SetNormalizedEmailAsync(IUser user, string normalizedEmail, CancellationToken cancellationToken)
public Task SetNormalizedEmailAsync(IdentityUser user, string normalizedEmail, CancellationToken cancellationToken)
{
((MongoUser)user).NormalizedEmail = normalizedEmail;
return TaskHelper.Done;
}
public Task AddClaimsAsync(IUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken)
public Task AddClaimsAsync(IdentityUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken)
{
((MongoUser)user).AddClaims(claims);
return TaskHelper.Done;
}
public Task ReplaceClaimAsync(IUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken)
public Task ReplaceClaimAsync(IdentityUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken)
{
((MongoUser)user).ReplaceClaim(claim, newClaim);
return TaskHelper.Done;
}
public Task RemoveClaimsAsync(IUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken)
public Task RemoveClaimsAsync(IdentityUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken)
{
((MongoUser)user).RemoveClaims(claims);
return TaskHelper.Done;
}
public Task SetPhoneNumberAsync(IUser user, string phoneNumber, CancellationToken cancellationToken)
public Task SetPhoneNumberAsync(IdentityUser user, string phoneNumber, CancellationToken cancellationToken)
{
((MongoUser)user).PhoneNumber = phoneNumber;
return TaskHelper.Done;
}
public Task SetPhoneNumberConfirmedAsync(IUser user, bool confirmed, CancellationToken cancellationToken)
public Task SetPhoneNumberConfirmedAsync(IdentityUser user, bool confirmed, CancellationToken cancellationToken)
{
((MongoUser)user).PhoneNumberConfirmed = confirmed;
return TaskHelper.Done;
}
public Task SetTwoFactorEnabledAsync(IUser user, bool enabled, CancellationToken cancellationToken)
public Task SetTwoFactorEnabledAsync(IdentityUser user, bool enabled, CancellationToken cancellationToken)
{
((MongoUser)user).TwoFactorEnabled = enabled;
return TaskHelper.Done;
}
public Task SetLockoutEndDateAsync(IUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken)
public Task SetLockoutEndDateAsync(IdentityUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken)
{
((MongoUser)user).LockoutEndDateUtc = lockoutEnd?.UtcDateTime;
((MongoUser)user).LockoutEnd = lockoutEnd?.UtcDateTime;
return TaskHelper.Done;
}
public Task<int> IncrementAccessFailedCountAsync(IUser user, CancellationToken cancellationToken)
public Task<int> IncrementAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken)
{
((MongoUser)user).AccessFailedCount++;
return Task.FromResult(((MongoUser)user).AccessFailedCount);
}
public Task ResetAccessFailedCountAsync(IUser user, CancellationToken cancellationToken)
public Task ResetAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken)
{
((MongoUser)user).AccessFailedCount = 0;
return TaskHelper.Done;
}
public Task SetLockoutEnabledAsync(IUser user, bool enabled, CancellationToken cancellationToken)
public Task SetLockoutEnabledAsync(IdentityUser user, bool enabled, CancellationToken cancellationToken)
{
((MongoUser)user).LockoutEnabled = enabled;
return TaskHelper.Done;
}
public Task SetTokenAsync(IUser user, string loginProvider, string name, string value, CancellationToken cancellationToken)
public Task SetTokenAsync(IdentityUser user, string loginProvider, string name, string value, CancellationToken cancellationToken)
{
((MongoUser)user).SetToken(loginProvider, name, value);
return TaskHelper.Done;
}
public Task RemoveTokenAsync(IUser user, string loginProvider, string name, CancellationToken cancellationToken)
public Task RemoveTokenAsync(IdentityUser user, string loginProvider, string name, CancellationToken cancellationToken)
{
((MongoUser)user).RemoveToken(loginProvider, name);
return TaskHelper.Done;
}
public async Task<IUser> FindByIdOrEmailAsync(string id)
public Task SetAuthenticatorKeyAsync(IdentityUser user, string key, CancellationToken cancellationToken)
{
if (ObjectId.TryParse(id, out _))
{
return await Collection.Find(x => x.Id == id).FirstOrDefaultAsync();
}
else
{
return await Collection.Find(x => x.NormalizedEmail == id.ToUpperInvariant()).FirstOrDefaultAsync();
}
((MongoUser)user).SetToken(InternalLoginProvider, AuthenticatorKeyTokenName, key);
return TaskHelper.Done;
}
public Task ReplaceCodesAsync(IdentityUser user, IEnumerable<string> recoveryCodes, CancellationToken cancellationToken)
{
((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.
// ==========================================================================
using Squidex.Shared.Users;
using Microsoft.AspNetCore.Identity;
namespace Squidex.Domain.Users
{
public interface IUserFactory
{
IUser Create(string email);
IdentityUser Create(string email);
bool IsId(string id);
}
}

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

@ -11,11 +11,10 @@ using Microsoft.AspNetCore.Identity;
using SharpPwned.NET;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
using Squidex.Shared.Users;
namespace Squidex.Domain.Users
{
public sealed class PwnedPasswordValidator : IPasswordValidator<IUser>
public sealed class PwnedPasswordValidator : IPasswordValidator<IdentityUser>
{
private const string ErrorCode = "PwnedError";
private const string ErrorText = "This password has previously appeared in a data breach and should never be used. If you've ever used it anywhere before, change it!";
@ -31,7 +30,7 @@ namespace Squidex.Domain.Users
this.log = log;
}
public async Task<IdentityResult> ValidateAsync(UserManager<IUser> manager, IUser user, string password)
public async Task<IdentityResult> ValidateAsync(UserManager<IdentityUser> manager, IdentityUser user, string password)
{
try
{

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

@ -12,6 +12,7 @@
</ItemGroup>
<ItemGroup>
<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="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<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.Extensions.Options;
using Squidex.Infrastructure.Security;
using Squidex.Shared.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)
{
}
public override async Task<ClaimsPrincipal> CreateAsync(IUser user)
public override async Task<ClaimsPrincipal> CreateAsync(IdentityUser user)
{
var principal = await base.CreateAsync(user);

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

@ -8,31 +8,88 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using Squidex.Shared.Users;
namespace Squidex.Domain.Users
{
public static class UserManagerExtensions
{
public static Task<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();
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;
@ -46,25 +103,24 @@ namespace Squidex.Domain.Users
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
{
user.SetDisplayName(displayName);
user.SetPictureUrlFromGravatar(email);
await DoChecked(() => userManager.CreateAsync(user), "Cannot create user.");
if (permissions != null)
var claims = values.ToClaims().ToList();
if (claims.Count > 0)
{
user.SetPermissions(permissions);
await DoChecked(() => userManager.AddClaimsAsync(user, claims), "Cannot add user.");
}
await DoChecked(() => userManager.CreateAsync(user), "Cannot create user.");
if (!string.IsNullOrWhiteSpace(password))
if (!string.IsNullOrWhiteSpace(values.Password))
{
await DoChecked(() => userManager.AddPasswordAsync(user, password), "Cannot create user.");
await DoChecked(() => userManager.AddPasswordAsync(user, values.Password), "Cannot create user.");
}
}
catch
@ -77,68 +133,73 @@ namespace Squidex.Domain.Users
return user;
}
public static Task<IdentityResult> UpdateAsync(this UserManager<IUser> userManager, IUser user, string email, string displayName, bool hidden)
{
user.SetHidden(hidden);
user.SetEmail(email);
user.SetDisplayName(displayName);
return userManager.UpdateAsync(user);
}
public static async Task UpdateAsync(this UserManager<IUser> userManager, string id, string email, string displayName, string password, PermissionSet permissions = null)
public static async Task UpdateAsync(this UserManager<IdentityUser> userManager, string id, UserValues values)
{
var user = await userManager.FindByIdAsync(id);
if (user == null)
{
throw new DomainObjectNotFoundException(id, typeof(IUser));
throw new DomainObjectNotFoundException(id, typeof(IdentityUser));
}
if (!string.IsNullOrWhiteSpace(email))
await UpdateAsync(userManager, user, values);
}
public static async Task<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.");
await DoChecked(() => userManager.SetUserNameAsync(user, email), "Cannot update email.");
return IdentityResult.Failed(ex.Errors.Select(x => new IdentityError { Description = x.Message }).ToArray());
}
}
if (!string.IsNullOrWhiteSpace(displayName))
public static async Task UpdateAsync(this UserManager<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.AddPasswordAsync(user, password), "Cannot update user.");
await DoChecked(() => userManager.RemovePasswordAsync(user), "Cannot replace password.");
await DoChecked(() => userManager.AddPasswordAsync(user, values.Password), "Cannot replace password.");
}
}
public static async Task LockAsync(this UserManager<IUser> userManager, string id)
public static async Task LockAsync(this UserManager<IdentityUser> userManager, string id)
{
var user = await userManager.FindByIdAsync(id);
if (user == null)
{
throw new DomainObjectNotFoundException(id, typeof(IUser));
throw new DomainObjectNotFoundException(id, typeof(IdentityUser));
}
await DoChecked(() => userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddYears(100)), "Cannot lock user.");
}
public static async Task UnlockAsync(this UserManager<IUser> userManager, string id)
public static async Task UnlockAsync(this UserManager<IdentityUser> userManager, string id)
{
var user = await userManager.FindByIdAsync(id);
if (user == null)
{
throw new DomainObjectNotFoundException(id, typeof(IUser));
throw new DomainObjectNotFoundException(id, typeof(IdentityUser));
}
await DoChecked(() => userManager.SetLockoutEndDateAsync(user, null), "Cannot unlock user.");
@ -153,5 +214,36 @@ namespace Squidex.Domain.Users
throw new ValidationException(message, result.Errors.Select(x => new ValidationError(x.Description)).ToArray());
}
}
public static async Task<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 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
{
bool IsLocked { get; }
string Id { get; }
string Email { get; }
string NormalizedEmail { 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.Linq;
using System.Security.Claims;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using Squidex.Shared.Identity;
@ -16,49 +14,9 @@ namespace Squidex.Shared.Users
{
public static class UserExtensions
{
public static void SetDisplayName(this IUser user, string displayName)
{
user.SetClaim(SquidexClaimTypes.DisplayName, displayName);
}
public static void SetPictureUrl(this IUser user, string pictureUrl)
{
user.SetClaim(SquidexClaimTypes.PictureUrl, pictureUrl);
}
public static void SetPictureUrlToStore(this IUser user)
{
user.SetClaim(SquidexClaimTypes.PictureUrl, "store");
}
public static void SetPictureUrlFromGravatar(this IUser user, string email)
{
user.SetClaim(SquidexClaimTypes.PictureUrl, GravatarHelper.CreatePictureUrl(email));
}
public static void SetHidden(this IUser user, bool value)
{
user.SetClaim(SquidexClaimTypes.Hidden, value.ToString());
}
public static void SetConsent(this IUser user)
{
user.SetClaim(SquidexClaimTypes.Consent, "true");
}
public static void SetConsentForEmails(this IUser user, bool value)
{
user.SetClaim(SquidexClaimTypes.ConsentForEmails, value.ToString());
}
public static void SetPermissions(this IUser user, PermissionSet permissions)
public static PermissionSet Permissions(this IUser user)
{
user.RemoveClaims(SquidexClaimTypes.Permissions);
foreach (var permission in permissions)
{
user.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission.Id));
}
return new PermissionSet(user.GetClaimValues(SquidexClaimTypes.Permissions).Select(x => new Permission(x)));
}
public static bool IsHidden(this IUser user)
@ -88,7 +46,7 @@ namespace Squidex.Shared.Users
public static bool IsPictureUrlStored(this IUser user)
{
return user.HasClaimValue(SquidexClaimTypes.PictureUrl, "store");
return user.HasClaimValue(SquidexClaimTypes.PictureUrl, SquidexClaimTypes.PictureUrlStore);
}
public static string PictureUrl(this IUser user)
@ -101,11 +59,6 @@ namespace Squidex.Shared.Users
return user.GetClaimValue(SquidexClaimTypes.DisplayName);
}
public static PermissionSet Permissions(this IUser user)
{
return new PermissionSet(user.GetClaimValues(SquidexClaimTypes.Permissions).Select(x => new Permission(x)));
}
public static string GetClaimValue(this IUser user, string type)
{
return user.Claims.FirstOrDefault(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase))?.Value;

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

@ -6,6 +6,8 @@
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Users;
using Squidex.Infrastructure.Security;
namespace Squidex.Areas.Api.Controllers.Users.Models
{
@ -35,5 +37,10 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
/// </summary>
[Required]
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 Squidex.Domain.Users;
using Squidex.Infrastructure.Security;
namespace Squidex.Areas.Api.Controllers.Users.Models
{
@ -34,5 +36,10 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
/// </summary>
[Required]
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.Pipeline;
using Squidex.Shared;
using Squidex.Shared.Users;
namespace Squidex.Areas.Api.Controllers.Users
{
[ApiModelValidation(true)]
public sealed class UserManagementController : ApiController
{
private readonly UserManager<IUser> userManager;
private readonly UserManager<IdentityUser> userManager;
private readonly IUserFactory userFactory;
public UserManagementController(ICommandBus commandBus, UserManager<IUser> userManager, IUserFactory userFactory)
public UserManagementController(ICommandBus commandBus, UserManager<IdentityUser> userManager, IUserFactory userFactory)
: base(commandBus)
{
this.userManager = userManager;
@ -58,7 +57,7 @@ namespace Squidex.Areas.Api.Controllers.Users
[ApiPermission(Permissions.AdminUsersRead)]
public async Task<IActionResult> GetUser(string id)
{
var entity = await userManager.FindByIdAsync(id);
var entity = await userManager.FindByIdWithClaimsAsync(id);
if (entity == null)
{
@ -75,7 +74,7 @@ namespace Squidex.Areas.Api.Controllers.Users
[ApiPermission(Permissions.AdminUsersCreate)]
public async Task<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 };
@ -87,7 +86,7 @@ namespace Squidex.Areas.Api.Controllers.Users
[ApiPermission(Permissions.AdminUsersUpdate)]
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();
}

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

@ -17,7 +17,6 @@ using Squidex.Domain.Users;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
using Squidex.Shared.Users;
namespace Squidex.Areas.IdentityServer.Config
{
@ -34,7 +33,7 @@ namespace Squidex.Areas.IdentityServer.Config
{
var options = services.GetService<IOptions<MyIdentityOptions>>().Value;
var userManager = services.GetService<UserManager<IUser>>();
var userManager = services.GetService<UserManager<IdentityUser>>();
var userFactory = services.GetService<IUserFactory>();
var log = services.GetService<ISemanticLog>();
@ -50,9 +49,15 @@ namespace Squidex.Areas.IdentityServer.Config
{
try
{
var permissions = new PermissionSet(Permissions.Admin);
var values = new UserValues
{
Email = adminEmail,
Password = adminPass,
Permissions = new PermissionSet(Permissions.Admin),
DisplayName = adminEmail
};
await userManager.CreateAsync(userFactory, adminEmail, adminEmail, adminPass, permissions);
await userManager.CreateAsync(userFactory, values);
}
catch (Exception ex)
{

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

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

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

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

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

@ -11,13 +11,12 @@ using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Identity;
using Squidex.Shared.Users;
namespace Squidex.Areas.IdentityServer.Controllers
{
public static class Extensions
{
public static async Task<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);
@ -26,7 +25,7 @@ namespace Squidex.Areas.IdentityServer.Controllers
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 externalProviders =

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

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

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

@ -20,6 +20,7 @@ using Squidex.Config;
using Squidex.Domain.Users;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared.Identity;
using Squidex.Shared.Users;
namespace Squidex.Areas.IdentityServer.Controllers.Profile
@ -27,15 +28,15 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Authorize]
public sealed class ProfileController : IdentityServerController
{
private readonly SignInManager<IUser> signInManager;
private readonly UserManager<IUser> userManager;
private readonly SignInManager<IdentityUser> signInManager;
private readonly UserManager<IdentityUser> userManager;
private readonly IUserPictureStore userPictureStore;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
private readonly IOptions<MyIdentityOptions> identityOptions;
public ProfileController(
SignInManager<IUser> signInManager,
UserManager<IUser> userManager,
SignInManager<IdentityUser> signInManager,
UserManager<IdentityUser> userManager,
IUserPictureStore userPictureStore,
IAssetThumbnailGenerator assetThumbnailGenerator,
IOptions<MyIdentityOptions> identityOptions)
@ -51,7 +52,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/")]
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));
}
@ -81,7 +82,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/update/")]
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.");
}
@ -89,7 +90,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/login-remove/")]
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.");
}
@ -97,7 +98,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/password-set/")]
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.");
}
@ -105,7 +106,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/password-change/")]
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.");
}
@ -117,14 +118,14 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
"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));
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)
{
@ -145,14 +146,12 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
await userPictureStore.UploadAsync(user.Id, thumbnailStream);
user.SetPictureUrlToStore();
return await userManager.UpdateAsync(user);
return await userManager.UpdateSafeAsync(user.Identity, new UserValues { PictureUrl = SquidexClaimTypes.PictureUrlStore });
}
private async Task<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)
{
@ -166,7 +165,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
if (result.Succeeded)
{
await signInManager.SignInAsync(user, true);
await signInManager.SignInAsync(user.Identity, true);
return RedirectToAction(nameof(Profile), new { successMessage });
}
@ -181,20 +180,24 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
return View(nameof(Profile), await GetProfileVM(user, model, errorMessage));
}
private async Task<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
{
Id = user.Id,
Email = user.Email,
ErrorMessage = errorMessage,
ExternalLogins = user.Logins,
ExternalProviders = externalProviders,
ExternalLogins = taskForLogins.Result,
ExternalProviders = taskForProviders.Result,
DisplayName = user.DisplayName(),
IsHidden = user.IsHidden(),
HasPassword = await userManager.HasPasswordAsync(user),
HasPassword = taskForPassword.Result,
HasPasswordAuth = identityOptions.Value.AllowPasswordAuth,
SuccessMessage = successMessage
};

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

@ -6,7 +6,7 @@
// ==========================================================================
using System.Collections.Generic;
using Squidex.Shared.Users;
using Microsoft.AspNetCore.Identity;
namespace Squidex.Areas.IdentityServer.Controllers.Profile
{
@ -28,8 +28,8 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
public bool HasPasswordAuth { get; set; }
public IReadOnlyList<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" />
</div>
<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>

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

@ -121,7 +121,7 @@
{
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>
</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.DependencyInjection;
using NodaTime;
using Squidex.Domain.Users;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Diagnostics;
using Squidex.Infrastructure.UsageTracking;
using Squidex.Shared.Users;
#pragma warning disable RECS0092 // Convert field to readonly
@ -48,6 +50,12 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<ActionContextAccessor>()
.As<IActionContextAccessor>();
services.AddSingletonAs<DefaultUserResolver>()
.As<IUserResolver>();
services.AddSingletonAs<AssetUserPictureStore>()
.As<IUserPictureStore>();
services.AddTransient(typeof(Lazy<>), typeof(Lazier<>));
}
}

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

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

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

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

3
src/Squidex/WebStartup.cs

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

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

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

Loading…
Cancel
Save