Format infrastructure dir

This commit is contained in:
Aaron Po
2026-02-12 01:10:04 -05:00
parent f48b8452d3
commit 74c5528ea2
16 changed files with 428 additions and 240 deletions

View File

@@ -3,4 +3,4 @@ namespace Service.Core.Jwt;
public interface IJwtService public interface IJwtService
{ {
string GenerateJwt(Guid userId, string username, DateTime expiry); string GenerateJwt(Guid userId, string username, DateTime expiry);
} }

View File

@@ -1,13 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>Service.Core.Jwt</RootNamespace> <RootNamespace>Service.Core.Jwt</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.2.1" /> <PackageReference
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1" /> Include="Microsoft.IdentityModel.JsonWebTokens"
</ItemGroup> Version="8.2.1"
/>
<PackageReference
Include="System.IdentityModel.Tokens.Jwt"
Version="8.2.1"
/>
</ItemGroup>
</Project> </Project>

View File

@@ -5,21 +5,27 @@ using Microsoft.IdentityModel.Tokens;
using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames; using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames;
namespace Service.Core.Jwt; namespace Service.Core.Jwt;
public class JwtService : IJwtService public class JwtService : IJwtService
{ {
private readonly string? _secret = Environment.GetEnvironmentVariable("JWT_SECRET"); private readonly string? _secret = Environment.GetEnvironmentVariable(
"JWT_SECRET"
);
public string GenerateJwt(Guid userId, string username, DateTime expiry) public string GenerateJwt(Guid userId, string username, DateTime expiry)
{ {
var handler = new JsonWebTokenHandler(); var handler = new JsonWebTokenHandler();
var key = Encoding.UTF8.GetBytes(_secret ?? throw new InvalidOperationException("secret not set")); var key = Encoding.UTF8.GetBytes(
_secret ?? throw new InvalidOperationException("secret not set")
);
// Base claims (always present) // Base claims (always present)
var claims = new List<Claim> var claims = new List<Claim>
{ {
new(JwtRegisteredClaimNames.Sub, userId.ToString()), new(JwtRegisteredClaimNames.Sub, userId.ToString()),
new(JwtRegisteredClaimNames.UniqueName, username), new(JwtRegisteredClaimNames.UniqueName, username),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
}; };
var tokenDescriptor = new SecurityTokenDescriptor var tokenDescriptor = new SecurityTokenDescriptor
@@ -28,7 +34,8 @@ public class JwtService : IJwtService
Expires = expiry, Expires = expiry,
SigningCredentials = new SigningCredentials( SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key), new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256) SecurityAlgorithms.HmacSha256
),
}; };
return handler.CreateToken(tokenDescriptor); return handler.CreateToken(tokenDescriptor);

View File

@@ -4,4 +4,4 @@ public interface IPasswordService
{ {
public string Hash(string password); public string Hash(string password);
public bool Verify(string password, string stored); public bool Verify(string password, string stored);
} }

View File

@@ -1,12 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>Service.Core.Password</RootNamespace> <RootNamespace>Service.Core.Password</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.1" /> <PackageReference
</ItemGroup> Include="Konscious.Security.Cryptography.Argon2"
Version="1.3.1"
/>
</ItemGroup>
</Project> </Project>

View File

@@ -19,7 +19,7 @@ public class PasswordService : IPasswordService
Salt = salt, Salt = salt,
DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1), DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1),
MemorySize = ArgonMemoryKb, MemorySize = ArgonMemoryKb,
Iterations = ArgonIterations Iterations = ArgonIterations,
}; };
var hash = argon2.GetBytes(HashSize); var hash = argon2.GetBytes(HashSize);
@@ -30,8 +30,12 @@ public class PasswordService : IPasswordService
{ {
try try
{ {
var parts = stored.Split(':', StringSplitOptions.RemoveEmptyEntries); var parts = stored.Split(
if (parts.Length != 2) return false; ':',
StringSplitOptions.RemoveEmptyEntries
);
if (parts.Length != 2)
return false;
var salt = Convert.FromBase64String(parts[0]); var salt = Convert.FromBase64String(parts[0]);
var expected = Convert.FromBase64String(parts[1]); var expected = Convert.FromBase64String(parts[1]);
@@ -41,7 +45,7 @@ public class PasswordService : IPasswordService
Salt = salt, Salt = salt,
DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1), DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1),
MemorySize = ArgonMemoryKb, MemorySize = ArgonMemoryKb,
Iterations = ArgonIterations Iterations = ArgonIterations,
}; };
var actual = argon2.GetBytes(expected.Length); var actual = argon2.GetBytes(expected.Length);
@@ -52,4 +56,4 @@ public class PasswordService : IPasswordService
return false; return false;
} }
} }
} }

View File

