using Microsoft.AspNetCore.Identity; using Raven.Client.Documents; using Raven.Client.Documents.Linq; namespace zero.Identity; public class UserService : IUserService { protected UserManager UserManager { get; private set; } protected IStoreOperations Operations { get; private set; } protected IZeroContext Context { get; private set; } public UserService(ISharedStoreOperationsWithInactive operations, IZeroContext context, IZeroOptions options, UserManager userManager) { UserManager = userManager; Operations = operations; Context = context; } /// public async Task GetUserById(string id) { ZeroUser user = await UserManager.FindByIdAsync(id); return user; } /// public async Task GetUserByEmail(string email) { ZeroUser user = await UserManager.FindByEmailAsync(email); return user; } /// public async Task> GetByIds(params string[] ids) { return await Operations.Load(ids); } /// public async Task> GetAll(int pageNumber, int pageSize) { return await Operations.Load(pageNumber, pageSize, q => q.OrderByDescending(x => x.CreatedDate)); } /// public async Task> Save(ZeroUser model) { bool updateSecurityStamp = false; bool isUpdate = false; if (!model.Id.IsNullOrEmpty()) { // TODO throws "Attempted to associate a different object with id ..." //ZeroUser origin = await GetUserById(model.Id); isUpdate = true; // origin != null; updateSecurityStamp = true; // origin != null && model.PasswordHash != origin.PasswordHash; } Result result = isUpdate ? await Operations.Update(model) : await Operations.Create(model); if (updateSecurityStamp) { await UserManager.UpdateSecurityStampAsync(model); } return result; } /// public async Task> Delete(string id) { return await Operations.Delete(id); } /// public async Task> HashPassword(ZeroUser user, string currentPassword, string newPassword, string confirmNewPassword) { if (newPassword != confirmNewPassword) { return Result.Fail(nameof(newPassword), "@errors.changepassword.newpasswordsnotmatching"); } if (currentPassword.IsNullOrWhiteSpace() || newPassword.IsNullOrWhiteSpace()) { return Result.Fail(nameof(currentPassword), "@errors.changepassword.emptyfields"); } if (user == null) { return Result.Fail("@errors.changepassword.nouser"); } if (UserManager.PasswordHasher.VerifyHashedPassword(user, user.PasswordHash, currentPassword) != PasswordVerificationResult.Success) { return Result.Fail("@errors.changepassword.passwordincorrect"); } // validate new password List errors = new(); bool isValid = true; foreach (var v in UserManager.PasswordValidators) { var result = await v.ValidateAsync(UserManager, user, newPassword); if (!result.Succeeded) { if (result.Errors.Any()) { errors.AddRange(result.Errors); } isValid = false; } } if (!isValid) { Result result = Result.Fail(); foreach (IdentityError error in errors) { result.AddError(error.Description); } return result; } return Result.Success(UserManager.PasswordHasher.HashPassword(user, newPassword)); } /// public async Task> UpdatePassword(ZeroUser user, string currentPassword, string newPassword) { if (currentPassword.IsNullOrWhiteSpace() || newPassword.IsNullOrWhiteSpace()) { return Result.Fail(nameof(currentPassword), "@errors.changepassword.emptyfields"); } if (user == null) { return Result.Fail("@errors.changepassword.nouser"); } IdentityResult identityResult = await UserManager.ChangePasswordAsync(user as ZeroUser, currentPassword, newPassword); if (!identityResult.Succeeded) { Result result = Result.Fail(); foreach (IdentityError error in identityResult.Errors) { result.AddError(error.Description); } return result; } return Result.Success(user); } /// public async Task> Enable(ZeroUser user) { return await UpdateActiveState(user, true); } /// public async Task> Disable(ZeroUser user) { return await UpdateActiveState(user, false); } /// /// Updates the active state of user. /// If IsActive=false, the user cannot login anymore /// async Task> UpdateActiveState(ZeroUser user, bool isActive) { user.IsActive = isActive; IdentityResult identityResult = await UserManager.UpdateAsync(user as ZeroUser); if (!identityResult.Succeeded) { Result result = Result.Fail(); foreach (IdentityError error in identityResult.Errors) { result.AddError(error.Description); } return result; } await UserManager.UpdateSecurityStampAsync(user as ZeroUser); return Result.Success(user); } /// public async Task TrySwitchApp(string appId) { IZeroDocumentSession session = Context.Store.Session(global: true); ZeroUser user = await UserManager.GetUserAsync(Context.BackofficeUser); if (user == null || appId.IsNullOrEmpty()) { return false; } string[] allowedAppIds = user.GetAllowedAppIds(); bool isMainId = appId.Equals(user.AppId, StringComparison.InvariantCultureIgnoreCase); bool isAllowedId = allowedAppIds.Contains(appId, StringComparer.InvariantCultureIgnoreCase); if (user.IsSuper || isMainId || isAllowedId) { user.CurrentAppId = appId; //byte[] bytes = new byte[20]; //RandomNumberGenerator.Fill(bytes); //user.SecurityStamp = Base32.ToBase32(bytes); // TODO update security stamp but Base32 is .net core internal await session.StoreAsync(user); await session.SaveChangesAsync(); return true; } return false; } } public interface IUserService { /// /// Find user by id /// Task GetUserById(string id); /// /// Find user by email /// Task GetUserByEmail(string email); /// /// Get users by ids /// Task> GetByIds(params string[] ids); /// /// Get all users for the current application /// Task> GetAll(int pageNumber, int pageSize); /// /// Creates or updates a user /// Task> Save(ZeroUser model); /// /// Deletes a user /// Task> Delete(string id); /// /// Changes the password of the current user. /// User is logged out if this operation succeeds. /// Task> UpdatePassword(ZeroUser user, string currentPassword, string newPassword); /// /// Tries to hash a new password /// Task> HashPassword(ZeroUser user, string currentPassword, string newPassword, string confirmNewPassword); /// /// Enables a user /// Task> Enable(ZeroUser user); /// /// Disables a user /// Task> Disable(ZeroUser user); /// /// Tries to switch the currently loaded backoffice application for the current user /// Task TrySwitchApp(string appId); }