Dreamine.Identity 1.0.0.0
Dreamine.Identity 프로젝트의 API와 구성 요소를 제공합니다.
로딩중...
검색중...
일치하는것 없음
SqliteUserStore.cs
이 파일의 문서화 페이지로 가기
1using Dreamine.Database.Abstractions;
3
4namespace Dreamine.Identity;
5
14public sealed class SqliteUserStore : IUserStore
15{
24 private const string SelectSql =
25 "SELECT Id, Provider, ProviderKey, Email, DisplayName, AvatarUrl, PasswordHash, " +
26 "TermsAcceptedAtUtc, PrivacyPolicyAcceptedAtUtc, AgeConfirmedAtUtc, CreatedAt, LastLoginAt " +
27 "FROM Users WHERE Provider = @Provider AND ProviderKey = @ProviderKey LIMIT 1";
28
37 private const string LocalProvider = "Local";
38
47 private readonly IDatabaseProvider _database;
48
73 public SqliteUserStore(IDatabaseProvider database)
74 {
75 _database = database ?? throw new ArgumentNullException(nameof(database));
76 _database.CreateTable<AuthUser>();
78 }
79
160 public async Task<AuthUser> UpsertAsync(
161 string provider,
162 string providerKey,
163 string email,
164 string displayName,
165 string avatarUrl,
166 CancellationToken cancellationToken = default)
167 {
168 ArgumentException.ThrowIfNullOrWhiteSpace(provider);
169 ArgumentException.ThrowIfNullOrWhiteSpace(providerKey);
170
171 var now = DateTime.UtcNow;
172 var existing = (await _database.QueryAsync<AuthUser>(
173 SelectSql,
174 new { Provider = provider, ProviderKey = providerKey },
175 cancellationToken).ConfigureAwait(false)).FirstOrDefault();
176
177 if (existing is not null)
178 {
179 existing.Email = email ?? string.Empty;
180 existing.DisplayName = displayName ?? string.Empty;
181 existing.AvatarUrl = avatarUrl ?? string.Empty;
182 existing.LastLoginAt = now;
183 await _database.UpdateAsync(existing, cancellationToken).ConfigureAwait(false);
184 return existing;
185 }
186
187 var created = new AuthUser
188 {
189 Provider = provider,
190 ProviderKey = providerKey,
191 Email = email ?? string.Empty,
192 DisplayName = displayName ?? string.Empty,
193 AvatarUrl = avatarUrl ?? string.Empty,
194 PasswordHash = string.Empty,
195 CreatedAt = now,
196 LastLoginAt = now
197 };
198
199 await _database.InsertAsync(created, cancellationToken).ConfigureAwait(false);
200
201 return (await _database.QueryAsync<AuthUser>(
202 SelectSql,
203 new { Provider = provider, ProviderKey = providerKey },
204 cancellationToken).ConfigureAwait(false)).First();
205 }
206
239 public async Task<AuthUser?> GetByIdAsync(long id, CancellationToken cancellationToken = default)
240 {
241 var rows = await _database.QueryAsync<AuthUser>(
242 "SELECT Id, Provider, ProviderKey, Email, DisplayName, AvatarUrl, PasswordHash, " +
243 "TermsAcceptedAtUtc, PrivacyPolicyAcceptedAtUtc, AgeConfirmedAtUtc, CreatedAt, LastLoginAt " +
244 "FROM Users WHERE Id = @Id LIMIT 1",
245 new { Id = id },
246 cancellationToken).ConfigureAwait(false);
247
248 return rows.FirstOrDefault();
249 }
250
307 public async Task<AuthUser?> UpdateDisplayNameAsync(
308 long id,
309 string displayName,
310 CancellationToken cancellationToken = default)
311 {
312 var user = await GetByIdAsync(id, cancellationToken).ConfigureAwait(false);
313 if (user is null)
314 {
315 return null;
316 }
317
318 displayName = displayName.Trim();
319 if (string.IsNullOrWhiteSpace(displayName))
320 {
321 throw new InvalidOperationException("표시 이름을 입력해 주세요.");
322 }
323
324 user.DisplayName = displayName;
325 await _database.UpdateAsync(user, cancellationToken).ConfigureAwait(false);
326 return user;
327 }
328
369 public async Task<AuthUser?> AcceptRequiredConsentsAsync(
370 long id,
371 DateTime acceptedAtUtc,
372 CancellationToken cancellationToken = default)
373 {
374 var user = await GetByIdAsync(id, cancellationToken).ConfigureAwait(false);
375 if (user is null)
376 {
377 return null;
378 }
379
380 user.TermsAcceptedAtUtc = acceptedAtUtc;
381 user.PrivacyPolicyAcceptedAtUtc = acceptedAtUtc;
382 user.AgeConfirmedAtUtc = acceptedAtUtc;
383 await _database.UpdateAsync(user, cancellationToken).ConfigureAwait(false);
384 return user;
385 }
386
459 public async Task<AuthUser?> ChangeLocalPasswordAsync(
460 long id,
461 string currentPassword,
462 string newPassword,
463 CancellationToken cancellationToken = default)
464 {
465 var user = await GetByIdAsync(id, cancellationToken).ConfigureAwait(false);
466 if (user is null)
467 {
468 return null;
469 }
470
471 if (!string.Equals(user.Provider, LocalProvider, StringComparison.OrdinalIgnoreCase))
472 {
473 throw new InvalidOperationException("소셜 로그인 계정의 비밀번호는 해당 로그인 제공자에서 관리됩니다.");
474 }
475
476 if (!DreaminePasswordHasher.VerifyPassword(currentPassword, user.PasswordHash))
477 {
478 throw new InvalidOperationException("현재 비밀번호가 올바르지 않습니다.");
479 }
480
481 ArgumentException.ThrowIfNullOrWhiteSpace(newPassword);
482 if (newPassword.Length < 8)
483 {
484 throw new InvalidOperationException("새 비밀번호는 8자 이상이어야 합니다.");
485 }
486
487 user.PasswordHash = DreaminePasswordHasher.HashPassword(newPassword);
488 await _database.UpdateAsync(user, cancellationToken).ConfigureAwait(false);
489 return user;
490 }
491
564 public async Task<AuthUser> CreateLocalAsync(
565 string email,
566 string displayName,
567 string password,
568 CancellationToken cancellationToken = default)
569 {
570 email = NormalizeEmail(email);
571 ArgumentException.ThrowIfNullOrWhiteSpace(password);
572
573 if (password.Length < 8)
574 {
575 throw new InvalidOperationException("비밀번호는 8자 이상이어야 합니다.");
576 }
577
578 var existing = await FindLocalByEmailAsync(email, cancellationToken).ConfigureAwait(false);
579 if (existing is not null)
580 {
581 throw new InvalidOperationException("이미 가입된 이메일입니다.");
582 }
583
584 var now = DateTime.UtcNow;
585 var user = new AuthUser
586 {
587 Provider = LocalProvider,
588 ProviderKey = email,
589 Email = email,
590 DisplayName = string.IsNullOrWhiteSpace(displayName) ? email : displayName.Trim(),
591 AvatarUrl = string.Empty,
592 PasswordHash = DreaminePasswordHasher.HashPassword(password),
593 CreatedAt = now,
594 LastLoginAt = now
595 };
596
597 await _database.InsertAsync(user, cancellationToken).ConfigureAwait(false);
598
599 return (await FindLocalByEmailAsync(email, cancellationToken).ConfigureAwait(false))!;
600 }
601
658 public async Task<AuthUser?> ValidateLocalAsync(
659 string email,
660 string password,
661 CancellationToken cancellationToken = default)
662 {
663 email = NormalizeEmail(email);
664 var user = await FindLocalByEmailAsync(email, cancellationToken).ConfigureAwait(false);
665 if (user is null)
666 {
667 return null;
668 }
669
670 var verification = DreaminePasswordHasher.VerifyPassword(password, user.PasswordHash, out var upgradedHash);
671 if (verification is PasswordHashVerificationResult.Failed)
672 {
673 return null;
674 }
675
676 user.LastLoginAt = DateTime.UtcNow;
677 if (verification is PasswordHashVerificationResult.SuccessRehashNeeded && upgradedHash is not null)
678 {
679 user.PasswordHash = upgradedHash;
680 }
681
682 await _database.UpdateAsync(user, cancellationToken).ConfigureAwait(false);
683 return user;
684 }
685
718 private async Task<AuthUser?> FindLocalByEmailAsync(
719 string email,
720 CancellationToken cancellationToken)
721 {
722 var rows = await _database.QueryAsync<AuthUser>(
723 "SELECT Id, Provider, ProviderKey, Email, DisplayName, AvatarUrl, PasswordHash, " +
724 "TermsAcceptedAtUtc, PrivacyPolicyAcceptedAtUtc, AgeConfirmedAtUtc, CreatedAt, LastLoginAt " +
725 "FROM Users WHERE Provider = @Provider AND ProviderKey = @ProviderKey LIMIT 1",
726 new { Provider = LocalProvider, ProviderKey = email },
727 cancellationToken).ConfigureAwait(false);
728
729 return rows.FirstOrDefault();
730 }
731
740 private void EnsureSchema()
741 {
742 var columns = _database.Query<TableColumn>("PRAGMA table_info(Users)")
743 .Select(column => column.Name)
744 .ToHashSet(StringComparer.OrdinalIgnoreCase);
745
746 if (!columns.Contains(nameof(AuthUser.PasswordHash)))
747 {
748 _database.ExecuteNonQuery("ALTER TABLE Users ADD COLUMN PasswordHash TEXT NOT NULL DEFAULT ''");
749 }
750
754 }
755
780 private void AddNullableDateColumnIfMissing(HashSet<string> columns, string columnName)
781 {
782 if (!columns.Contains(columnName))
783 {
784 _database.ExecuteNonQuery($"ALTER TABLE Users ADD COLUMN {columnName} TEXT NULL");
785 }
786 }
787
828 private static string NormalizeEmail(string email)
829 {
830 ArgumentException.ThrowIfNullOrWhiteSpace(email);
831 return email.Trim().ToLowerInvariant();
832 }
833
842 private sealed class TableColumn
843 {
852 public string Name { get; set; } = string.Empty;
853 }
854}
static PasswordHashVerificationResult VerifyPassword(string password, string storedHash, out string? upgradedHash)
static string HashPassword(string password)
async Task< AuthUser?> FindLocalByEmailAsync(string email, CancellationToken cancellationToken)
async Task< AuthUser > UpsertAsync(string provider, string providerKey, string email, string displayName, string avatarUrl, CancellationToken cancellationToken=default)
async Task< AuthUser > CreateLocalAsync(string email, string displayName, string password, CancellationToken cancellationToken=default)
async Task< AuthUser?> ChangeLocalPasswordAsync(long id, string currentPassword, string newPassword, CancellationToken cancellationToken=default)
SqliteUserStore(IDatabaseProvider database)
async Task< AuthUser?> AcceptRequiredConsentsAsync(long id, DateTime acceptedAtUtc, CancellationToken cancellationToken=default)
async Task< AuthUser?> ValidateLocalAsync(string email, string password, CancellationToken cancellationToken=default)
async Task< AuthUser?> UpdateDisplayNameAsync(long id, string displayName, CancellationToken cancellationToken=default)
readonly IDatabaseProvider _database
void AddNullableDateColumnIfMissing(HashSet< string > columns, string columnName)
async Task< AuthUser?> GetByIdAsync(long id, CancellationToken cancellationToken=default)
static string NormalizeEmail(string email)