@@ -5,13 +5,12 @@ using Repository.Core.Sql;
namespace Repository.Core.Repositories.Auth namespace Repository.Core.Repositories.Auth
{ {
public class AuthRepository : Repository<Domain.Core.Entities.UserAccount>, IAuthRepository public class AuthRepository
: Repository<Domain.Core.Entities.UserAccount>,
IAuthRepository
{ {
public AuthRepository(ISqlConnectionFactory connectionFactory) public AuthRepository(ISqlConnectionFactory connectionFactory)
: base(connectionFactory) : base(connectionFactory) { }
{
}
public async Task<Domain.Core.Entities.UserAccount> RegisterUserAsync( public async Task<Domain.Core.Entities.UserAccount> RegisterUserAsync(
string username, string username,
@@ -19,7 +18,8 @@ namespace Repository.Core.Repositories.Auth
string lastName, string lastName,
string email, string email,
DateTime dateOfBirth, DateTime dateOfBirth,
string passwordHash) string passwordHash
)
{ {
await using var connection = await CreateConnection(); await using var connection = await CreateConnection();
await using var command = connection.CreateCommand(); await using var command = connection.CreateCommand();
@@ -45,12 +45,13 @@ namespace Repository.Core.Repositories.Auth
LastName = lastName, LastName = lastName,
Email = email, Email = email,
DateOfBirth = dateOfBirth, DateOfBirth = dateOfBirth,
CreatedAt = DateTime.UtcNow CreatedAt = DateTime.UtcNow,
}; };
} }
public async Task<Domain.Core.Entities.UserAccount?> GetUserByEmailAsync(
public async Task<Domain.Core.Entities.UserAccount?> GetUserByEmailAsync(string email) string email
)
{ {
await using var connection = await CreateConnection(); await using var connection = await CreateConnection();
await using var command = connection.CreateCommand(); await using var command = connection.CreateCommand();
@@ -63,8 +64,9 @@ namespace Repository.Core.Repositories.Auth
return await reader.ReadAsync() ? MapToEntity(reader) : null; return await reader.ReadAsync() ? MapToEntity(reader) : null;
} }
public async Task<Domain.Core.Entities.UserAccount?> GetUserByUsernameAsync(
public async Task<Domain.Core.Entities.UserAccount?> GetUserByUsernameAsync(string username) string username
)
{ {
await using var connection = await CreateConnection(); await using var connection = await CreateConnection();
await using var command = connection.CreateCommand(); await using var command = connection.CreateCommand();
@@ -77,7 +79,9 @@ namespace Repository.Core.Repositories.Auth
return await reader.ReadAsync() ? MapToEntity(reader) : null; return await reader.ReadAsync() ? MapToEntity(reader) : null;
} }
public async Task<UserCredential?> GetActiveCredentialByUserAccountIdAsync(Guid userAccountId) public async Task<UserCredential?> GetActiveCredentialByUserAccountIdAsync(
Guid userAccountId
)
{ {
await using var connection = await CreateConnection(); await using var connection = await CreateConnection();
await using var command = connection.CreateCommand(); await using var command = connection.CreateCommand();
@@ -87,10 +91,15 @@ namespace Repository.Core.Repositories.Auth
AddParameter(command, "@UserAccountId", userAccountId); AddParameter(command, "@UserAccountId", userAccountId);
await using var reader = await command.ExecuteReaderAsync(); await using var reader = await command.ExecuteReaderAsync();
return await reader.ReadAsync() ? MapToCredentialEntity(reader) : null; return await reader.ReadAsync()
? MapToCredentialEntity(reader)
: null;
} }
public async Task RotateCredentialAsync(Guid userAccountId, string newPasswordHash) public async Task RotateCredentialAsync(
Guid userAccountId,
string newPasswordHash
)
{ {
await using var connection = await CreateConnection(); await using var connection = await CreateConnection();
await using var command = connection.CreateCommand(); await using var command = connection.CreateCommand();
@@ -106,11 +115,15 @@ namespace Repository.Core.Repositories.Auth
/// <summary> /// <summary>
/// Maps a data reader row to a UserAccount entity. /// Maps a data reader row to a UserAccount entity.
/// </summary> /// </summary>
protected override Domain.Core.Entities.UserAccount MapToEntity(DbDataReader reader) protected override Domain.Core.Entities.UserAccount MapToEntity(
DbDataReader reader
)
{ {
return new Domain.Core.Entities.UserAccount return new Domain.Core.Entities.UserAccount
{ {
UserAccountId = reader.GetGuid(reader.GetOrdinal("UserAccountId")), UserAccountId = reader.GetGuid(
reader.GetOrdinal("UserAccountId")
),
Username = reader.GetString(reader.GetOrdinal("Username")), Username = reader.GetString(reader.GetOrdinal("Username")),
FirstName = reader.GetString(reader.GetOrdinal("FirstName")), FirstName = reader.GetString(reader.GetOrdinal("FirstName")),
LastName = reader.GetString(reader.GetOrdinal("LastName")), LastName = reader.GetString(reader.GetOrdinal("LastName")),
@@ -119,10 +132,12 @@ namespace Repository.Core.Repositories.Auth
UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt")) UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt"))
? null ? null
: reader.GetDateTime(reader.GetOrdinal("UpdatedAt")), : reader.GetDateTime(reader.GetOrdinal("UpdatedAt")),
DateOfBirth = reader.GetDateTime(reader.GetOrdinal("DateOfBirth")), DateOfBirth = reader.GetDateTime(
reader.GetOrdinal("DateOfBirth")
),
Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) Timer = reader.IsDBNull(reader.GetOrdinal("Timer"))
? null ? null
: (byte[])reader["Timer"] : (byte[])reader["Timer"],
}; };
} }
@@ -133,22 +148,34 @@ namespace Repository.Core.Repositories.Auth
{ {
var entity = new UserCredential var entity = new UserCredential
{ {
UserCredentialId = reader.GetGuid(reader.GetOrdinal("UserCredentialId")), UserCredentialId = reader.GetGuid(
UserAccountId = reader.GetGuid(reader.GetOrdinal("UserAccountId")), reader.GetOrdinal("UserCredentialId")
),
UserAccountId = reader.GetGuid(
reader.GetOrdinal("UserAccountId")
),
Hash = reader.GetString(reader.GetOrdinal("Hash")), Hash = reader.GetString(reader.GetOrdinal("Hash")),
CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")) CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")),
}; };
// Optional columns // Optional columns
var hasTimer = reader.GetSchemaTable()?.Rows var hasTimer =
.Cast<System.Data.DataRow>() reader
.Any(r => string.Equals(r["ColumnName"]?.ToString(), "Timer", .GetSchemaTable()
StringComparison.OrdinalIgnoreCase)) ?? ?.Rows.Cast<System.Data.DataRow>()
false; .Any(r =>
string.Equals(
r["ColumnName"]?.ToString(),
"Timer",
StringComparison.OrdinalIgnoreCase
)
) ?? false;
if (hasTimer) if (hasTimer)
{ {
entity.Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) ? null : (byte[])reader["Timer"]; entity.Timer = reader.IsDBNull(reader.GetOrdinal("Timer"))
? null
: (byte[])reader["Timer"];
} }
return entity; return entity;
@@ -157,7 +184,11 @@ namespace Repository.Core.Repositories.Auth
/// <summary> /// <summary>
/// Helper method to add a parameter to a database command. /// Helper method to add a parameter to a database command.
/// </summary> /// </summary>
private static void AddParameter(DbCommand command, string name, object? value) private static void AddParameter(
DbCommand command,
string name,
object? value
)
{ {
var p = command.CreateParameter(); var p = command.CreateParameter();
p.ParameterName = name; p.ParameterName = name;

View File

@@ -24,7 +24,8 @@ namespace Repository.Core.Repositories.Auth
string lastName, string lastName,
string email, string email,
DateTime dateOfBirth, DateTime dateOfBirth,
string passwordHash); string passwordHash
);
/// <summary> /// <summary>
/// Retrieves a user account by email address (typically used for login). /// Retrieves a user account by email address (typically used for login).
@@ -32,7 +33,9 @@ namespace Repository.Core.Repositories.Auth
/// </summary> /// </summary>
/// <param name="email">Email address to search for</param> /// <param name="email">Email address to search for</param>
/// <returns>UserAccount if found, null otherwise</returns> /// <returns>UserAccount if found, null otherwise</returns>
Task<Domain.Core.Entities.UserAccount?> GetUserByEmailAsync(string email); Task<Domain.Core.Entities.UserAccount?> GetUserByEmailAsync(
string email
);
/// <summary> /// <summary>
/// Retrieves a user account by username (typically used for login). /// Retrieves a user account by username (typically used for login).
@@ -40,7 +43,9 @@ namespace Repository.Core.Repositories.Auth
/// </summary> /// </summary>
/// <param name="username">Username to search for</param> /// <param name="username">Username to search for</param>
/// <returns>UserAccount if found, null otherwise</returns> /// <returns>UserAccount if found, null otherwise</returns>
Task<Domain.Core.Entities.UserAccount?> GetUserByUsernameAsync(string username); Task<Domain.Core.Entities.UserAccount?> GetUserByUsernameAsync(
string username
);
/// <summary> /// <summary>
/// Retrieves the active (non-revoked) credential for a user account. /// Retrieves the active (non-revoked) credential for a user account.
@@ -48,7 +53,9 @@ namespace Repository.Core.Repositories.Auth
/// </summary> /// </summary>
/// <param name="userAccountId">ID of the user account</param> /// <param name="userAccountId">ID of the user account</param>
/// <returns>Active UserCredential if found, null otherwise</returns> /// <returns>Active UserCredential if found, null otherwise</returns>
Task<UserCredential?> GetActiveCredentialByUserAccountIdAsync(Guid userAccountId); Task<UserCredential?> GetActiveCredentialByUserAccountIdAsync(
Guid userAccountId
);
/// <summary> /// <summary>
/// Rotates a user's credential by invalidating all existing credentials and creating a new one. /// Rotates a user's credential by invalidating all existing credentials and creating a new one.

