using Microsoft.AspNetCore.Identity; using Raven.Client.Documents; using Raven.Client.Documents.Linq; using Raven.Client.Documents.Session; using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using zero.Core.Database; using zero.Core.Entities; using zero.Core.Extensions; using zero.Core.Options; namespace zero.Core.Identity { public partial class RavenUserStore : IUserStore, IUserEmailStore, IUserLockoutStore, IUserPasswordStore, IUserClaimStore, IUserSecurityStampStore, IProtectedUserStore where TUser : class, IIdentityUser { protected IZeroStore Store { get; private set; } protected IZeroOptions Options { get; private set; } public RavenUserStore(IZeroStore store, IZeroOptions options) { Store = store; Options = options; } /// /// Get the Raven compare/exchange key for this user. /// This should be unique within the store, otherwise you can't create users with the same email in different stores. /// protected virtual string GetReservationKey(TUser user) => Constants.Database.ReservationPrefix + typeof(TUser) + ':' + user.Email; // /// Scope queries to an optional application or something else /// protected virtual IRavenQueryable ScopeQuery(IRavenQueryable query) => query; /// /// Get the database to operate on for this store /// protected virtual string GetDatabase() => Store.ResolvedDatabase; /// /// Get a new document store session /// protected virtual IAsyncDocumentSession GetSession() => Store.OpenAsyncSession(GetDatabase()); /// /// Whether an email is already reserved /// protected virtual async Task IsEmailReserved(IAsyncDocumentSession session, TUser user, CancellationToken cancellationToken = default) { TUser existingUser = await ScopeQuery(session.Query()).FirstOrDefaultAsync(x => x.Email == user.Email, cancellationToken); return existingUser != null && existingUser.Id != user.Id; } /// public async Task CreateAsync(TUser user, CancellationToken cancellationToken) { using IAsyncDocumentSession session = GetSession(); if (await IsEmailReserved(session, user)) { return IdentityResult.Failed(new IdentityError { Code = "DuplicateEmail", Description = $"The email address {user.Email} is already taken." }); } await session.StoreAsync(user, cancellationToken); await session.SaveChangesAsync(cancellationToken); return IdentityResult.Success; } /// public async Task DeleteAsync(TUser user, CancellationToken cancellationToken) { using IAsyncDocumentSession session = GetSession(); TUser source = await session.LoadAsync(user.Id, cancellationToken); session.Delete(source); await session.SaveChangesAsync(cancellationToken); return IdentityResult.Success; } /// public async Task UpdateAsync(TUser user, CancellationToken cancellationToken) { using (IAsyncDocumentSession session = GetSession()) { TUser source = await session.LoadAsync(user.Id, cancellationToken); if (source == null) { return IdentityResult.Failed(new IdentityError { Code = "UserNotFound", Description = $"Could not find stored user with id {user.Id}." }); } if (source.Email != user.Email && await IsEmailReserved(session, user)) { return IdentityResult.Failed(new IdentityError { Code = "DuplicateEmail", Description = $"The email address {user.Email} is already taken." }); } } using (IAsyncDocumentSession session = GetSession()) { await session.StoreAsync(user, cancellationToken); await session.SaveChangesAsync(cancellationToken); } return IdentityResult.Success; } /// public async Task FindByIdAsync(string userId, CancellationToken cancellationToken) { using IAsyncDocumentSession session = GetSession(); return await ScopeQuery(session.Query()).FirstOrDefaultAsync(x => x.Id == userId, cancellationToken); } /// public async Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) { using IAsyncDocumentSession session = GetSession(); return await ScopeQuery(session.Query()).FirstOrDefaultAsync(x => x.Username == normalizedUserName, cancellationToken); } /// public Task GetNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.Username); /// public Task GetUserIdAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.Id); /// public Task GetUserNameAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.Username); /// public Task SetNormalizedUserNameAsync(TUser user, string normalizedName, CancellationToken cancellationToken) { user.Username = normalizedName; return Task.CompletedTask; } /// public async Task SetUserNameAsync(TUser user, string userName, CancellationToken cancellationToken) { await SetNormalizedUserNameAsync(user, userName, cancellationToken); } /// void IDisposable.Dispose() { } /* * **************************************************** * SECURITY STAMP * **************************************************** */ /// public Task GetSecurityStampAsync(TUser user, CancellationToken cancellationToken) { return Task.FromResult(user.SecurityStamp); } /// public Task SetSecurityStampAsync(TUser user, string stamp, CancellationToken cancellationToken) { user.SecurityStamp = stamp; return Task.CompletedTask; } /* * **************************************************** * EMAIL * **************************************************** */ /// public async Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken) { using IAsyncDocumentSession session = GetSession(); return await ScopeQuery(session.Query()).FirstOrDefaultAsync(x => x.Email == normalizedEmail, cancellationToken); } /// public Task GetEmailAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.Email); /// public Task GetEmailConfirmedAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.IsEmailConfirmed); /// public Task GetNormalizedEmailAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.Email); /// public Task SetEmailAsync(TUser user, string email, CancellationToken cancellationToken) { user.Email = email.ToLowerInvariant(); return Task.CompletedTask; } /// public Task SetEmailConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken) { user.IsEmailConfirmed = confirmed; return Task.CompletedTask; } /// public Task SetNormalizedEmailAsync(TUser user, string normalizedEmail, CancellationToken cancellationToken) { user.Email = normalizedEmail.ToLowerInvariant(); return Task.CompletedTask; } /* * **************************************************** * LOCKOUT * **************************************************** */ /// public Task GetAccessFailedCountAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.AccessFailedCount); /// public Task GetLockoutEnabledAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.LockoutEnabled); /// public Task GetLockoutEndDateAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.LockoutEnd); /// public Task IncrementAccessFailedCountAsync(TUser user, CancellationToken cancellationToken) { user.AccessFailedCount += 1; return Task.FromResult(user.AccessFailedCount); } /// public Task ResetAccessFailedCountAsync(TUser user, CancellationToken cancellationToken) { user.AccessFailedCount = 0; return Task.CompletedTask; } /// public Task SetLockoutEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken) { user.LockoutEnabled = enabled; return Task.CompletedTask; } /// public Task SetLockoutEndDateAsync(TUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken) { user.LockoutEnd = lockoutEnd; return Task.CompletedTask; } /* * **************************************************** * PASSWORD * **************************************************** */ /// public Task GetPasswordHashAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.PasswordHash); /// public Task HasPasswordAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(!user.PasswordHash.IsNullOrEmpty()); /// public Task SetPasswordHashAsync(TUser user, string passwordHash, CancellationToken cancellationToken) { user.PasswordHash = passwordHash; return Task.CompletedTask; } /* * **************************************************** * CLAIM * **************************************************** */ /// public async Task AddClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken) { using IAsyncDocumentSession session = GetSession(); user.Claims.AddRange(claims.Select(claim => new UserClaim(claim))); await session.StoreAsync(user, cancellationToken); await session.SaveChangesAsync(cancellationToken); } /// public Task> GetClaimsAsync(TUser user, CancellationToken cancellationToken) { return Task.FromResult((IList)user.Claims.Select(claim => claim.ToClaim()).ToList()); } /// public async Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken) { using IAsyncDocumentSession session = GetSession(); UserClaim userClaim = new UserClaim(claim); return await ScopeQuery(session.Query()).Where(x => x.Claims.Any(c => c.Type == userClaim.Type && c.Value == userClaim.Value)).ToListAsync(); } /// public async Task RemoveClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken) { using IAsyncDocumentSession session = GetSession(); IEnumerable userClaims = claims.Select(c => new UserClaim(c)).ToList(); user.Claims = user.Claims.Except(userClaims, new UserClaimComparer()).ToList(); await session.StoreAsync(user, cancellationToken); await session.SaveChangesAsync(cancellationToken); } /// public async Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken) { using IAsyncDocumentSession session = GetSession(); UserClaim userClaim = new UserClaim(claim); UserClaim newUserClaim = new UserClaim(newClaim); user.Claims.Remove(userClaim); user.Claims.Add(newUserClaim); await session.StoreAsync(user, cancellationToken); await session.SaveChangesAsync(cancellationToken); } } }