using AutoMapper; using AutoMapper.QueryableExtensions; using LANCommander.Server.Data.Enums; using LANCommander.Server.Data.Models; using LANCommander.Server.Services.Factories; using LANCommander.Server.Services.Models; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System.Linq.Expressions; using LANCommander.Server.Services.Abstractions; using LANCommander.Server.Services.Exceptions; using LANCommander.Server.Data; using ZiggyCreatures.Caching.Fusion; namespace LANCommander.Server.Services { public class UserService : BaseService, IBaseDatabaseService { private readonly IdentityContext IdentityContext; private readonly CollectionService CollectionService; private readonly IDbContextFactory ContextFactory; private readonly IMapper Mapper; private readonly IFusionCache Cache; protected readonly List, IQueryable>> _modifiers = new(); public UserService( ILogger logger, SettingsProvider settingsProvider, IMapper mapper, IFusionCache cache, CollectionService collectionService, IDbContextFactory contextFactory, IdentityContextFactory identityContextFactory) : base(logger, settingsProvider) { IdentityContext = identityContextFactory.Create(); CollectionService = collectionService; ContextFactory = contextFactory; Mapper = mapper; Cache = cache; } public void Reconfigure() { var options = IdentityContext.UserManager.Options; if (options == null) return; options.Password.RequireNonAlphanumeric = _settingsProvider.CurrentValue.Server.Authentication.PasswordRequireNonAlphanumeric; options.Password.RequireLowercase = _settingsProvider.CurrentValue.Server.Authentication.PasswordRequireLowercase; options.Password.RequireUppercase = _settingsProvider.CurrentValue.Server.Authentication.PasswordRequireUppercase; options.Password.RequireDigit = _settingsProvider.CurrentValue.Server.Authentication.PasswordRequireDigit; options.Password.RequiredLength = _settingsProvider.CurrentValue.Server.Authentication.PasswordRequiredLength; } public async Task GetAsync(string userName) { return await FirstOrDefaultAsync(u => u.UserName.ToUpper() == userName.ToUpper()); } public async Task GetAsync(string userName) { return await FirstOrDefaultAsync(u => u.UserName.ToUpper() == userName.ToUpper()); } public async Task> GetRolesAsync(User user) { var roles = await Cache.GetOrSetAsync($"User/{user.Id}/Roles", async _ => { try { user = await Query(q => { return q .Include(u => u.UserRoles) .ThenInclude(ur => ur.Role); }).FirstOrDefaultAsync(u => u.Id == user.Id); return user.Roles; } catch (Exception ex) { _logger.LogError(ex, "Could not get roles for user {Username}", user.UserName); return new List(); } }, tags: ["User/Security", "User/Roles", $"User/{user.Id}"]); return roles; } public async Task IsInRoleAsync(User user, string roleName) { var roles = await GetRolesAsync(user); return roles.Any(r => r.Name == roleName); } public async Task> GetCollectionsAsync(User user) { try { var roles = await GetRolesAsync(user); var roleIds = roles.Select(r => r.Id).ToList(); if (roles.Any(r => r.Name.Equals(RoleService.AdministratorRoleName, StringComparison.OrdinalIgnoreCase))) return await CollectionService.GetAsync(); else return await CollectionService .Include(c => c.Roles) .GetAsync(c => c.Roles.Any(r => roleIds.Contains(r.Id))); } catch (Exception ex) { _logger.LogError(ex, "Could not get collections for user {UserName}", user.UserName); return new List(); } } public async Task ExistsAsync(Expression> predicate) { return await IdentityContext.DatabaseContext.Users.AnyAsync(predicate); } public Task AddAsync(User user) { return AddAsync(user, bypassPasswordPolicy: false); } public async Task AddAsync(User user, bool bypassPasswordPolicy, string? password = null) { IdentityResult result; if (bypassPasswordPolicy && !string.IsNullOrEmpty(password)) { user.SecurityStamp = Guid.NewGuid().ToString(); user.ConcurrencyStamp = Guid.NewGuid().ToString(); // hash & set password var hasher = new PasswordHasher(); user.PasswordHash = hasher.HashPassword(user, password); // insert & save using var context = await ContextFactory.CreateDbContextAsync(); context.Users!.Add(user); await context.SaveChangesAsync(); result = IdentityResult.Success; } else { result = await IdentityContext.UserManager.CreateAsync(user); } if (result.Succeeded) return await IdentityContext.UserManager.FindByNameAsync(user.UserName); else throw new UserRegistrationException(result, "Could not create user"); } public async Task AddToRoleAsync(string userName, string roleName) { var user = await GetAsync(userName); await IdentityContext.UserManager.AddToRoleAsync(user, roleName); } public async Task AddToRolesAsync(string userName, IEnumerable roleNames) { var user = await GetAsync(userName); var result = await IdentityContext.UserManager.AddToRolesAsync(user, roleNames); await Cache.RemoveByTagAsync(["User/Security", "User/Roles", $"User/{user.Id}", $"Library/{user.Id}"]); if (!result.Succeeded) throw new AddRoleException(result, "Could not add roles"); } public async Task RemoveFromRole(string userName, string roleName) { var user = await GetAsync(userName); await IdentityContext.UserManager.RemoveFromRoleAsync(user, roleName); } public async Task CheckPassword(string userName, string password) { var user = await GetAsync(userName); return await IdentityContext.UserManager.CheckPasswordAsync(user, password); } public async Task CheckRegister(User user, string password) { var registerErrors = new List(); var userManager = IdentityContext.UserManager; foreach (var validator in userManager.UserValidators ?? []) { var result = await validator.ValidateAsync(userManager, user); if (!result.Succeeded) { registerErrors.AddRange(result.Errors); } } foreach (var validator in userManager.PasswordValidators ?? []) { var result = await validator.ValidateAsync(userManager, user, password); if (!result.Succeeded) { registerErrors.AddRange(result.Errors); } } return registerErrors.Count > 0 ? IdentityResult.Failed(registerErrors.ToArray()) : IdentityResult.Success; } public async Task ChangePassword(string userName, string currentPassword, string newPassword) { var user = await GetAsync(userName); var result = await IdentityContext.UserManager.ChangePasswordAsync(user, currentPassword, newPassword); return result; } public Task ChangePassword(string userName, string newPassword) { return ChangePassword(userName, newPassword, bypassPolicy: false); } public async Task ChangePassword(string userName, string newPassword, bool bypassPolicy) { IdentityResult result; var user = await GetAsync(userName); if (bypassPolicy && IdentityContext.UserManager.PasswordValidators.Any()) { await IdentityContext.UserManager.RemovePasswordAsync(user); result = await IdentityContext.UserManager.AddPasswordAsync(user, newPassword); } else { var token = await IdentityContext.UserManager.GeneratePasswordResetTokenAsync(user); result = await IdentityContext.UserManager.ResetPasswordAsync(user, token, newPassword); } return result; } public async Task SignOut() { } public async Task> GetAsync() { try { var queryable = IdentityContext.UserManager.Users.AsQueryable(); foreach (var modifier in _modifiers) queryable = modifier.Invoke(queryable); return await queryable.ToListAsync(); } finally { Reset(); } } public async Task> GetAsync() { try { var queryable = IdentityContext .UserManager .Users .AsQueryable(); foreach (var modifier in _modifiers) queryable = modifier.Invoke(queryable); return await queryable .ProjectTo(Mapper.ConfigurationProvider) .ToListAsync(); } finally { Reset(); } } public async Task GetAsync(Guid id) { try { return await FirstOrDefaultAsync(u => u.Id == id); } finally { Reset(); } } public async Task GetAsync(Guid id) { try { return await FirstOrDefaultAsync(u => u.Id == id); } finally { Reset(); } } public async Task> GetAsync(Expression> predicate) { try { var queryable = IdentityContext .UserManager .Users .AsQueryable(); foreach (var modifier in _modifiers) queryable = modifier.Invoke(queryable); return await queryable.Where(predicate).ToListAsync(); } finally { Reset(); } } public async Task> GetAsync(Expression> predicate) { try { var queryable = IdentityContext .UserManager .Users .AsQueryable(); foreach (var modifier in _modifiers) queryable = modifier.Invoke(queryable); return await queryable .Where(predicate) .ProjectTo(Mapper.ConfigurationProvider) .ToListAsync(); } finally { Reset(); } } public async Task FirstOrDefaultAsync(Expression> predicate) { try { var queryable = IdentityContext .UserManager .Users .AsQueryable(); foreach (var modifier in _modifiers) queryable = modifier.Invoke(queryable); return await queryable.FirstOrDefaultAsync(predicate); } finally { Reset(); } } public async Task FirstOrDefaultAsync(Expression> predicate) { try { var queryable = IdentityContext .UserManager .Users .AsQueryable(); foreach (var modifier in _modifiers) queryable = modifier.Invoke(queryable); return await queryable .Where(predicate) .ProjectTo(Mapper.ConfigurationProvider) .FirstOrDefaultAsync(); } finally { Reset(); } } public async Task FirstOrDefaultAsync(Expression> predicate, Expression> orderKeySelector) { try { var queryable = IdentityContext .UserManager .Users .AsQueryable(); foreach (var modifier in _modifiers) queryable = modifier.Invoke(queryable); return await queryable .Where(predicate) .OrderBy(orderKeySelector) .FirstOrDefaultAsync(); } finally { Reset(); } } public async Task FirstOrDefaultAsync(Expression> predicate, Expression> orderKeySelector) { try { var queryable = IdentityContext .UserManager .Users .AsQueryable(); foreach (var modifier in _modifiers) queryable = modifier.Invoke(queryable); return await queryable .Where(predicate) .ProjectTo(Mapper.ConfigurationProvider) .OrderBy(orderKeySelector) .FirstOrDefaultAsync(); } finally { Reset(); } } public async Task ExistsAsync(Guid id) { var user = await IdentityContext .UserManager .FindByIdAsync(id.ToString()); return user != null; } public async Task> AddMissingAsync(Expression> predicate, User entity) { var result = new ExistingEntityResult(); var user = await IdentityContext .UserManager .Users .FirstOrDefaultAsync(predicate); if (user == null) { await IdentityContext.UserManager.CreateAsync(entity); result.Existing = false; result.Value = await IdentityContext.UserManager.FindByNameAsync(user.UserName); } else { result.Existing = true; result.Value = user; } return result; } public async Task UpdateAsync(User entity) { var user = await IdentityContext .UserManager .FindByIdAsync(entity.Id.ToString()); user.UserName = entity.UserName; user.PhoneNumber = entity.PhoneNumber; user.Email = entity.Email; user.TwoFactorEnabled = entity.TwoFactorEnabled; user.Alias = entity.Alias; user.Approved = entity.Approved; user.ApprovedOn = entity.ApprovedOn; await IdentityContext.UserManager.UpdateAsync(user); return user; } public async Task DeleteAsync(User entity) { var user = await IdentityContext .UserManager .FindByIdAsync(entity.Id.ToString()); await IdentityContext.UserManager.DeleteAsync(user); } public IBaseDatabaseService AsNoTracking() { return Query((queryable) => { return queryable.AsNoTracking(); }); } public IBaseDatabaseService AsSplitQuery() { return Query((queryable) => { return queryable.AsSplitQuery(); }); } public IBaseDatabaseService Query(Func, IQueryable> modifier) { _modifiers.Add(modifier); return this; } public IBaseDatabaseService Include(params Expression>[] expressions) { return Query((queryable) => { foreach (var expression in expressions) { queryable = queryable.Include(expression); } return queryable; }); } public IBaseDatabaseService SortBy(Expression> expression, SortDirection direction = SortDirection.Ascending) { switch (direction) { case SortDirection.Descending: return Query((queryable) => { return queryable.OrderByDescending(expression); }); case SortDirection.Ascending: default: return Query((queryable) => { return queryable.OrderBy(expression); }); } } protected void Reset() { _modifiers.Clear(); } public void Dispose() { } } }