using System.Data; using Dapper; using PlotLine.Models; namespace PlotLine.Data; public interface IUserRepository { Task GetByEmailAsync(string email); Task GetByIdAsync(int userId); Task CreateAsync(string email, string displayName, string passwordHash, bool emailConfirmed); Task UpdateLoginSuccessAsync(int userId); Task UpdateFailedLoginAsync(int userId, DateTime? lockoutEndUtc); Task ConfirmEmailAsync(int userId, Guid token); Task ResetPasswordAsync(int userId, Guid token, string passwordHash); Task ChangePasswordAsync(int userId, string passwordHash); Task EnableTwoFactorAsync(int userId); Task DisableTwoFactorAsync(int userId); } public interface IAuthenticationRepository { Task CreateEmailVerificationTokenAsync(int userId, Guid token, DateTime expiryUtc); Task GetEmailVerificationTokenAsync(Guid token); Task CreatePasswordResetTokenAsync(int userId, Guid token, DateTime expiryUtc); Task GetPasswordResetTokenAsync(Guid token); Task GetTwoFactorAsync(int userId); Task SaveTwoFactorAsync(int userId, string secretKey, string? recoveryCodes, DateTime? confirmedUtc); Task EnableTwoFactorAsync(int userId); Task DisableTwoFactorAsync(int userId); Task SaveRecoveryCodesAsync(int userId, string recoveryCodes); Task GetRecoveryCodesAsync(int userId); Task UpdateLastUsedTimeStepAsync(int userId, long lastUsedTimeStep); Task InsertLoginAuditAsync(UserLoginAudit audit); } public sealed class UserRepository(ISqlConnectionFactory connectionFactory) : IUserRepository { public async Task GetByEmailAsync(string email) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.User_GetByEmail", new { Email = email }, commandType: CommandType.StoredProcedure); } public async Task GetByIdAsync(int userId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.User_GetById", new { UserID = userId }, commandType: CommandType.StoredProcedure); } public async Task CreateAsync(string email, string displayName, string passwordHash, bool emailConfirmed) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.User_Create", new { Email = email, DisplayName = displayName, PasswordHash = passwordHash, EmailConfirmed = emailConfirmed }, commandType: CommandType.StoredProcedure); } public async Task UpdateLoginSuccessAsync(int userId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.User_UpdateLoginSuccess", new { UserID = userId }, commandType: CommandType.StoredProcedure); } public async Task UpdateFailedLoginAsync(int userId, DateTime? lockoutEndUtc) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.User_UpdateFailedLogin", new { UserID = userId, LockoutEndUtc = lockoutEndUtc }, commandType: CommandType.StoredProcedure); } public async Task ConfirmEmailAsync(int userId, Guid token) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.User_ConfirmEmail", new { UserID = userId, Token = token }, commandType: CommandType.StoredProcedure); } public async Task ResetPasswordAsync(int userId, Guid token, string passwordHash) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.User_ResetPassword", new { UserID = userId, Token = token, PasswordHash = passwordHash }, commandType: CommandType.StoredProcedure); } public async Task ChangePasswordAsync(int userId, string passwordHash) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.User_ChangePassword", new { UserID = userId, PasswordHash = passwordHash }, commandType: CommandType.StoredProcedure); } public async Task EnableTwoFactorAsync(int userId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.User_EnableTwoFactor", new { UserID = userId }, commandType: CommandType.StoredProcedure); } public async Task DisableTwoFactorAsync(int userId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.User_DisableTwoFactor", new { UserID = userId }, commandType: CommandType.StoredProcedure); } } public sealed class AuthenticationRepository(ISqlConnectionFactory connectionFactory) : IAuthenticationRepository { public async Task CreateEmailVerificationTokenAsync(int userId, Guid token, DateTime expiryUtc) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.User_CreateEmailVerificationToken", new { UserID = userId, Token = token, ExpiryUtc = expiryUtc }, commandType: CommandType.StoredProcedure); } public async Task GetEmailVerificationTokenAsync(Guid token) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.User_GetEmailVerificationToken", new { Token = token }, commandType: CommandType.StoredProcedure); } public async Task CreatePasswordResetTokenAsync(int userId, Guid token, DateTime expiryUtc) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.User_CreatePasswordResetToken", new { UserID = userId, Token = token, ExpiryUtc = expiryUtc }, commandType: CommandType.StoredProcedure); } public async Task GetPasswordResetTokenAsync(Guid token) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.User_GetPasswordResetToken", new { Token = token }, commandType: CommandType.StoredProcedure); } public async Task GetTwoFactorAsync(int userId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.UserTwoFactor_Get", new { UserID = userId }, commandType: CommandType.StoredProcedure); } public async Task SaveTwoFactorAsync(int userId, string secretKey, string? recoveryCodes, DateTime? confirmedUtc) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.UserTwoFactor_Save", new { UserID = userId, SecretKey = secretKey, RecoveryCodes = recoveryCodes, ConfirmedUtc = confirmedUtc }, commandType: CommandType.StoredProcedure); } public async Task EnableTwoFactorAsync(int userId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.UserTwoFactor_Enable", new { UserID = userId }, commandType: CommandType.StoredProcedure); } public async Task DisableTwoFactorAsync(int userId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.UserTwoFactor_Disable", new { UserID = userId }, commandType: CommandType.StoredProcedure); } public async Task SaveRecoveryCodesAsync(int userId, string recoveryCodes) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.UserTwoFactor_SaveRecoveryCodes", new { UserID = userId, RecoveryCodes = recoveryCodes }, commandType: CommandType.StoredProcedure); } public async Task GetRecoveryCodesAsync(int userId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.UserTwoFactor_GetRecoveryCodes", new { UserID = userId }, commandType: CommandType.StoredProcedure); } public async Task UpdateLastUsedTimeStepAsync(int userId, long lastUsedTimeStep) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.UserTwoFactor_UpdateLastUsedTimeStep", new { UserID = userId, LastUsedTimeStep = lastUsedTimeStep }, commandType: CommandType.StoredProcedure); } public async Task InsertLoginAuditAsync(UserLoginAudit audit) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.User_InsertLoginAudit", new { audit.UserID, audit.EmailAttempted, audit.IpAddress, audit.UserAgent, audit.WasSuccessful, audit.Reason }, commandType: CommandType.StoredProcedure); } }