LiteDbUserStore.cs
1 using GUNRPG.Core.Identity; 2 using LiteDB; 3 using Microsoft.AspNetCore.Identity; 4 5 namespace GUNRPG.Infrastructure.Identity; 6 7 /// <summary> 8 /// LiteDB-backed ASP.NET Identity user store. 9 /// Implements the minimum interfaces required by <see cref="UserManager{TUser}"/>: 10 /// user CRUD, password hashing storage, and e-mail lookup. 11 /// </summary> 12 public sealed class LiteDbUserStore : 13 IUserStore<ApplicationUser>, 14 IUserPasswordStore<ApplicationUser>, 15 IUserEmailStore<ApplicationUser> 16 { 17 internal const string CollectionName = "identity_users"; 18 19 private readonly ILiteCollection<ApplicationUser> _users; 20 21 public LiteDbUserStore(ILiteDatabase db) 22 { 23 _users = db.GetCollection<ApplicationUser>(CollectionName); 24 _users.EnsureIndex(u => u.NormalizedUserName, unique: true); 25 _users.EnsureIndex(u => u.NormalizedEmail); 26 } 27 28 // ── IUserStore ────────────────────────────────────────────────────────── 29 30 public Task<IdentityResult> CreateAsync(ApplicationUser user, CancellationToken ct) 31 { 32 try 33 { 34 _users.Insert(user); 35 return Task.FromResult(IdentityResult.Success); 36 } 37 catch (LiteException ex) 38 { 39 return Task.FromResult(IdentityResult.Failed(new IdentityError 40 { 41 Code = "LiteDbInsertError", 42 Description = $"Failed to create user: {ex.Message}", 43 })); 44 } 45 } 46 47 public Task<IdentityResult> UpdateAsync(ApplicationUser user, CancellationToken ct) 48 { 49 try 50 { 51 if (!_users.Update(user)) 52 return Task.FromResult(IdentityResult.Failed(new IdentityError 53 { 54 Code = "UserUpdateFailed", 55 Description = "Failed to update user: user not found or not modified.", 56 })); 57 58 return Task.FromResult(IdentityResult.Success); 59 } 60 catch (LiteException ex) 61 { 62 return Task.FromResult(IdentityResult.Failed(new IdentityError 63 { 64 Code = "LiteDbUpdateError", 65 Description = $"Failed to update user: {ex.Message}", 66 })); 67 } 68 } 69 70 public Task<IdentityResult> DeleteAsync(ApplicationUser user, CancellationToken ct) 71 { 72 try 73 { 74 if (!_users.Delete(user.Id)) 75 return Task.FromResult(IdentityResult.Failed(new IdentityError 76 { 77 Code = "UserDeleteFailed", 78 Description = "Failed to delete user: user not found.", 79 })); 80 81 return Task.FromResult(IdentityResult.Success); 82 } 83 catch (LiteException ex) 84 { 85 return Task.FromResult(IdentityResult.Failed(new IdentityError 86 { 87 Code = "LiteDbDeleteError", 88 Description = $"Failed to delete user: {ex.Message}", 89 })); 90 } 91 } 92 93 public Task<ApplicationUser?> FindByIdAsync(string userId, CancellationToken ct) => 94 Task.FromResult<ApplicationUser?>(_users.FindById(userId)); 95 96 public Task<ApplicationUser?> FindByNameAsync(string normalizedUserName, CancellationToken ct) => 97 Task.FromResult<ApplicationUser?>(_users.FindOne(u => u.NormalizedUserName == normalizedUserName)); 98 99 public Task<string> GetUserIdAsync(ApplicationUser user, CancellationToken ct) => 100 Task.FromResult(user.Id); 101 102 public Task<string?> GetUserNameAsync(ApplicationUser user, CancellationToken ct) => 103 Task.FromResult(user.UserName); 104 105 public Task SetUserNameAsync(ApplicationUser user, string? userName, CancellationToken ct) 106 { 107 user.UserName = userName; 108 return Task.CompletedTask; 109 } 110 111 public Task<string?> GetNormalizedUserNameAsync(ApplicationUser user, CancellationToken ct) => 112 Task.FromResult(user.NormalizedUserName); 113 114 public Task SetNormalizedUserNameAsync(ApplicationUser user, string? normalizedName, CancellationToken ct) 115 { 116 user.NormalizedUserName = normalizedName; 117 return Task.CompletedTask; 118 } 119 120 // ── IUserPasswordStore ────────────────────────────────────────────────── 121 122 public Task SetPasswordHashAsync(ApplicationUser user, string? passwordHash, CancellationToken ct) 123 { 124 user.PasswordHash = passwordHash; 125 return Task.CompletedTask; 126 } 127 128 public Task<string?> GetPasswordHashAsync(ApplicationUser user, CancellationToken ct) => 129 Task.FromResult(user.PasswordHash); 130 131 public Task<bool> HasPasswordAsync(ApplicationUser user, CancellationToken ct) => 132 Task.FromResult(user.PasswordHash is not null); 133 134 // ── IUserEmailStore ───────────────────────────────────────────────────── 135 136 public Task SetEmailAsync(ApplicationUser user, string? email, CancellationToken ct) 137 { 138 user.Email = email; 139 return Task.CompletedTask; 140 } 141 142 public Task<string?> GetEmailAsync(ApplicationUser user, CancellationToken ct) => 143 Task.FromResult(user.Email); 144 145 public Task<bool> GetEmailConfirmedAsync(ApplicationUser user, CancellationToken ct) => 146 Task.FromResult(user.EmailConfirmed); 147 148 public Task SetEmailConfirmedAsync(ApplicationUser user, bool confirmed, CancellationToken ct) 149 { 150 user.EmailConfirmed = confirmed; 151 return Task.CompletedTask; 152 } 153 154 public Task<ApplicationUser?> FindByEmailAsync(string normalizedEmail, CancellationToken ct) => 155 Task.FromResult<ApplicationUser?>(_users.FindOne(u => u.NormalizedEmail == normalizedEmail)); 156 157 public Task<string?> GetNormalizedEmailAsync(ApplicationUser user, CancellationToken ct) => 158 Task.FromResult(user.NormalizedEmail); 159 160 public Task SetNormalizedEmailAsync(ApplicationUser user, string? normalizedEmail, CancellationToken ct) 161 { 162 user.NormalizedEmail = normalizedEmail; 163 return Task.CompletedTask; 164 } 165 166 public void Dispose() { /* LiteDatabase lifetime is managed by the DI container. */ } 167 }