View File

@@ -1,15 +1,19 @@
using Domain.Core.Entities; using Domain.Core.Entities;
namespace Repository.Core.Repositories.UserAccount namespace Repository.Core.Repositories.UserAccount
{ {
public interface IUserAccountRepository public interface IUserAccountRepository
{ {
Task<Domain.Core.Entities.UserAccount?> GetByIdAsync(Guid id); Task<Domain.Core.Entities.UserAccount?> GetByIdAsync(Guid id);
Task<IEnumerable<Domain.Core.Entities.UserAccount>> GetAllAsync(int? limit, int? offset); Task<IEnumerable<Domain.Core.Entities.UserAccount>> GetAllAsync(
int? limit,
int? offset
);
Task UpdateAsync(Domain.Core.Entities.UserAccount userAccount); Task UpdateAsync(Domain.Core.Entities.UserAccount userAccount);
Task DeleteAsync(Guid id); Task DeleteAsync(Guid id);
Task<Domain.Core.Entities.UserAccount?> GetByUsernameAsync(string username); Task<Domain.Core.Entities.UserAccount?> GetByUsernameAsync(
string username
);
Task<Domain.Core.Entities.UserAccount?> GetByEmailAsync(string email); Task<Domain.Core.Entities.UserAccount?> GetByEmailAsync(string email);
} }
} }

