From 82db763951cc775780b0566940e055c3d6518365 Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Thu, 22 Jan 2026 11:14:23 -0500 Subject: [PATCH] Refactor repository methods to async and update credential logic --- .../scripts/01-schema/schema.sql | 11 +++-- .../USP_AddUserCredential.sql | 20 ++++---- .../USP_DeleteUserCredential.sql | 0 .../USP_GetUserCredentialByUserAccountId.sql | 18 +++++++ .../USP_UpdateUserCredential.sql | 0 .../Repositories/Repository.cs | 10 ++-- .../UserAccount/IUserAccountRepository.cs | 14 +++--- .../UserAccount/UserAccountRepository.cs | 14 +++--- .../IUserCredentialRepository.cs | 12 ++--- .../UserAccountRepositoryTests.cs | 48 +++++++++---------- Service/Service.Core/Services/UserService.cs | 4 +- 11 files changed, 84 insertions(+), 67 deletions(-) delete mode 100644 Database/Database.Core/scripts/03-crud/02-UserCredential/USP_DeleteUserCredential.sql delete mode 100644 Database/Database.Core/scripts/03-crud/02-UserCredential/USP_UpdateUserCredential.sql diff --git a/Database/Database.Core/scripts/01-schema/schema.sql b/Database/Database.Core/scripts/01-schema/schema.sql index 637832e..5a09894 100644 --- a/Database/Database.Core/scripts/01-schema/schema.sql +++ b/Database/Database.Core/scripts/01-schema/schema.sql @@ -164,7 +164,13 @@ CREATE TABLE UserCredential -- delete credentials when user account is deleted Hash NVARCHAR(MAX) NOT NULL, -- uses argon2 - Timer ROWVERSION, + IsRevoked BIT NOT NULL + CONSTRAINT DF_UserCredential_IsRevoked DEFAULT 0, + + RevokedAt DATETIME NULL, + + Timer ROWVERSION, + CONSTRAINT PK_UserCredential PRIMARY KEY (UserCredentialID), @@ -173,9 +179,6 @@ CREATE TABLE UserCredential -- delete credentials when user account is deleted FOREIGN KEY (UserAccountID) REFERENCES UserAccount(UserAccountID) ON DELETE CASCADE, - - CONSTRAINT AK_UserCredential_UserAccountID - UNIQUE (UserAccountID) ); CREATE NONCLUSTERED INDEX IX_UserCredential_UserAccount diff --git a/Database/Database.Core/scripts/03-crud/02-UserCredential/USP_AddUserCredential.sql b/Database/Database.Core/scripts/03-crud/02-UserCredential/USP_AddUserCredential.sql index bd9395e..f2b7b6a 100644 --- a/Database/Database.Core/scripts/03-crud/02-UserCredential/USP_AddUserCredential.sql +++ b/Database/Database.Core/scripts/03-crud/02-UserCredential/USP_AddUserCredential.sql @@ -1,6 +1,6 @@ -CREATE OR ALTER PROCEDURE dbo.USP_AddUserCredential( - @UserAccountId uniqueidentifier, - @Hash nvarchar(max) +CREATE OR ALTER PROCEDURE dbo.USP_AddUpdateUserCredential( + @UserAccountId UNIQUEIDENTIFIER, + @Hash NVARCHAR(MAX) ) AS BEGIN @@ -16,14 +16,14 @@ BEGIN ) THROW 50001, 'UserAccountID does not exist.', 1; - IF EXISTS ( - SELECT 1 - FROM dbo.UserCredential - WHERE UserAccountID = @UserAccountId - ) - THROW 50002, 'UserCredential for this UserAccountID already exists.', 1; - + -- invalidate old credentials + UPDATE dbo.UserCredential + SET IsRevoked = 1, + RevokedAt = GETDATE() + WHERE UserAccountId = @UserAccountId + AND IsRevoked = 0; + INSERT INTO dbo.UserCredential (UserAccountId, Hash) VALUES diff --git a/Database/Database.Core/scripts/03-crud/02-UserCredential/USP_DeleteUserCredential.sql b/Database/Database.Core/scripts/03-crud/02-UserCredential/USP_DeleteUserCredential.sql deleted file mode 100644 index e69de29..0000000 diff --git a/Database/Database.Core/scripts/03-crud/02-UserCredential/USP_GetUserCredentialByUserAccountId.sql b/Database/Database.Core/scripts/03-crud/02-UserCredential/USP_GetUserCredentialByUserAccountId.sql index e69de29..88a67a5 100644 --- a/Database/Database.Core/scripts/03-crud/02-UserCredential/USP_GetUserCredentialByUserAccountId.sql +++ b/Database/Database.Core/scripts/03-crud/02-UserCredential/USP_GetUserCredentialByUserAccountId.sql @@ -0,0 +1,18 @@ +CREATE OR ALTER PROCEDURE dbo.USP_GetUserCredentialByUserAccountId( + @UserAccountId UNIQUEIDENTIFIER +) +AS +BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + + SELECT + UserCredentialId, + UserAccountId, + Hash, + IsRevoked, + CreatedAt, + RevokedAt + FROM dbo.UserCredential + WHERE UserAccountId = @UserAccountId AND IsRevoked = 0; +END; \ No newline at end of file diff --git a/Database/Database.Core/scripts/03-crud/02-UserCredential/USP_UpdateUserCredential.sql b/Database/Database.Core/scripts/03-crud/02-UserCredential/USP_UpdateUserCredential.sql deleted file mode 100644 index e69de29..0000000 diff --git a/Repository/Repository.Core/Repositories/Repository.cs b/Repository/Repository.Core/Repositories/Repository.cs index 14cab10..dd3de00 100644 --- a/Repository/Repository.Core/Repositories/Repository.cs +++ b/Repository/Repository.Core/Repositories/Repository.cs @@ -13,11 +13,11 @@ namespace DataAccessLayer.Repositories return connection; } - public abstract Task Add(T entity); - public abstract Task> GetAll(int? limit, int? offset); - public abstract Task GetById(Guid id); - public abstract Task Update(T entity); - public abstract Task Delete(Guid id); + public abstract Task AddAsync(T entity); + public abstract Task> GetAllAsync(int? limit, int? offset); + public abstract Task GetByIdAsync(Guid id); + public abstract Task UpdateAsync(T entity); + public abstract Task DeleteAsync(Guid id); protected abstract T MapToEntity(SqlDataReader reader); } diff --git a/Repository/Repository.Core/Repositories/UserAccount/IUserAccountRepository.cs b/Repository/Repository.Core/Repositories/UserAccount/IUserAccountRepository.cs index dd5b76b..6fcd832 100644 --- a/Repository/Repository.Core/Repositories/UserAccount/IUserAccountRepository.cs +++ b/Repository/Repository.Core/Repositories/UserAccount/IUserAccountRepository.cs @@ -4,12 +4,12 @@ namespace DataAccessLayer.Repositories.UserAccount { public interface IUserAccountRepository { - Task Add(Entities.UserAccount userAccount); - Task GetById(Guid id); - Task> GetAll(int? limit, int? offset); - Task Update(Entities.UserAccount userAccount); - Task Delete(Guid id); - Task GetByUsername(string username); - Task GetByEmail(string email); + Task AddAsync(Entities.UserAccount userAccount); + Task GetByIdAsync(Guid id); + Task> GetAllAsync(int? limit, int? offset); + Task UpdateAsync(Entities.UserAccount userAccount); + Task DeleteAsync(Guid id); + Task GetByUsernameAsync(string username); + Task GetByEmailAsync(string email); } } diff --git a/Repository/Repository.Core/Repositories/UserAccount/UserAccountRepository.cs b/Repository/Repository.Core/Repositories/UserAccount/UserAccountRepository.cs index 80ee038..a71d147 100644 --- a/Repository/Repository.Core/Repositories/UserAccount/UserAccountRepository.cs +++ b/Repository/Repository.Core/Repositories/UserAccount/UserAccountRepository.cs @@ -7,7 +7,7 @@ namespace DataAccessLayer.Repositories.UserAccount public class UserAccountRepository(ISqlConnectionFactory connectionFactory) : Repository(connectionFactory), IUserAccountRepository { - public override async Task Add(Entities.UserAccount userAccount) + public override async Task AddAsync(Entities.UserAccount userAccount) { await using var connection = await CreateConnection(); await using var command = new SqlCommand("usp_CreateUserAccount", connection); @@ -23,7 +23,7 @@ namespace DataAccessLayer.Repositories.UserAccount await command.ExecuteNonQueryAsync(); } - public override async Task GetById(Guid id) + public override async Task GetByIdAsync(Guid id) { await using var connection = await CreateConnection(); await using var command = new SqlCommand("usp_GetUserAccountById", connection) @@ -37,7 +37,7 @@ namespace DataAccessLayer.Repositories.UserAccount return await reader.ReadAsync() ? MapToEntity(reader) : null; } - public override async Task> GetAll(int? limit, int? offset) + public override async Task> GetAllAsync(int? limit, int? offset) { await using var connection = await CreateConnection(); await using var command = new SqlCommand("usp_GetAllUserAccounts", connection); @@ -60,7 +60,7 @@ namespace DataAccessLayer.Repositories.UserAccount return users; } - public override async Task Update(Entities.UserAccount userAccount) + public override async Task UpdateAsync(Entities.UserAccount userAccount) { await using var connection = await CreateConnection(); await using var command = new SqlCommand("usp_UpdateUserAccount", connection); @@ -76,7 +76,7 @@ namespace DataAccessLayer.Repositories.UserAccount await command.ExecuteNonQueryAsync(); } - public override async Task Delete(Guid id) + public override async Task DeleteAsync(Guid id) { await using var connection = await CreateConnection(); await using var command = new SqlCommand("usp_DeleteUserAccount", connection); @@ -86,7 +86,7 @@ namespace DataAccessLayer.Repositories.UserAccount await command.ExecuteNonQueryAsync(); } - public async Task GetByUsername(string username) + public async Task GetByUsernameAsync(string username) { await using var connection = await CreateConnection(); await using var command = new SqlCommand("usp_GetUserAccountByUsername", connection); @@ -98,7 +98,7 @@ namespace DataAccessLayer.Repositories.UserAccount return await reader.ReadAsync() ? MapToEntity(reader) : null; } - public async Task GetByEmail(string email) + public async Task GetByEmailAsync(string email) { await using var connection = await CreateConnection(); await using var command = new SqlCommand("usp_GetUserAccountByEmail", connection); diff --git a/Repository/Repository.Core/Repositories/UserCredential/IUserCredentialRepository.cs b/Repository/Repository.Core/Repositories/UserCredential/IUserCredentialRepository.cs index 9ac74c2..b6b4ae8 100644 --- a/Repository/Repository.Core/Repositories/UserCredential/IUserCredentialRepository.cs +++ b/Repository/Repository.Core/Repositories/UserCredential/IUserCredentialRepository.cs @@ -1,11 +1,7 @@ -namespace DataAccessLayer.Repositories.UserCredential; +using DataAccessLayer.Entities; public interface IUserCredentialRepository { - Task Add(Entities.UserCredential credential); - Task GetById(Guid userCredentialId); - Task GetByUserAccountId(Guid userAccountId); - Task> GetAll(int? limit, int? offset); - Task Update(Entities.UserCredential credential); - Task Delete(Guid userCredentialId); -} \ No newline at end of file + Task RotateCredentialAsync(Guid userAccountId, UserCredential credential); + Task GetActiveCredentialByUserAccountIdAsync(Guid userAccountId); +} diff --git a/Repository/Repository.Tests/UserAccountRepositoryTests.cs b/Repository/Repository.Tests/UserAccountRepositoryTests.cs index 3cca273..4c0cb1e 100644 --- a/Repository/Repository.Tests/UserAccountRepositoryTests.cs +++ b/Repository/Repository.Tests/UserAccountRepositoryTests.cs @@ -25,8 +25,8 @@ namespace DALTests }; // Act - await _repository.Add(userAccount); - var retrievedUser = await _repository.GetById(userAccount.UserAccountId); + await _repository.AddAsync(userAccount); + var retrievedUser = await _repository.GetByIdAsync(userAccount.UserAccountId); // Assert Assert.NotNull(retrievedUser); @@ -48,10 +48,10 @@ namespace DALTests CreatedAt = DateTime.UtcNow, DateOfBirth = new DateTime(1985, 5, 15), }; - await _repository.Add(userAccount); + await _repository.AddAsync(userAccount); // Act - var retrievedUser = await _repository.GetById(userId); + var retrievedUser = await _repository.GetByIdAsync(userId); // Assert Assert.NotNull(retrievedUser); @@ -72,12 +72,12 @@ namespace DALTests CreatedAt = DateTime.UtcNow, DateOfBirth = new DateTime(1992, 3, 10), }; - await _repository.Add(userAccount); + await _repository.AddAsync(userAccount); // Act userAccount.FirstName = "Updated"; - await _repository.Update(userAccount); - var updatedUser = await _repository.GetById(userAccount.UserAccountId); + await _repository.UpdateAsync(userAccount); + var updatedUser = await _repository.GetByIdAsync(userAccount.UserAccountId); // Assert Assert.NotNull(updatedUser); @@ -98,11 +98,11 @@ namespace DALTests CreatedAt = DateTime.UtcNow, DateOfBirth = new DateTime(1995, 7, 20), }; - await _repository.Add(userAccount); + await _repository.AddAsync(userAccount); // Act - await _repository.Delete(userAccount.UserAccountId); - var deletedUser = await _repository.GetById(userAccount.UserAccountId); + await _repository.DeleteAsync(userAccount.UserAccountId); + var deletedUser = await _repository.GetByIdAsync(userAccount.UserAccountId); // Assert Assert.Null(deletedUser); @@ -132,11 +132,11 @@ namespace DALTests CreatedAt = DateTime.UtcNow, DateOfBirth = new DateTime(1992, 2, 2), }; - await _repository.Add(user1); - await _repository.Add(user2); + await _repository.AddAsync(user1); + await _repository.AddAsync(user2); // Act - var allUsers = await _repository.GetAll(null, null); + var allUsers = await _repository.GetAllAsync(null, null); // Assert Assert.NotNull(allUsers); @@ -183,11 +183,11 @@ namespace DALTests foreach (var user in users) { - await _repository.Add(user); + await _repository.AddAsync(user); } // Act - var page = (await _repository.GetAll(2, 0)).ToList(); + var page = (await _repository.GetAllAsync(2, 0)).ToList(); // Assert Assert.Equal(2, page.Count); @@ -197,10 +197,10 @@ namespace DALTests public async Task GetAll_WithPagination_ShouldValidateArguments() { await Assert.ThrowsAsync(async () => - (await _repository.GetAll(0, 0)).ToList() + (await _repository.GetAllAsync(0, 0)).ToList() ); await Assert.ThrowsAsync(async () => - (await _repository.GetAll(1, -1)).ToList() + (await _repository.GetAllAsync(1, -1)).ToList() ); } } @@ -209,7 +209,7 @@ namespace DALTests { private readonly Dictionary _store = new(); - public Task Add(UserAccount userAccount) + public Task AddAsync(UserAccount userAccount) { if (userAccount.UserAccountId == Guid.Empty) { @@ -219,13 +219,13 @@ namespace DALTests return Task.CompletedTask; } - public Task GetById(Guid id) + public Task GetByIdAsync(Guid id) { _store.TryGetValue(id, out var user); return Task.FromResult(user is null ? null : Clone(user)); } - public Task> GetAll(int? limit, int? offset) + public Task> GetAllAsync(int? limit, int? offset) { if (limit.HasValue && limit.Value <= 0) throw new ArgumentOutOfRangeException(nameof(limit)); if (offset.HasValue && offset.Value < 0) throw new ArgumentOutOfRangeException(nameof(offset)); @@ -240,26 +240,26 @@ namespace DALTests return Task.FromResult>(query.ToList()); } - public Task Update(UserAccount userAccount) + public Task UpdateAsync(UserAccount userAccount) { if (!_store.ContainsKey(userAccount.UserAccountId)) return Task.CompletedTask; _store[userAccount.UserAccountId] = Clone(userAccount); return Task.CompletedTask; } - public Task Delete(Guid id) + public Task DeleteAsync(Guid id) { _store.Remove(id); return Task.CompletedTask; } - public Task GetByUsername(string username) + public Task GetByUsernameAsync(string username) { var user = _store.Values.FirstOrDefault(u => u.Username == username); return Task.FromResult(user is null ? null : Clone(user)); } - public Task GetByEmail(string email) + public Task GetByEmailAsync(string email) { var user = _store.Values.FirstOrDefault(u => u.Email == email); return Task.FromResult(user is null ? null : Clone(user)); diff --git a/Service/Service.Core/Services/UserService.cs b/Service/Service.Core/Services/UserService.cs index b4c4a85..2085f8b 100644 --- a/Service/Service.Core/Services/UserService.cs +++ b/Service/Service.Core/Services/UserService.cs @@ -8,12 +8,12 @@ namespace BusinessLayer.Services { public async Task> GetAllAsync(int? limit = null, int? offset = null) { - return await repository.GetAll(limit, offset); + return await repository.GetAllAsync(limit, offset); } public async Task GetByIdAsync(Guid id) { - return await repository.GetById(id); + return await repository.GetByIdAsync(id); } } }