// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== 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.Log; using Squidex.Shared; using Squidex.Shared.Identity; using Squidex.Shared.Users; namespace Squidex.Domain.Users { public sealed class DefaultUserService : IUserService { private readonly UserManager userManager; private readonly IUserFactory userFactory; private readonly IEnumerable userEvents; private readonly ISemanticLog log; public DefaultUserService(UserManager userManager, IUserFactory userFactory, IEnumerable userEvents, ISemanticLog log) { Guard.NotNull(userManager, nameof(userManager)); Guard.NotNull(userFactory, nameof(userFactory)); Guard.NotNull(userEvents, nameof(userEvents)); Guard.NotNull(log, nameof(log)); this.userManager = userManager; this.userFactory = userFactory; this.userEvents = userEvents; this.log = log; } public async Task IsEmptyAsync() { var result = await QueryAsync(null, 1, 0); return result.Total == 0; } public string GetUserId(ClaimsPrincipal user) { Guard.NotNull(user, nameof(user)); return userManager.GetUserId(user); } public async Task> QueryAsync(IEnumerable ids) { Guard.NotNull(ids, nameof(ids)); ids = ids.Where(userFactory.IsId); if (!ids.Any()) { return ResultList.CreateFrom(0); } var users = userManager.Users.Where(x => ids.Contains(x.Id)).ToList(); var resolved = await ResolveAsync(users); return ResultList.Create(users.Count, resolved); } public async Task> QueryAsync(string? query, int take, int skip) { Guard.GreaterThan(take, 0, nameof(take)); Guard.GreaterEquals(skip, 0, nameof(skip)); IQueryable QueryUsers(string? email = null) { var result = userManager.Users; if (!string.IsNullOrWhiteSpace(email)) { var normalizedEmail = userManager.NormalizeEmail(email); result = result.Where(x => x.NormalizedEmail.Contains(normalizedEmail)); } return result; } var userItems = QueryUsers(query).Take(take).Skip(skip).ToList(); var userTotal = QueryUsers(query).LongCount(); var resolved = await ResolveAsync(userItems); return ResultList.Create(userTotal, resolved); } public Task> GetLoginsAsync(IUser user) { Guard.NotNull(user, nameof(user)); return userManager.GetLoginsAsync((IdentityUser)user.Identity); } public Task HasPasswordAsync(IUser user) { Guard.NotNull(user, nameof(user)); return userManager.HasPasswordAsync((IdentityUser)user.Identity); } public async Task FindByLoginAsync(string provider, string key) { Guard.NotNullOrEmpty(provider, nameof(provider)); var user = await userManager.FindByLoginAsync(provider, key); return await ResolveOptionalAsync(user); } public async Task FindByEmailAsync(string email) { Guard.NotNullOrEmpty(email, nameof(email)); var user = await userManager.FindByEmailAsync(email); return await ResolveOptionalAsync(user); } public async Task GetAsync(ClaimsPrincipal principal) { Guard.NotNull(principal, nameof(principal)); var user = await userManager.GetUserAsync(principal); return await ResolveOptionalAsync(user); } public async Task FindByIdAsync(string id) { if (!userFactory.IsId(id)) { return null; } var user = await userManager.FindByIdAsync(id); return await ResolveOptionalAsync(user); } public async Task CreateAsync(string email, UserValues? values = null, bool lockAutomatically = false) { Guard.NotNullOrEmpty(email, nameof(email)); var isFirst = !userManager.Users.Any(); var user = userFactory.Create(email); try { await userManager.CreateAsync(user).Throw(log); values ??= new UserValues(); if (string.IsNullOrWhiteSpace(values.DisplayName)) { values.DisplayName = email; } if (isFirst) { var permissions = values.Permissions?.ToIds().ToList() ?? new List(); permissions.Add(Permissions.Admin); values.Permissions = new PermissionSet(permissions); } await userManager.SyncClaims(user, values).Throw(log); if (!string.IsNullOrWhiteSpace(values.Password)) { await userManager.AddPasswordAsync(user, values.Password).Throw(log); } if (!isFirst && lockAutomatically) { await userManager.SetLockoutEndDateAsync(user, LockoutDate()).Throw(log); } } catch (Exception) { try { if (userFactory.IsId(user.Id)) { await userManager.DeleteAsync(user); } } catch (Exception ex2) { log.LogError(ex2, w => w .WriteProperty("action", "CleanupUser") .WriteProperty("status", "Failed")); } throw; } var resolved = await ResolveAsync(user); foreach (var @events in userEvents) { await @events.OnUserRegisteredAsync(resolved); } if (HasConsentGiven(values, null!)) { foreach (var @events in userEvents) { await @events.OnConsentGivenAsync(resolved); } } return resolved; } public Task SetPasswordAsync(string id, string password, string? oldPassword) { Guard.NotNullOrEmpty(id, nameof(id)); return ForUserAsync(id, async user => { if (await userManager.HasPasswordAsync(user)) { await userManager.ChangePasswordAsync(user, oldPassword!, password).Throw(log); } else { await userManager.AddPasswordAsync(user, password).Throw(log); } }); } public async Task UpdateAsync(string id, UserValues values, bool silent = false) { Guard.NotNullOrEmpty(id, nameof(id)); Guard.NotNull(values, nameof(values)); var user = await GetUserAsync(id); var oldUser = await ResolveAsync(user); if (!string.IsNullOrWhiteSpace(values.Email) && values.Email != user.Email) { await userManager.SetEmailAsync(user, values.Email).Throw(log); await userManager.SetUserNameAsync(user, values.Email).Throw(log); } await userManager.SyncClaims(user, values).Throw(log); if (!string.IsNullOrWhiteSpace(values.Password)) { if (await userManager.HasPasswordAsync(user)) { await userManager.RemovePasswordAsync(user).Throw(log); } await userManager.AddPasswordAsync(user, values.Password).Throw(log); } var resolved = await ResolveAsync(user); if (!silent) { foreach (var @events in userEvents) { await @events.OnUserUpdatedAsync(resolved, oldUser); } if (HasConsentGiven(values, oldUser)) { foreach (var @events in userEvents) { await @events.OnConsentGivenAsync(resolved); } } } return resolved; } public Task LockAsync(string id) { Guard.NotNullOrEmpty(id, nameof(id)); return ForUserAsync(id, user => userManager.SetLockoutEndDateAsync(user, LockoutDate()).Throw(log)); } public Task UnlockAsync(string id) { Guard.NotNullOrEmpty(id, nameof(id)); return ForUserAsync(id, user => userManager.SetLockoutEndDateAsync(user, null).Throw(log)); } public Task AddLoginAsync(string id, ExternalLoginInfo externalLogin) { Guard.NotNullOrEmpty(id, nameof(id)); return ForUserAsync(id, user => userManager.AddLoginAsync(user, externalLogin).Throw(log)); } public Task RemoveLoginAsync(string id, string loginProvider, string providerKey) { Guard.NotNullOrEmpty(id, nameof(id)); return ForUserAsync(id, user => userManager.RemoveLoginAsync(user, loginProvider, providerKey).Throw(log)); } public async Task DeleteAsync(string id) { Guard.NotNullOrEmpty(id, nameof(id)); var user = await GetUserAsync(id); var resolved = await ResolveAsync(user); await userManager.DeleteAsync(user).Throw(log); foreach (var @events in userEvents) { await @events.OnUserDeletedAsync(resolved); } } private async Task ForUserAsync(string id, Func action) { var user = await GetUserAsync(id); await action(user); return await ResolveAsync(user); } private async Task GetUserAsync(string id) { if (!userFactory.IsId(id)) { throw new DomainObjectNotFoundException(id); } var user = await userManager.FindByIdAsync(id); if (user == null) { throw new DomainObjectNotFoundException(id); } return user; } private Task ResolveAsync(IEnumerable users) { return Task.WhenAll(users.Select(async user => { return await ResolveAsync(user); })); } private async Task ResolveAsync(IdentityUser user) { var claims = await userManager.GetClaimsAsync(user); if (!claims.Any(x => string.Equals(x.Type, SquidexClaimTypes.DisplayName, StringComparison.OrdinalIgnoreCase))) { claims.Add(new Claim(SquidexClaimTypes.DisplayName, user.Email)); } return new UserWithClaims(user, claims.ToList()); } private async Task ResolveOptionalAsync(IdentityUser? user) { if (user == null) { return null; } return await ResolveAsync(user); } private static bool HasConsentGiven(UserValues values, IUser? oldUser) { if (values.Consent == true && oldUser?.Claims.HasConsent() != true) { return true; } return values.ConsentForEmails == true && oldUser?.Claims.HasConsentForEmails() != true; } private static DateTimeOffset LockoutDate() { return DateTimeOffset.UtcNow.AddYears(100); } } }