View File

@@ -6,9 +6,12 @@ using Repository.Core.Sql;
namespace Repository.Core.Repositories.UserAccount namespace Repository.Core.Repositories.UserAccount
{ {
public class UserAccountRepository(ISqlConnectionFactory connectionFactory) public class UserAccountRepository(ISqlConnectionFactory connectionFactory)
: Repository<Domain.Core.Entities.UserAccount>(connectionFactory), IUserAccountRepository : Repository<Domain.Core.Entities.UserAccount>(connectionFactory),
IUserAccountRepository
{ {
public async Task<Domain.Core.Entities.UserAccount?> GetByIdAsync(Guid id) public async Task<Domain.Core.Entities.UserAccount?> GetByIdAsync(
Guid id
)
{ {
await using var connection = await CreateConnection(); await using var connection = await CreateConnection();
await using var command = connection.CreateCommand(); await using var command = connection.CreateCommand();
@@ -21,7 +24,9 @@ namespace Repository.Core.Repositories.UserAccount
return await reader.ReadAsync() ? MapToEntity(reader) : null; return await reader.ReadAsync() ? MapToEntity(reader) : null;
} }
public async Task<IEnumerable<Domain.Core.Entities.UserAccount>> GetAllAsync(int? limit, int? offset) public async Task<
IEnumerable<Domain.Core.Entities.UserAccount>
> GetAllAsync(int? limit, int? offset)
{ {
await using var connection = await CreateConnection(); await using var connection = await CreateConnection();
await using var command = connection.CreateCommand(); await using var command = connection.CreateCommand();
@@ -45,7 +50,9 @@ namespace Repository.Core.Repositories.UserAccount
return users; return users;
} }
public async Task UpdateAsync(Domain.Core.Entities.UserAccount userAccount) public async Task UpdateAsync(
Domain.Core.Entities.UserAccount userAccount
)
{ {
await using var connection = await CreateConnection(); await using var connection = await CreateConnection();
await using var command = connection.CreateCommand(); await using var command = connection.CreateCommand();
@@ -73,7 +80,9 @@ namespace Repository.Core.Repositories.UserAccount
await command.ExecuteNonQueryAsync(); await command.ExecuteNonQueryAsync();
} }
public async Task<Domain.Core.Entities.UserAccount?> GetByUsernameAsync(string username) public async Task<Domain.Core.Entities.UserAccount?> GetByUsernameAsync(
string username
)
{ {
await using var connection = await CreateConnection(); await using var connection = await CreateConnection();
await using var command = connection.CreateCommand(); await using var command = connection.CreateCommand();
@@ -86,7 +95,9 @@ namespace Repository.Core.Repositories.UserAccount
return await reader.ReadAsync() ? MapToEntity(reader) : null; return await reader.ReadAsync() ? MapToEntity(reader) : null;
} }
public async Task<Domain.Core.Entities.UserAccount?> GetByEmailAsync(string email) public async Task<Domain.Core.Entities.UserAccount?> GetByEmailAsync(
string email
)
{ {
await using var connection = await CreateConnection(); await using var connection = await CreateConnection();
await using var command = connection.CreateCommand(); await using var command = connection.CreateCommand();
@@ -99,11 +110,15 @@ namespace Repository.Core.Repositories.UserAccount
return await reader.ReadAsync() ? MapToEntity(reader) : null; return await reader.ReadAsync() ? MapToEntity(reader) : null;
} }
protected override Domain.Core.Entities.UserAccount MapToEntity(DbDataReader reader) protected override Domain.Core.Entities.UserAccount MapToEntity(
DbDataReader reader
)
{ {
return new Domain.Core.Entities.UserAccount return new Domain.Core.Entities.UserAccount
{ {
UserAccountId = reader.GetGuid(reader.GetOrdinal("UserAccountId")), UserAccountId = reader.GetGuid(
reader.GetOrdinal("UserAccountId")
),
Username = reader.GetString(reader.GetOrdinal("Username")), Username = reader.GetString(reader.GetOrdinal("Username")),
FirstName = reader.GetString(reader.GetOrdinal("FirstName")), FirstName = reader.GetString(reader.GetOrdinal("FirstName")),
LastName = reader.GetString(reader.GetOrdinal("LastName")), LastName = reader.GetString(reader.GetOrdinal("LastName")),
@@ -112,14 +127,20 @@ namespace Repository.Core.Repositories.UserAccount
UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt")) UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt"))
? null ? null
: reader.GetDateTime(reader.GetOrdinal("UpdatedAt")), : reader.GetDateTime(reader.GetOrdinal("UpdatedAt")),
DateOfBirth = reader.GetDateTime(reader.GetOrdinal("DateOfBirth")), DateOfBirth = reader.GetDateTime(
reader.GetOrdinal("DateOfBirth")
),
Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) Timer = reader.IsDBNull(reader.GetOrdinal("Timer"))
? null ? null
: (byte[])reader["Timer"] : (byte[])reader["Timer"],
}; };
} }
private static void AddParameter(DbCommand command, string name, object? value) private static void AddParameter(
DbCommand command,
string name,
object? value
)
{ {
var p = command.CreateParameter(); var p = command.CreateParameter();
p.ParameterName = name; p.ParameterName = name;

View File

@@ -12,7 +12,10 @@
Version="160.1000.6" Version="160.1000.6"
/> />
<PackageReference Include="System.Data.SqlClient" Version="4.9.0" /> <PackageReference Include="System.Data.SqlClient" Version="4.9.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" /> <PackageReference
Include="Microsoft.Extensions.Configuration.Abstractions"
Version="8.0.0"
/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\Domain\Domain.csproj" /> <ProjectReference Include="..\..\..\Domain\Domain.csproj" />

View File

@@ -2,17 +2,21 @@ using System.Data.Common;
using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
namespace Repository.Core.Sql namespace Repository.Core.Sql
{ {
public class DefaultSqlConnectionFactory(IConfiguration configuration) : ISqlConnectionFactory public class DefaultSqlConnectionFactory(IConfiguration configuration)
: ISqlConnectionFactory
{ {
private readonly string _connectionString = GetConnectionString(configuration); private readonly string _connectionString = GetConnectionString(
configuration
);
private static string GetConnectionString(IConfiguration configuration) private static string GetConnectionString(IConfiguration configuration)
{ {
// Check for full connection string first // Check for full connection string first
var fullConnectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING"); var fullConnectionString = Environment.GetEnvironmentVariable(
"DB_CONNECTION_STRING"
);
if (!string.IsNullOrEmpty(fullConnectionString)) if (!string.IsNullOrEmpty(fullConnectionString))
{ {
return fullConnectionString; return fullConnectionString;

View File

@@ -12,18 +12,30 @@ namespace Repository.Core.Sql
/// <returns>A properly formatted SQL Server connection string.</returns> /// <returns>A properly formatted SQL Server connection string.</returns>
public static string BuildConnectionString(string? databaseName = null) public static string BuildConnectionString(string? databaseName = null)
{ {
var server = Environment.GetEnvironmentVariable("DB_SERVER") var server =
?? throw new InvalidOperationException("DB_SERVER environment variable is not set"); Environment.GetEnvironmentVariable("DB_SERVER")
?? throw new InvalidOperationException(
"DB_SERVER environment variable is not set"
);
var dbName = databaseName var dbName =
databaseName
?? Environment.GetEnvironmentVariable("DB_NAME") ?? Environment.GetEnvironmentVariable("DB_NAME")
?? throw new InvalidOperationException("DB_NAME environment variable is not set"); ?? throw new InvalidOperationException(
"DB_NAME environment variable is not set"
);
var user = Environment.GetEnvironmentVariable("DB_USER") var user =
?? throw new InvalidOperationException("DB_USER environment variable is not set"); Environment.GetEnvironmentVariable("DB_USER")
?? throw new InvalidOperationException(
"DB_USER environment variable is not set"
);
var password = Environment.GetEnvironmentVariable("DB_PASSWORD") var password =
?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set"); Environment.GetEnvironmentVariable("DB_PASSWORD")
?? throw new InvalidOperationException(
"DB_PASSWORD environment variable is not set"
);
var builder = new SqlConnectionStringBuilder var builder = new SqlConnectionStringBuilder
{ {
@@ -32,7 +44,7 @@ namespace Repository.Core.Sql
UserID = user, UserID = user,
Password = password, Password = password,
TrustServerCertificate = true, TrustServerCertificate = true,
Encrypt = true Encrypt = true,
}; };
return builder.ConnectionString; return builder.ConnectionString;

View File

@@ -1,15 +1,15 @@
using Apps72.Dev.Data.DbMocker;
using Repository.Core.Repositories.Auth;
using FluentAssertions;
using Repository.Tests.Database;
using System.Data; using System.Data;
using Apps72.Dev.Data.DbMocker;
using FluentAssertions;
using Repository.Core.Repositories.Auth;
using Repository.Tests.Database;
namespace Repository.Tests.Auth; namespace Repository.Tests.Auth;
public class AuthRepositoryTest public class AuthRepositoryTest
{ {
private static AuthRepository CreateRepo(MockDbConnection conn) private static AuthRepository CreateRepo(MockDbConnection conn) =>
=> new(new TestConnectionFactory(conn)); new(new TestConnectionFactory(conn));
[Fact] [Fact]
public async Task RegisterUserAsync_CreatesUserWithCredential_ReturnsUserAccount() public async Task RegisterUserAsync_CreatesUserWithCredential_ReturnsUserAccount()
@@ -17,10 +17,12 @@ public class AuthRepositoryTest
var expectedUserId = Guid.NewGuid(); var expectedUserId = Guid.NewGuid();
var conn = new MockDbConnection(); var conn = new MockDbConnection();
conn.Mocks conn.Mocks.When(cmd => cmd.CommandText == "USP_RegisterUser")
.When(cmd => cmd.CommandText == "USP_RegisterUser") .ReturnsTable(
.ReturnsTable(MockTable.WithColumns(("UserAccountId", typeof(Guid))) MockTable
.AddRow(expectedUserId)); .WithColumns(("UserAccountId", typeof(Guid)))
.AddRow(expectedUserId)
);
var repo = CreateRepo(conn); var repo = CreateRepo(conn);
var result = await repo.RegisterUserAsync( var result = await repo.RegisterUserAsync(
@@ -47,29 +49,32 @@ public class AuthRepositoryTest
var userId = Guid.NewGuid(); var userId = Guid.NewGuid();
var conn = new MockDbConnection(); var conn = new MockDbConnection();
conn.Mocks conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail") .ReturnsTable(
.ReturnsTable(MockTable.WithColumns( MockTable
("UserAccountId", typeof(Guid)), .WithColumns(
("Username", typeof(string)), ("UserAccountId", typeof(Guid)),
("FirstName", typeof(string)), ("Username", typeof(string)),
("LastName", typeof(string)), ("FirstName", typeof(string)),
("Email", typeof(string)), ("LastName", typeof(string)),
("CreatedAt", typeof(DateTime)), ("Email", typeof(string)),
("UpdatedAt", typeof(DateTime?)), ("CreatedAt", typeof(DateTime)),
("DateOfBirth", typeof(DateTime)), ("UpdatedAt", typeof(DateTime?)),
("Timer", typeof(byte[])) ("DateOfBirth", typeof(DateTime)),
).AddRow( ("Timer", typeof(byte[]))
userId, )
"emailuser", .AddRow(
"Email", userId,
"User", "emailuser",
"emailuser@example.com", "Email",
DateTime.UtcNow, "User",
null, "emailuser@example.com",
new DateTime(1990, 5, 15), DateTime.UtcNow,
null null,
)); new DateTime(1990, 5, 15),
null
)
);
var repo = CreateRepo(conn); var repo = CreateRepo(conn);
var result = await repo.GetUserByEmailAsync("emailuser@example.com"); var result = await repo.GetUserByEmailAsync("emailuser@example.com");
@@ -87,8 +92,7 @@ public class AuthRepositoryTest
{ {
var conn = new MockDbConnection(); var conn = new MockDbConnection();
conn.Mocks conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
.ReturnsTable(MockTable.Empty()); .ReturnsTable(MockTable.Empty());
var repo = CreateRepo(conn); var repo = CreateRepo(conn);
@@ -103,29 +107,34 @@ public class AuthRepositoryTest
var userId = Guid.NewGuid(); var userId = Guid.NewGuid();
var conn = new MockDbConnection(); var conn = new MockDbConnection();
conn.Mocks conn.Mocks.When(cmd =>
.When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername") cmd.CommandText == "usp_GetUserAccountByUsername"
.ReturnsTable(MockTable.WithColumns( )
("UserAccountId", typeof(Guid)), .ReturnsTable(
("Username", typeof(string)), MockTable
("FirstName", typeof(string)), .WithColumns(
("LastName", typeof(string)), ("UserAccountId", typeof(Guid)),
("Email", typeof(string)), ("Username", typeof(string)),
("CreatedAt", typeof(DateTime)), ("FirstName", typeof(string)),
("UpdatedAt", typeof(DateTime?)), ("LastName", typeof(string)),
("DateOfBirth", typeof(DateTime)), ("Email", typeof(string)),
("Timer", typeof(byte[])) ("CreatedAt", typeof(DateTime)),
).AddRow( ("UpdatedAt", typeof(DateTime?)),
userId, ("DateOfBirth", typeof(DateTime)),
"usernameuser", ("Timer", typeof(byte[]))
"Username", )
"User", .AddRow(
"username@example.com", userId,
DateTime.UtcNow, "usernameuser",
null, "Username",
new DateTime(1985, 8, 20), "User",
null "username@example.com",
)); DateTime.UtcNow,
null,
new DateTime(1985, 8, 20),
null
)
);
var repo = CreateRepo(conn); var repo = CreateRepo(conn);
var result = await repo.GetUserByUsernameAsync("usernameuser"); var result = await repo.GetUserByUsernameAsync("usernameuser");
@@ -141,8 +150,9 @@ public class AuthRepositoryTest
{ {
var conn = new MockDbConnection(); var conn = new MockDbConnection();
conn.Mocks conn.Mocks.When(cmd =>
.When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername") cmd.CommandText == "usp_GetUserAccountByUsername"
)
.ReturnsTable(MockTable.Empty()); .ReturnsTable(MockTable.Empty());
var repo = CreateRepo(conn); var repo = CreateRepo(conn);
@@ -158,21 +168,26 @@ public class AuthRepositoryTest
var credentialId = Guid.NewGuid(); var credentialId = Guid.NewGuid();
var conn = new MockDbConnection(); var conn = new MockDbConnection();
conn.Mocks conn.Mocks.When(cmd =>
.When(cmd => cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId") cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId"
.ReturnsTable(MockTable.WithColumns( )
("UserCredentialId", typeof(Guid)), .ReturnsTable(
("UserAccountId", typeof(Guid)), MockTable
("Hash", typeof(string)), .WithColumns(
("CreatedAt", typeof(DateTime)), ("UserCredentialId", typeof(Guid)),
("Timer", typeof(byte[])) ("UserAccountId", typeof(Guid)),
).AddRow( ("Hash", typeof(string)),
credentialId, ("CreatedAt", typeof(DateTime)),
userId, ("Timer", typeof(byte[]))
"hashed_password_value", )
DateTime.UtcNow, .AddRow(
null credentialId,
)); userId,
"hashed_password_value",
DateTime.UtcNow,
null
)
);
var repo = CreateRepo(conn); var repo = CreateRepo(conn);
var result = await repo.GetActiveCredentialByUserAccountIdAsync(userId); var result = await repo.GetActiveCredentialByUserAccountIdAsync(userId);
@@ -189,8 +204,9 @@ public class AuthRepositoryTest
var userId = Guid.NewGuid(); var userId = Guid.NewGuid();
var conn = new MockDbConnection(); var conn = new MockDbConnection();
conn.Mocks conn.Mocks.When(cmd =>
.When(cmd => cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId") cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId"
)
.ReturnsTable(MockTable.Empty()); .ReturnsTable(MockTable.Empty());
var repo = CreateRepo(conn); var repo = CreateRepo(conn);
@@ -206,14 +222,14 @@ public class AuthRepositoryTest
var newPasswordHash = "new_hashed_password"; var newPasswordHash = "new_hashed_password";
var conn = new MockDbConnection(); var conn = new MockDbConnection();
conn.Mocks conn.Mocks.When(cmd => cmd.CommandText == "USP_RotateUserCredential")
.When(cmd => cmd.CommandText == "USP_RotateUserCredential")
.ReturnsScalar(1); .ReturnsScalar(1);
var repo = CreateRepo(conn); var repo = CreateRepo(conn);
// Should not throw // Should not throw
var act = async () => await repo.RotateCredentialAsync(userId, newPasswordHash); var act = async () =>
await repo.RotateCredentialAsync(userId, newPasswordHash);
await act.Should().NotThrowAsync(); await act.Should().NotThrowAsync();
} }
} }

View File

@@ -15,9 +15,18 @@
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="FluentAssertions" Version="6.9.0" /> <PackageReference Include="FluentAssertions" Version="6.9.0" />
<PackageReference Include="DbMocker" Version="1.26.0" /> <PackageReference Include="DbMocker" Version="1.26.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" /> <PackageReference
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" /> Include="Microsoft.Extensions.Configuration"
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.0" /> Version="9.0.0"
/>
<PackageReference
Include="Microsoft.Extensions.Configuration.Abstractions"
Version="9.0.0"
/>
<PackageReference
Include="Microsoft.Extensions.Configuration.Binder"
Version="9.0.0"
/>
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" /> <PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
</ItemGroup> </ItemGroup>
@@ -28,4 +37,4 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Repository.Core\Repository.Core.csproj" /> <ProjectReference Include="..\Repository.Core\Repository.Core.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,38 +1,50 @@
using Apps72.Dev.Data.DbMocker; using Apps72.Dev.Data.DbMocker;
using Repository.Core.Repositories.UserAccount;
using FluentAssertions; using FluentAssertions;
using Repository.Core.Repositories.UserAccount;
using Repository.Tests.Database; using Repository.Tests.Database;
namespace Repository.Tests.UserAccount; namespace Repository.Tests.UserAccount;
public class UserAccountRepositoryTest public class UserAccountRepositoryTest
{ {
private static UserAccountRepository CreateRepo(MockDbConnection conn) private static UserAccountRepository CreateRepo(MockDbConnection conn) =>
=> new(new TestConnectionFactory(conn)); new(new TestConnectionFactory(conn));
[Fact] [Fact]
public async Task GetByIdAsync_ReturnsRow_Mapped() public async Task GetByIdAsync_ReturnsRow_Mapped()
{ {
var conn = new MockDbConnection(); var conn = new MockDbConnection();
conn.Mocks conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountById")
.When(cmd => cmd.CommandText == "usp_GetUserAccountById") .ReturnsTable(
.ReturnsTable(MockTable.WithColumns( MockTable
("UserAccountId", typeof(Guid)), .WithColumns(
("Username", typeof(string)), ("UserAccountId", typeof(Guid)),
("FirstName", typeof(string)), ("Username", typeof(string)),
("LastName", typeof(string)), ("FirstName", typeof(string)),
("Email", typeof(string)), ("LastName", typeof(string)),
("CreatedAt", typeof(DateTime)), ("Email", typeof(string)),
("UpdatedAt", typeof(DateTime?)), ("CreatedAt", typeof(DateTime)),
("DateOfBirth", typeof(DateTime)), ("UpdatedAt", typeof(DateTime?)),
("Timer", typeof(byte[])) ("DateOfBirth", typeof(DateTime)),
).AddRow(Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), ("Timer", typeof(byte[]))
"yerb", "Aaron", "Po", "aaronpo@example.com", )
new DateTime(2020, 1, 1), null, .AddRow(
new DateTime(1990, 1, 1), null)); Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
"yerb",
"Aaron",
"Po",
"aaronpo@example.com",
new DateTime(2020, 1, 1),
null,
new DateTime(1990, 1, 1),
null
)
);
var repo = CreateRepo(conn); var repo = CreateRepo(conn);
var result = await repo.GetByIdAsync(Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")); var result = await repo.GetByIdAsync(
Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
);
result.Should().NotBeNull(); result.Should().NotBeNull();
result!.Username.Should().Be("yerb"); result!.Username.Should().Be("yerb");
@@ -43,48 +55,85 @@ public class UserAccountRepositoryTest
public async Task GetAllAsync_ReturnsMultipleRows() public async Task GetAllAsync_ReturnsMultipleRows()
{ {
var conn = new MockDbConnection(); var conn = new MockDbConnection();
conn.Mocks conn.Mocks.When(cmd => cmd.CommandText == "usp_GetAllUserAccounts")
.When(cmd => cmd.CommandText == "usp_GetAllUserAccounts") .ReturnsTable(
.ReturnsTable(MockTable.WithColumns( MockTable
("UserAccountId", typeof(Guid)), .WithColumns(
("Username", typeof(string)), ("UserAccountId", typeof(Guid)),
("FirstName", typeof(string)), ("Username", typeof(string)),
("LastName", typeof(string)), ("FirstName", typeof(string)),
("Email", typeof(string)), ("LastName", typeof(string)),
("CreatedAt", typeof(DateTime)), ("Email", typeof(string)),
("UpdatedAt", typeof(DateTime?)), ("CreatedAt", typeof(DateTime)),
("DateOfBirth", typeof(DateTime)), ("UpdatedAt", typeof(DateTime?)),
("Timer", typeof(byte[])) ("DateOfBirth", typeof(DateTime)),
).AddRow(Guid.NewGuid(), "a", "A", "A", "a@example.com", DateTime.UtcNow, null, DateTime.UtcNow.Date, ("Timer", typeof(byte[]))
null) )
.AddRow(Guid.NewGuid(), "b", "B", "B", "b@example.com", DateTime.UtcNow, null, DateTime.UtcNow.Date, .AddRow(
null)); Guid.NewGuid(),
"a",
"A",
"A",
"a@example.com",
DateTime.UtcNow,
null,
DateTime.UtcNow.Date,
null
)
.AddRow(
Guid.NewGuid(),
"b",
"B",
"B",
"b@example.com",
DateTime.UtcNow,
null,
DateTime.UtcNow.Date,
null
)
);
var repo = CreateRepo(conn); var repo = CreateRepo(conn);
var results = (await repo.GetAllAsync(null, null)).ToList(); var results = (await repo.GetAllAsync(null, null)).ToList();
results.Should().HaveCount(2); results.Should().HaveCount(2);
results.Select(r => r.Username).Should().BeEquivalentTo(new[] { "a", "b" }); results
.Select(r => r.Username)
.Should()
.BeEquivalentTo(new[] { "a", "b" });
} }
[Fact] [Fact]
public async Task GetByUsername_ReturnsRow() public async Task GetByUsername_ReturnsRow()
{ {
var conn = new MockDbConnection(); var conn = new MockDbConnection();
conn.Mocks conn.Mocks.When(cmd =>
.When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername") cmd.CommandText == "usp_GetUserAccountByUsername"
.ReturnsTable(MockTable.WithColumns( )
("UserAccountId", typeof(Guid)), .ReturnsTable(
("Username", typeof(string)), MockTable
("FirstName", typeof(string)), .WithColumns(
("LastName", typeof(string)), ("UserAccountId", typeof(Guid)),
("Email", typeof(string)), ("Username", typeof(string)),
("CreatedAt", typeof(DateTime)), ("FirstName", typeof(string)),
("UpdatedAt", typeof(DateTime?)), ("LastName", typeof(string)),
("DateOfBirth", typeof(DateTime)), ("Email", typeof(string)),
("Timer", typeof(byte[])) ("CreatedAt", typeof(DateTime)),
).AddRow(Guid.NewGuid(), "lookupuser", "L", "U", "lookup@example.com", DateTime.UtcNow, null, ("UpdatedAt", typeof(DateTime?)),
DateTime.UtcNow.Date, null)); ("DateOfBirth", typeof(DateTime)),
("Timer", typeof(byte[]))
)
.AddRow(
Guid.NewGuid(),
"lookupuser",
"L",
"U",
"lookup@example.com",
DateTime.UtcNow,
null,
DateTime.UtcNow.Date,
null
)
);
var repo = CreateRepo(conn); var repo = CreateRepo(conn);
var result = await repo.GetByUsernameAsync("lookupuser"); var result = await repo.GetByUsernameAsync("lookupuser");
@@ -96,20 +145,32 @@ public class UserAccountRepositoryTest
public async Task GetByEmail_ReturnsRow() public async Task GetByEmail_ReturnsRow()
{ {
var conn = new MockDbConnection(); var conn = new MockDbConnection();
conn.Mocks conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail") .ReturnsTable(
.ReturnsTable(MockTable.WithColumns( MockTable
("UserAccountId", typeof(Guid)), .WithColumns(
("Username", typeof(string)), ("UserAccountId", typeof(Guid)),
("FirstName", typeof(string)), ("Username", typeof(string)),
("LastName", typeof(string)), ("FirstName", typeof(string)),
("Email", typeof(string)), ("LastName", typeof(string)),
("CreatedAt", typeof(DateTime)), ("Email", typeof(string)),
("UpdatedAt", typeof(DateTime?)), ("CreatedAt", typeof(DateTime)),
("DateOfBirth", typeof(DateTime)), ("UpdatedAt", typeof(DateTime?)),
("Timer", typeof(byte[])) ("DateOfBirth", typeof(DateTime)),
).AddRow(Guid.NewGuid(), "byemail", "B", "E", "byemail@example.com", DateTime.UtcNow, null, ("Timer", typeof(byte[]))
DateTime.UtcNow.Date, null)); )
.AddRow(
Guid.NewGuid(),
"byemail",
"B",
"E",
"byemail@example.com",
DateTime.UtcNow,
null,
DateTime.UtcNow.Date,
null
)
);
var repo = CreateRepo(conn); var repo = CreateRepo(conn);
var result = await repo.GetByEmailAsync("byemail@example.com"); var result = await repo.GetByEmailAsync("byemail@example.com");