diff --git a/.gitignore b/.gitignore
index ecede2b..2dc761b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -483,9 +483,6 @@ FodyWeavers.xsd
*.feature.cs
-
-database
-
.env
.env.dev
.env.test
diff --git a/README.md b/README.md
index f1d8087..5304939 100644
--- a/README.md
+++ b/README.md
@@ -2,46 +2,6 @@
A social platform for craft beer enthusiasts to discover breweries, share reviews, and connect with fellow beer lovers.
-
-## Table of Contents
-
-- [Project Status](#project-status)
-- [Repository Structure](#repository-structure)
-- [Technology Stack](#technology-stack)
-- [Getting Started](#getting-started)
- - [Prerequisites](#prerequisites)
- - [Quick Start (Development Environment)](#quick-start-development-environment)
- - [Manual Setup (Without Docker)](#manual-setup-without-docker)
-- [Environment Variables](#environment-variables)
- - [Overview](#overview)
- - [Backend Variables (.NET API)](#backend-variables-net-api)
- - [Frontend Variables (Next.js)](#frontend-variables-nextjs)
- - [Docker Variables](#docker-variables)
- - [External Services](#external-services)
- - [Generating Secrets](#generating-secrets)
- - [Environment File Structure](#environment-file-structure)
- - [Variable Reference Table](#variable-reference-table)
-- [Testing](#testing)
-- [Database Schema](#database-schema)
-- [Authentication & Security](#authentication--security)
-- [Architecture Patterns](#architecture-patterns)
-- [Docker & Containerization](#docker--containerization)
- - [Container Architecture](#container-architecture)
- - [Docker Compose Environments](#docker-compose-environments)
- - [Service Dependencies](#service-dependencies)
- - [Health Checks](#health-checks)
- - [Volumes](#volumes)
- - [Networks](#networks)
- - [Environment Variables](#environment-variables)
- - [Container Lifecycle](#container-lifecycle)
-- [Docker Tips & Troubleshooting](#docker-tips--troubleshooting)
-- [Roadmap](#roadmap)
-- [License](#license)
-- [Contact & Support](#contact--support)
-
----
-
-
## Project Status
This project is in active development, transitioning from a full-stack Next.js application to a **multi-project monorepo** with:
@@ -49,10 +9,10 @@ This project is in active development, transitioning from a full-stack Next.js a
- **Frontend**: Next.js with TypeScript
- **Architecture**: SQL-first approach using stored procedures
-**Current State** (February 2025):
+**Current State** (February 2026):
- Core authentication and user management APIs functional
- Database schema and migrations established
-- Repository and service layers implemented
+- Domain, Infrastructure, Repository, and Service layers implemented
- Frontend integration with .NET API in progress
- Migrating remaining features from Next.js serverless functions
@@ -65,18 +25,26 @@ This project is in active development, transitioning from a full-stack Next.js a
```
src/Core/
├── API/
-│ ├── API.Core/ # ASP.NET Core Web API with Swagger/OpenAPI
-│ └── API.Specs/ # Integration tests using Reqnroll (BDD)
+│ ├── API.Core/ # ASP.NET Core Web API with Swagger/OpenAPI
+│ └── API.Specs/ # Integration tests using Reqnroll (BDD)
├── Database/
-│ ├── Database.Migrations/ # DbUp migrations (embedded SQL scripts)
-│ └── Database.Seed/ # Database seeding for development/testing
-├── Repository/
-│ ├── Repository.Core/ # Data access layer (stored procedure-based)
-│ └── Repository.Tests/ # Unit tests for repositories
+│ ├── Database.Migrations/ # DbUp migrations (embedded SQL scripts)
+│ └── Database.Seed/ # Database seeding for development/testing
+├── Domain/
+│ └── Domain.csproj # Domain entities and models
+│ └── Entities/ # Core domain entities (UserAccount, UserCredential, etc.)
+├── Infrastructure/
+│ ├── Infrastructure.Jwt/ # JWT token generation and validation
+│ ├── Infrastructure.PasswordHashing/ # Argon2id password hashing
+│ └── Infrastructure.Repository/
+│ ├── Infrastructure.Repository/ # Data access layer (stored procedure-based)
+│ └── Infrastructure.Repository.Tests/ # Unit tests for repositories
└── Service/
- └── Service.Core/ # Business logic layer
+ └── Service.Core/ # Business logic layer
-Website/ # Next.js frontend application
+Website/ # Next.js frontend application
+misc/
+└── raw-data/ # Sample data files (breweries, beers)
```
### Key Components
@@ -86,6 +54,7 @@ Website/ # Next.js frontend application
- Controllers: `AuthController`, `UserController`
- Configured with Swagger UI for API exploration
- Health checks and structured logging
+- Middleware for error handling and request processing
**Database Layer**
- SQL Server with stored procedures for all data operations
@@ -93,7 +62,19 @@ Website/ # Next.js frontend application
- Comprehensive schema including users, breweries, beers, locations, and social features
- Seeders for development data (users, locations across US/Canada/Mexico)
-**Repository Layer** (`Repository.Core`)
+**Domain Layer** (`Domain`)
+- Core business entities and models
+- Entities: `UserAccount`, `UserCredential`, `UserVerification`
+- Shared domain logic and value objects
+- No external dependencies - pure domain model
+
+**Infrastructure Layer**
+- **Infrastructure.Jwt**: JWT token generation, validation, and configuration
+- **Infrastructure.PasswordHashing**: Argon2id password hashing with configurable parameters
+- **Infrastructure.Password**: Password utilities and validation
+- **Infrastructure.Repository**: Repository pattern infrastructure and base classes
+
+**Repository Layer** (`Infrastructure.Repository`)
- Abstraction over SQL Server using ADO.NET
- `ISqlConnectionFactory` for connection management
- Repositories: `AuthRepository`, `UserAccountRepository`
@@ -101,9 +82,9 @@ Website/ # Next.js frontend application
**Service Layer** (`Service.Core`)
- Business logic and orchestration
-- Services: `AuthService`, `UserService`, `JwtService`
-- Password hashing with Argon2id
-- JWT token generation
+- Services: `AuthService`, `UserService`
+- Integration with infrastructure components
+- Transaction management and business rule enforcement
**Frontend** (`Website`)
- Next.js 14+ with TypeScript
@@ -334,7 +315,7 @@ Provide a complete SQL Server connection string:
DB_CONNECTION_STRING="Server=localhost,1433;Database=Biergarten;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True;"
```
-The connection factory checks for `DB_CONNECTION_STRING` first, then falls back to building from components. See [DefaultSqlConnectionFactory.cs](src/Core/Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs).
+The connection factory checks for `DB_CONNECTION_STRING` first, then falls back to building from components. See [DefaultSqlConnectionFactory.cs](src/Core/Infrastructure/Infrastructure.Repository/Infrastructure.Repository/Sql/DefaultSqlConnectionFactory.cs).
#### JWT Authentication
@@ -578,7 +559,7 @@ docker compose -f docker-compose.test.yaml up --abort-on-container-exit
This runs:
- **API.Specs** - BDD integration tests
-- **Repository.Tests** - Unit tests for data access
+- **Infrastructure.Repository.Tests** - Unit tests for data access
Test results are output to `./test-results/`.
@@ -590,10 +571,10 @@ cd src/Core
dotnet test API/API.Specs/API.Specs.csproj
```
-**Unit Tests (Repository.Tests)**
+**Unit Tests (Infrastructure.Repository.Tests)**
```bash
cd src/Core
-dotnet test Repository/Repository.Tests/Repository.Tests.csproj
+dotnet test Infrastructure/Infrastructure.Repository/Infrastructure.Repository.Tests/Repository.Tests.csproj
```
### Test Features
diff --git a/docker-compose.test.yaml b/docker-compose.test.yaml
index 17b2cf2..06aff2f 100644
--- a/docker-compose.test.yaml
+++ b/docker-compose.test.yaml
@@ -101,7 +101,7 @@ services:
condition: service_completed_successfully
build:
context: ./src/Core
- dockerfile: Repository/Repository.Tests/Dockerfile
+ dockerfile: Infrastructure/Infrastructure.Repository.Tests/Dockerfile
args:
BUILD_CONFIGURATION: Release
environment:
diff --git a/src/Core/API/API.Core/API.Core.csproj b/src/Core/API/API.Core/API.Core.csproj
index 55b89d1..4ddfa09 100644
--- a/src/Core/API/API.Core/API.Core.csproj
+++ b/src/Core/API/API.Core/API.Core.csproj
@@ -19,7 +19,8 @@
-
+
+
diff --git a/src/Core/API/API.Core/Controllers/AuthController.cs b/src/Core/API/API.Core/Controllers/AuthController.cs
index 0f22ea5..62a9018 100644
--- a/src/Core/API/API.Core/Controllers/AuthController.cs
+++ b/src/Core/API/API.Core/Controllers/AuthController.cs
@@ -1,9 +1,9 @@
using API.Core.Contracts.Auth;
using API.Core.Contracts.Common;
-using Domain.Core.Entities;
+using Domain.Entities;
+using Infrastructure.Jwt;
using Microsoft.AspNetCore.Mvc;
using Service.Core.Auth;
-using Service.Core.Jwt;
namespace API.Core.Controllers
{
diff --git a/src/Core/API/API.Core/Controllers/UserController.cs b/src/Core/API/API.Core/Controllers/UserController.cs
index 5077e44..6039ce6 100644
--- a/src/Core/API/API.Core/Controllers/UserController.cs
+++ b/src/Core/API/API.Core/Controllers/UserController.cs
@@ -1,4 +1,4 @@
-using Domain.Core.Entities;
+using Domain.Entities;
using Microsoft.AspNetCore.Mvc;
using Service.Core.User;
diff --git a/src/Core/API/API.Core/Dockerfile b/src/Core/API/API.Core/Dockerfile
index 7376967..cd61359 100644
--- a/src/Core/API/API.Core/Dockerfile
+++ b/src/Core/API/API.Core/Dockerfile
@@ -9,7 +9,10 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["API/API.Core/API.Core.csproj", "API/API.Core/"]
-COPY ["Repository/Repository.Core/Repository.Core.csproj", "Repository/Repository.Core/"]
+COPY ["Domain/Domain.csproj", "Domain/"]
+COPY ["Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj", "Infrastructure/Infrastructure.Repository/"]
+COPY ["Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj", "Infrastructure/Infrastructure.Jwt/"]
+COPY ["Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj", "Infrastructure/Infrastructure.PasswordHashing/"]
COPY ["Service/Service.Core/Service.Core.csproj", "Service/Service.Core/"]
RUN dotnet restore "API/API.Core/API.Core.csproj"
COPY . .
diff --git a/src/Core/API/API.Core/Middleware/ValidationExceptionHandlingMiddleware.cs b/src/Core/API/API.Core/Middleware/ValidationExceptionHandlingMiddleware.cs
new file mode 100644
index 0000000..a631cd0
--- /dev/null
+++ b/src/Core/API/API.Core/Middleware/ValidationExceptionHandlingMiddleware.cs
@@ -0,0 +1,47 @@
+using System.Net;
+using System.Text.Json;
+using API.Core.Contracts.Common;
+using FluentValidation;
+
+namespace API.Core.Middleware;
+
+public class ValidationExceptionHandlingMiddleware(RequestDelegate next)
+{
+ public async Task InvokeAsync(HttpContext context)
+ {
+ try
+ {
+ await next(context);
+ }
+ catch (ValidationException ex)
+ {
+ await HandleValidationExceptionAsync(context, ex);
+ }
+ }
+
+ private static Task HandleValidationExceptionAsync(HttpContext context, ValidationException exception)
+ {
+ context.Response.ContentType = "application/json";
+ context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
+
+ var errors = exception.Errors
+ .Select(e => e.ErrorMessage)
+ .ToList();
+
+ var message = errors.Count == 1
+ ? errors[0]
+ : "Validation failed. " + string.Join(" ", errors);
+
+ var response = new ResponseBody
+ {
+ Message = message
+ };
+
+ var jsonOptions = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ return context.Response.WriteAsync(JsonSerializer.Serialize(response, jsonOptions));
+ }
+}
diff --git a/src/Core/API/API.Core/Program.cs b/src/Core/API/API.Core/Program.cs
index 93b762f..640dbb6 100644
--- a/src/Core/API/API.Core/Program.cs
+++ b/src/Core/API/API.Core/Program.cs
@@ -1,21 +1,45 @@
using FluentValidation;
-using Repository.Core.Repositories.Auth;
-using Repository.Core.Repositories.UserAccount;
-using Repository.Core.Sql;
+using FluentValidation.AspNetCore;
+using Infrastructure.Jwt;
+using Infrastructure.PasswordHashing;
+using Infrastructure.Repository.Auth;
+using Infrastructure.Repository.Sql;
+using Infrastructure.Repository.UserAccount;
+using Microsoft.AspNetCore.Mvc;
using Service.Core.Auth;
-using Service.Core.Jwt;
-using Service.Core.Password;
using Service.Core.User;
var builder = WebApplication.CreateBuilder(args);
-builder.Services.AddControllers();
+builder.Services.AddControllers()
+ .ConfigureApiBehaviorOptions(options =>
+ {
+ options.InvalidModelStateResponseFactory = context =>
+ {
+ var errors = context.ModelState.Values
+ .SelectMany(v => v.Errors)
+ .Select(e => e.ErrorMessage)
+ .ToList();
+
+ var message = errors.Count == 1
+ ? errors[0]
+ : string.Join(" ", errors);
+
+ var response = new
+ {
+ message
+ };
+
+ return new BadRequestObjectResult(response);
+ };
+ });
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddOpenApi();
// Add FluentValidation
builder.Services.AddValidatorsFromAssemblyContaining();
+builder.Services.AddFluentValidationAutoValidation();
// Add health checks
builder.Services.AddHealthChecks();
@@ -35,7 +59,7 @@ builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
-builder.Services.AddScoped();
+builder.Services.AddScoped();
var app = builder.Build();
@@ -59,3 +83,6 @@ lifetime.ApplicationStopping.Register(() =>
});
app.Run();
+
+// Make Program class accessible to test projects
+public partial class Program { }
diff --git a/src/Core/API/API.Specs/Dockerfile b/src/Core/API/API.Specs/Dockerfile
index 97139c9..b934157 100644
--- a/src/Core/API/API.Specs/Dockerfile
+++ b/src/Core/API/API.Specs/Dockerfile
@@ -3,7 +3,10 @@ ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["API/API.Core/API.Core.csproj", "API/API.Core/"]
COPY ["API/API.Specs/API.Specs.csproj", "API/API.Specs/"]
-COPY ["Repository/Repository.Core/Repository.Core.csproj", "Repository/Repository.Core/"]
+COPY ["Domain/Domain.csproj", "Domain/"]
+COPY ["Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj", "Infrastructure/Infrastructure.Repository/"]
+COPY ["Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj", "Infrastructure/Infrastructure.Jwt/"]
+COPY ["Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj", "Infrastructure/Infrastructure.PasswordHashing/"]
COPY ["Service/Service.Core/Service.Core.csproj", "Service/Service.Core/"]
RUN dotnet restore "API/API.Specs/API.Specs.csproj"
COPY . .
diff --git a/src/Core/API/API.Specs/Features/Registration.feature b/src/Core/API/API.Specs/Features/Registration.feature
index 6140bea..cf6d3d0 100644
--- a/src/Core/API/API.Specs/Features/Registration.feature
+++ b/src/Core/API/API.Specs/Features/Registration.feature
@@ -52,7 +52,6 @@ Feature: User Registration
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| newuser | New | User | newuser@example.com | 1990-01-01 | weakpass |
Then the response has HTTP status 400
- And the response JSON should have "message" equal "Password does not meet complexity requirements."
Scenario: Cannot register a user younger than 19 years of age (regulatory requirement)
Given the API is running
diff --git a/src/Core/Core.slnx b/src/Core/Core.slnx
index 8555a8f..a9f0a9f 100644
--- a/src/Core/Core.slnx
+++ b/src/Core/Core.slnx
@@ -10,9 +10,11 @@
-
-
-
+
+
+
+
+
diff --git a/src/Core/Database/Database.Seed/Database.Seed.csproj b/src/Core/Database/Database.Seed/Database.Seed.csproj
index 16493b5..9a44208 100644
--- a/src/Core/Database/Database.Seed/Database.Seed.csproj
+++ b/src/Core/Database/Database.Seed/Database.Seed.csproj
@@ -19,6 +19,6 @@
-
+
diff --git a/src/Core/Database/Database.Seed/ISeeder.cs b/src/Core/Database/Database.Seed/ISeeder.cs
index 1ecf897..bd0d18d 100644
--- a/src/Core/Database/Database.Seed/ISeeder.cs
+++ b/src/Core/Database/Database.Seed/ISeeder.cs
@@ -1,9 +1,8 @@
using Microsoft.Data.SqlClient;
-namespace DBSeed
+namespace Database.Seed;
+
+internal interface ISeeder
{
- internal interface ISeeder
- {
- Task SeedAsync(SqlConnection connection);
- }
+ Task SeedAsync(SqlConnection connection);
}
\ No newline at end of file
diff --git a/src/Core/Database/Database.Seed/LocationSeeder.cs b/src/Core/Database/Database.Seed/LocationSeeder.cs
index 52d3d78..9e42050 100644
--- a/src/Core/Database/Database.Seed/LocationSeeder.cs
+++ b/src/Core/Database/Database.Seed/LocationSeeder.cs
@@ -1,328 +1,326 @@
using System.Data;
using Microsoft.Data.SqlClient;
-namespace DBSeed
+namespace Database.Seed;
+
+internal class LocationSeeder : ISeeder
{
+ private static readonly IReadOnlyList<(
+ string CountryName,
+ string CountryCode
+ )> Countries =
+ [
+ ("Canada", "CA"),
+ ("Mexico", "MX"),
+ ("United States", "US"),
+ ];
- internal class LocationSeeder : ISeeder
+ private static IReadOnlyList<(string StateProvinceName, string StateProvinceCode, string CountryCode)> States
{
- private static readonly IReadOnlyList<(
- string CountryName,
- string CountryCode
- )> Countries =
- [
- ("Canada", "CA"),
- ("Mexico", "MX"),
- ("United States", "US"),
- ];
+ get;
+ } =
+ [
+ ("Alabama", "US-AL", "US"),
+ ("Alaska", "US-AK", "US"),
+ ("Arizona", "US-AZ", "US"),
+ ("Arkansas", "US-AR", "US"),
+ ("California", "US-CA", "US"),
+ ("Colorado", "US-CO", "US"),
+ ("Connecticut", "US-CT", "US"),
+ ("Delaware", "US-DE", "US"),
+ ("Florida", "US-FL", "US"),
+ ("Georgia", "US-GA", "US"),
+ ("Hawaii", "US-HI", "US"),
+ ("Idaho", "US-ID", "US"),
+ ("Illinois", "US-IL", "US"),
+ ("Indiana", "US-IN", "US"),
+ ("Iowa", "US-IA", "US"),
+ ("Kansas", "US-KS", "US"),
+ ("Kentucky", "US-KY", "US"),
+ ("Louisiana", "US-LA", "US"),
+ ("Maine", "US-ME", "US"),
+ ("Maryland", "US-MD", "US"),
+ ("Massachusetts", "US-MA", "US"),
+ ("Michigan", "US-MI", "US"),
+ ("Minnesota", "US-MN", "US"),
+ ("Mississippi", "US-MS", "US"),
+ ("Missouri", "US-MO", "US"),
+ ("Montana", "US-MT", "US"),
+ ("Nebraska", "US-NE", "US"),
+ ("Nevada", "US-NV", "US"),
+ ("New Hampshire", "US-NH", "US"),
+ ("New Jersey", "US-NJ", "US"),
+ ("New Mexico", "US-NM", "US"),
+ ("New York", "US-NY", "US"),
+ ("North Carolina", "US-NC", "US"),
+ ("North Dakota", "US-ND", "US"),
+ ("Ohio", "US-OH", "US"),
+ ("Oklahoma", "US-OK", "US"),
+ ("Oregon", "US-OR", "US"),
+ ("Pennsylvania", "US-PA", "US"),
+ ("Rhode Island", "US-RI", "US"),
+ ("South Carolina", "US-SC", "US"),
+ ("South Dakota", "US-SD", "US"),
+ ("Tennessee", "US-TN", "US"),
+ ("Texas", "US-TX", "US"),
+ ("Utah", "US-UT", "US"),
+ ("Vermont", "US-VT", "US"),
+ ("Virginia", "US-VA", "US"),
+ ("Washington", "US-WA", "US"),
+ ("West Virginia", "US-WV", "US"),
+ ("Wisconsin", "US-WI", "US"),
+ ("Wyoming", "US-WY", "US"),
+ ("District of Columbia", "US-DC", "US"),
+ ("Puerto Rico", "US-PR", "US"),
+ ("U.S. Virgin Islands", "US-VI", "US"),
+ ("Guam", "US-GU", "US"),
+ ("Northern Mariana Islands", "US-MP", "US"),
+ ("American Samoa", "US-AS", "US"),
+ ("Ontario", "CA-ON", "CA"),
+ ("Québec", "CA-QC", "CA"),
+ ("Nova Scotia", "CA-NS", "CA"),
+ ("New Brunswick", "CA-NB", "CA"),
+ ("Manitoba", "CA-MB", "CA"),
+ ("British Columbia", "CA-BC", "CA"),
+ ("Prince Edward Island", "CA-PE", "CA"),
+ ("Saskatchewan", "CA-SK", "CA"),
+ ("Alberta", "CA-AB", "CA"),
+ ("Newfoundland and Labrador", "CA-NL", "CA"),
+ ("Northwest Territories", "CA-NT", "CA"),
+ ("Yukon", "CA-YT", "CA"),
+ ("Nunavut", "CA-NU", "CA"),
+ ("Aguascalientes", "MX-AGU", "MX"),
+ ("Baja California", "MX-BCN", "MX"),
+ ("Baja California Sur", "MX-BCS", "MX"),
+ ("Campeche", "MX-CAM", "MX"),
+ ("Chiapas", "MX-CHP", "MX"),
+ ("Chihuahua", "MX-CHH", "MX"),
+ ("Coahuila de Zaragoza", "MX-COA", "MX"),
+ ("Colima", "MX-COL", "MX"),
+ ("Durango", "MX-DUR", "MX"),
+ ("Guanajuato", "MX-GUA", "MX"),
+ ("Guerrero", "MX-GRO", "MX"),
+ ("Hidalgo", "MX-HID", "MX"),
+ ("Jalisco", "MX-JAL", "MX"),
+ ("México State", "MX-MEX", "MX"),
+ ("Michoacán de Ocampo", "MX-MIC", "MX"),
+ ("Morelos", "MX-MOR", "MX"),
+ ("Nayarit", "MX-NAY", "MX"),
+ ("Nuevo León", "MX-NLE", "MX"),
+ ("Oaxaca", "MX-OAX", "MX"),
+ ("Puebla", "MX-PUE", "MX"),
+ ("Querétaro", "MX-QUE", "MX"),
+ ("Quintana Roo", "MX-ROO", "MX"),
+ ("San Luis Potosí", "MX-SLP", "MX"),
+ ("Sinaloa", "MX-SIN", "MX"),
+ ("Sonora", "MX-SON", "MX"),
+ ("Tabasco", "MX-TAB", "MX"),
+ ("Tamaulipas", "MX-TAM", "MX"),
+ ("Tlaxcala", "MX-TLA", "MX"),
+ ("Veracruz de Ignacio de la Llave", "MX-VER", "MX"),
+ ("Yucatán", "MX-YUC", "MX"),
+ ("Zacatecas", "MX-ZAC", "MX"),
+ ("Ciudad de México", "MX-CMX", "MX"),
+ ];
- private static IReadOnlyList<(string StateProvinceName, string StateProvinceCode, string CountryCode)> States
+ private static IReadOnlyList<(string StateProvinceCode, string CityName)> Cities { get; } =
+ [
+ ("US-CA", "Los Angeles"),
+ ("US-CA", "San Diego"),
+ ("US-CA", "San Francisco"),
+ ("US-CA", "Sacramento"),
+ ("US-TX", "Houston"),
+ ("US-TX", "Dallas"),
+ ("US-TX", "Austin"),
+ ("US-TX", "San Antonio"),
+ ("US-FL", "Miami"),
+ ("US-FL", "Orlando"),
+ ("US-FL", "Tampa"),
+ ("US-NY", "New York"),
+ ("US-NY", "Buffalo"),
+ ("US-NY", "Rochester"),
+ ("US-IL", "Chicago"),
+ ("US-IL", "Springfield"),
+ ("US-PA", "Philadelphia"),
+ ("US-PA", "Pittsburgh"),
+ ("US-AZ", "Phoenix"),
+ ("US-AZ", "Tucson"),
+ ("US-CO", "Denver"),
+ ("US-CO", "Colorado Springs"),
+ ("US-MA", "Boston"),
+ ("US-MA", "Worcester"),
+ ("US-WA", "Seattle"),
+ ("US-WA", "Spokane"),
+ ("US-GA", "Atlanta"),
+ ("US-GA", "Savannah"),
+ ("US-NV", "Las Vegas"),
+ ("US-NV", "Reno"),
+ ("US-MI", "Detroit"),
+ ("US-MI", "Grand Rapids"),
+ ("US-MN", "Minneapolis"),
+ ("US-MN", "Saint Paul"),
+ ("US-OH", "Columbus"),
+ ("US-OH", "Cleveland"),
+ ("US-OR", "Portland"),
+ ("US-OR", "Salem"),
+ ("US-TN", "Nashville"),
+ ("US-TN", "Memphis"),
+ ("US-VA", "Richmond"),
+ ("US-VA", "Virginia Beach"),
+ ("US-MD", "Baltimore"),
+ ("US-MD", "Frederick"),
+ ("US-DC", "Washington"),
+ ("US-UT", "Salt Lake City"),
+ ("US-UT", "Provo"),
+ ("US-LA", "New Orleans"),
+ ("US-LA", "Baton Rouge"),
+ ("US-KY", "Louisville"),
+ ("US-KY", "Lexington"),
+ ("US-IA", "Des Moines"),
+ ("US-IA", "Cedar Rapids"),
+ ("US-OK", "Oklahoma City"),
+ ("US-OK", "Tulsa"),
+ ("US-NE", "Omaha"),
+ ("US-NE", "Lincoln"),
+ ("US-MO", "Kansas City"),
+ ("US-MO", "St. Louis"),
+ ("US-NC", "Charlotte"),
+ ("US-NC", "Raleigh"),
+ ("US-SC", "Columbia"),
+ ("US-SC", "Charleston"),
+ ("US-WI", "Milwaukee"),
+ ("US-WI", "Madison"),
+ ("US-MN", "Duluth"),
+ ("US-AK", "Anchorage"),
+ ("US-HI", "Honolulu"),
+ ("CA-ON", "Toronto"),
+ ("CA-ON", "Ottawa"),
+ ("CA-QC", "Montréal"),
+ ("CA-QC", "Québec City"),
+ ("CA-BC", "Vancouver"),
+ ("CA-BC", "Victoria"),
+ ("CA-AB", "Calgary"),
+ ("CA-AB", "Edmonton"),
+ ("CA-MB", "Winnipeg"),
+ ("CA-NS", "Halifax"),
+ ("CA-SK", "Saskatoon"),
+ ("CA-SK", "Regina"),
+ ("CA-NB", "Moncton"),
+ ("CA-NB", "Saint John"),
+ ("CA-PE", "Charlottetown"),
+ ("CA-NL", "St. John's"),
+ ("CA-ON", "Hamilton"),
+ ("CA-ON", "London"),
+ ("CA-QC", "Gatineau"),
+ ("CA-QC", "Laval"),
+ ("CA-BC", "Kelowna"),
+ ("CA-AB", "Red Deer"),
+ ("CA-MB", "Brandon"),
+ ("MX-CMX", "Ciudad de México"),
+ ("MX-JAL", "Guadalajara"),
+ ("MX-NLE", "Monterrey"),
+ ("MX-PUE", "Puebla"),
+ ("MX-ROO", "Cancún"),
+ ("MX-GUA", "Guanajuato"),
+ ("MX-MIC", "Morelia"),
+ ("MX-BCN", "Tijuana"),
+ ("MX-JAL", "Zapopan"),
+ ("MX-NLE", "San Nicolás"),
+ ("MX-CAM", "Campeche"),
+ ("MX-TAB", "Villahermosa"),
+ ("MX-VER", "Veracruz"),
+ ("MX-OAX", "Oaxaca"),
+ ("MX-SLP", "San Luis Potosí"),
+ ("MX-CHH", "Chihuahua"),
+ ("MX-AGU", "Aguascalientes"),
+ ("MX-MEX", "Toluca"),
+ ("MX-COA", "Saltillo"),
+ ("MX-BCS", "La Paz"),
+ ("MX-NAY", "Tepic"),
+ ("MX-ZAC", "Zacatecas"),
+ ];
+
+ public async Task SeedAsync(SqlConnection connection)
+ {
+ foreach (var (countryName, countryCode) in Countries)
{
- get;
- } =
- [
- ("Alabama", "US-AL", "US"),
- ("Alaska", "US-AK", "US"),
- ("Arizona", "US-AZ", "US"),
- ("Arkansas", "US-AR", "US"),
- ("California", "US-CA", "US"),
- ("Colorado", "US-CO", "US"),
- ("Connecticut", "US-CT", "US"),
- ("Delaware", "US-DE", "US"),
- ("Florida", "US-FL", "US"),
- ("Georgia", "US-GA", "US"),
- ("Hawaii", "US-HI", "US"),
- ("Idaho", "US-ID", "US"),
- ("Illinois", "US-IL", "US"),
- ("Indiana", "US-IN", "US"),
- ("Iowa", "US-IA", "US"),
- ("Kansas", "US-KS", "US"),
- ("Kentucky", "US-KY", "US"),
- ("Louisiana", "US-LA", "US"),
- ("Maine", "US-ME", "US"),
- ("Maryland", "US-MD", "US"),
- ("Massachusetts", "US-MA", "US"),
- ("Michigan", "US-MI", "US"),
- ("Minnesota", "US-MN", "US"),
- ("Mississippi", "US-MS", "US"),
- ("Missouri", "US-MO", "US"),
- ("Montana", "US-MT", "US"),
- ("Nebraska", "US-NE", "US"),
- ("Nevada", "US-NV", "US"),
- ("New Hampshire", "US-NH", "US"),
- ("New Jersey", "US-NJ", "US"),
- ("New Mexico", "US-NM", "US"),
- ("New York", "US-NY", "US"),
- ("North Carolina", "US-NC", "US"),
- ("North Dakota", "US-ND", "US"),
- ("Ohio", "US-OH", "US"),
- ("Oklahoma", "US-OK", "US"),
- ("Oregon", "US-OR", "US"),
- ("Pennsylvania", "US-PA", "US"),
- ("Rhode Island", "US-RI", "US"),
- ("South Carolina", "US-SC", "US"),
- ("South Dakota", "US-SD", "US"),
- ("Tennessee", "US-TN", "US"),
- ("Texas", "US-TX", "US"),
- ("Utah", "US-UT", "US"),
- ("Vermont", "US-VT", "US"),
- ("Virginia", "US-VA", "US"),
- ("Washington", "US-WA", "US"),
- ("West Virginia", "US-WV", "US"),
- ("Wisconsin", "US-WI", "US"),
- ("Wyoming", "US-WY", "US"),
- ("District of Columbia", "US-DC", "US"),
- ("Puerto Rico", "US-PR", "US"),
- ("U.S. Virgin Islands", "US-VI", "US"),
- ("Guam", "US-GU", "US"),
- ("Northern Mariana Islands", "US-MP", "US"),
- ("American Samoa", "US-AS", "US"),
- ("Ontario", "CA-ON", "CA"),
- ("Québec", "CA-QC", "CA"),
- ("Nova Scotia", "CA-NS", "CA"),
- ("New Brunswick", "CA-NB", "CA"),
- ("Manitoba", "CA-MB", "CA"),
- ("British Columbia", "CA-BC", "CA"),
- ("Prince Edward Island", "CA-PE", "CA"),
- ("Saskatchewan", "CA-SK", "CA"),
- ("Alberta", "CA-AB", "CA"),
- ("Newfoundland and Labrador", "CA-NL", "CA"),
- ("Northwest Territories", "CA-NT", "CA"),
- ("Yukon", "CA-YT", "CA"),
- ("Nunavut", "CA-NU", "CA"),
- ("Aguascalientes", "MX-AGU", "MX"),
- ("Baja California", "MX-BCN", "MX"),
- ("Baja California Sur", "MX-BCS", "MX"),
- ("Campeche", "MX-CAM", "MX"),
- ("Chiapas", "MX-CHP", "MX"),
- ("Chihuahua", "MX-CHH", "MX"),
- ("Coahuila de Zaragoza", "MX-COA", "MX"),
- ("Colima", "MX-COL", "MX"),
- ("Durango", "MX-DUR", "MX"),
- ("Guanajuato", "MX-GUA", "MX"),
- ("Guerrero", "MX-GRO", "MX"),
- ("Hidalgo", "MX-HID", "MX"),
- ("Jalisco", "MX-JAL", "MX"),
- ("México State", "MX-MEX", "MX"),
- ("Michoacán de Ocampo", "MX-MIC", "MX"),
- ("Morelos", "MX-MOR", "MX"),
- ("Nayarit", "MX-NAY", "MX"),
- ("Nuevo León", "MX-NLE", "MX"),
- ("Oaxaca", "MX-OAX", "MX"),
- ("Puebla", "MX-PUE", "MX"),
- ("Querétaro", "MX-QUE", "MX"),
- ("Quintana Roo", "MX-ROO", "MX"),
- ("San Luis Potosí", "MX-SLP", "MX"),
- ("Sinaloa", "MX-SIN", "MX"),
- ("Sonora", "MX-SON", "MX"),
- ("Tabasco", "MX-TAB", "MX"),
- ("Tamaulipas", "MX-TAM", "MX"),
- ("Tlaxcala", "MX-TLA", "MX"),
- ("Veracruz de Ignacio de la Llave", "MX-VER", "MX"),
- ("Yucatán", "MX-YUC", "MX"),
- ("Zacatecas", "MX-ZAC", "MX"),
- ("Ciudad de México", "MX-CMX", "MX"),
- ];
-
- private static IReadOnlyList<(string StateProvinceCode, string CityName)> Cities { get; } =
- [
- ("US-CA", "Los Angeles"),
- ("US-CA", "San Diego"),
- ("US-CA", "San Francisco"),
- ("US-CA", "Sacramento"),
- ("US-TX", "Houston"),
- ("US-TX", "Dallas"),
- ("US-TX", "Austin"),
- ("US-TX", "San Antonio"),
- ("US-FL", "Miami"),
- ("US-FL", "Orlando"),
- ("US-FL", "Tampa"),
- ("US-NY", "New York"),
- ("US-NY", "Buffalo"),
- ("US-NY", "Rochester"),
- ("US-IL", "Chicago"),
- ("US-IL", "Springfield"),
- ("US-PA", "Philadelphia"),
- ("US-PA", "Pittsburgh"),
- ("US-AZ", "Phoenix"),
- ("US-AZ", "Tucson"),
- ("US-CO", "Denver"),
- ("US-CO", "Colorado Springs"),
- ("US-MA", "Boston"),
- ("US-MA", "Worcester"),
- ("US-WA", "Seattle"),
- ("US-WA", "Spokane"),
- ("US-GA", "Atlanta"),
- ("US-GA", "Savannah"),
- ("US-NV", "Las Vegas"),
- ("US-NV", "Reno"),
- ("US-MI", "Detroit"),
- ("US-MI", "Grand Rapids"),
- ("US-MN", "Minneapolis"),
- ("US-MN", "Saint Paul"),
- ("US-OH", "Columbus"),
- ("US-OH", "Cleveland"),
- ("US-OR", "Portland"),
- ("US-OR", "Salem"),
- ("US-TN", "Nashville"),
- ("US-TN", "Memphis"),
- ("US-VA", "Richmond"),
- ("US-VA", "Virginia Beach"),
- ("US-MD", "Baltimore"),
- ("US-MD", "Frederick"),
- ("US-DC", "Washington"),
- ("US-UT", "Salt Lake City"),
- ("US-UT", "Provo"),
- ("US-LA", "New Orleans"),
- ("US-LA", "Baton Rouge"),
- ("US-KY", "Louisville"),
- ("US-KY", "Lexington"),
- ("US-IA", "Des Moines"),
- ("US-IA", "Cedar Rapids"),
- ("US-OK", "Oklahoma City"),
- ("US-OK", "Tulsa"),
- ("US-NE", "Omaha"),
- ("US-NE", "Lincoln"),
- ("US-MO", "Kansas City"),
- ("US-MO", "St. Louis"),
- ("US-NC", "Charlotte"),
- ("US-NC", "Raleigh"),
- ("US-SC", "Columbia"),
- ("US-SC", "Charleston"),
- ("US-WI", "Milwaukee"),
- ("US-WI", "Madison"),
- ("US-MN", "Duluth"),
- ("US-AK", "Anchorage"),
- ("US-HI", "Honolulu"),
- ("CA-ON", "Toronto"),
- ("CA-ON", "Ottawa"),
- ("CA-QC", "Montréal"),
- ("CA-QC", "Québec City"),
- ("CA-BC", "Vancouver"),
- ("CA-BC", "Victoria"),
- ("CA-AB", "Calgary"),
- ("CA-AB", "Edmonton"),
- ("CA-MB", "Winnipeg"),
- ("CA-NS", "Halifax"),
- ("CA-SK", "Saskatoon"),
- ("CA-SK", "Regina"),
- ("CA-NB", "Moncton"),
- ("CA-NB", "Saint John"),
- ("CA-PE", "Charlottetown"),
- ("CA-NL", "St. John's"),
- ("CA-ON", "Hamilton"),
- ("CA-ON", "London"),
- ("CA-QC", "Gatineau"),
- ("CA-QC", "Laval"),
- ("CA-BC", "Kelowna"),
- ("CA-AB", "Red Deer"),
- ("CA-MB", "Brandon"),
- ("MX-CMX", "Ciudad de México"),
- ("MX-JAL", "Guadalajara"),
- ("MX-NLE", "Monterrey"),
- ("MX-PUE", "Puebla"),
- ("MX-ROO", "Cancún"),
- ("MX-GUA", "Guanajuato"),
- ("MX-MIC", "Morelia"),
- ("MX-BCN", "Tijuana"),
- ("MX-JAL", "Zapopan"),
- ("MX-NLE", "San Nicolás"),
- ("MX-CAM", "Campeche"),
- ("MX-TAB", "Villahermosa"),
- ("MX-VER", "Veracruz"),
- ("MX-OAX", "Oaxaca"),
- ("MX-SLP", "San Luis Potosí"),
- ("MX-CHH", "Chihuahua"),
- ("MX-AGU", "Aguascalientes"),
- ("MX-MEX", "Toluca"),
- ("MX-COA", "Saltillo"),
- ("MX-BCS", "La Paz"),
- ("MX-NAY", "Tepic"),
- ("MX-ZAC", "Zacatecas"),
- ];
-
- public async Task SeedAsync(SqlConnection connection)
- {
- foreach (var (countryName, countryCode) in Countries)
- {
- await CreateCountryAsync(connection, countryName, countryCode);
- }
-
- foreach (
- var (stateProvinceName, stateProvinceCode, countryCode) in States
- )
- {
- await CreateStateProvinceAsync(
- connection,
- stateProvinceName,
- stateProvinceCode,
- countryCode
- );
- }
-
- foreach (var (stateProvinceCode, cityName) in Cities)
- {
- await CreateCityAsync(connection, cityName, stateProvinceCode);
- }
+ await CreateCountryAsync(connection, countryName, countryCode);
}
- private static async Task CreateCountryAsync(
- SqlConnection connection,
- string countryName,
- string countryCode
+ foreach (
+ var (stateProvinceName, stateProvinceCode, countryCode) in States
)
{
- await using var command = new SqlCommand(
- "dbo.USP_CreateCountry",
- connection
+ await CreateStateProvinceAsync(
+ connection,
+ stateProvinceName,
+ stateProvinceCode,
+ countryCode
);
- command.CommandType = CommandType.StoredProcedure;
- command.Parameters.AddWithValue("@CountryName", countryName);
- command.Parameters.AddWithValue("@ISO3616_1", countryCode);
-
- await command.ExecuteNonQueryAsync();
}
- private static async Task CreateStateProvinceAsync(
- SqlConnection connection,
- string stateProvinceName,
- string stateProvinceCode,
- string countryCode
- )
+ foreach (var (stateProvinceCode, cityName) in Cities)
{
- await using var command = new SqlCommand(
- "dbo.USP_CreateStateProvince",
- connection
- );
- command.CommandType = CommandType.StoredProcedure;
- command.Parameters.AddWithValue(
- "@StateProvinceName",
- stateProvinceName
- );
- command.Parameters.AddWithValue("@ISO3616_2", stateProvinceCode);
- command.Parameters.AddWithValue("@CountryCode", countryCode);
-
- await command.ExecuteNonQueryAsync();
- }
-
- private static async Task CreateCityAsync(
- SqlConnection connection,
- string cityName,
- string stateProvinceCode
- )
- {
- await using var command = new SqlCommand(
- "dbo.USP_CreateCity",
- connection
- );
- command.CommandType = CommandType.StoredProcedure;
- command.Parameters.AddWithValue("@CityName", cityName);
- command.Parameters.AddWithValue(
- "@StateProvinceCode",
- stateProvinceCode
- );
-
- await command.ExecuteNonQueryAsync();
+ await CreateCityAsync(connection, cityName, stateProvinceCode);
}
}
+
+ private static async Task CreateCountryAsync(
+ SqlConnection connection,
+ string countryName,
+ string countryCode
+ )
+ {
+ await using var command = new SqlCommand(
+ "dbo.USP_CreateCountry",
+ connection
+ );
+ command.CommandType = CommandType.StoredProcedure;
+ command.Parameters.AddWithValue("@CountryName", countryName);
+ command.Parameters.AddWithValue("@ISO3616_1", countryCode);
+
+ await command.ExecuteNonQueryAsync();
+ }
+
+ private static async Task CreateStateProvinceAsync(
+ SqlConnection connection,
+ string stateProvinceName,
+ string stateProvinceCode,
+ string countryCode
+ )
+ {
+ await using var command = new SqlCommand(
+ "dbo.USP_CreateStateProvince",
+ connection
+ );
+ command.CommandType = CommandType.StoredProcedure;
+ command.Parameters.AddWithValue(
+ "@StateProvinceName",
+ stateProvinceName
+ );
+ command.Parameters.AddWithValue("@ISO3616_2", stateProvinceCode);
+ command.Parameters.AddWithValue("@CountryCode", countryCode);
+
+ await command.ExecuteNonQueryAsync();
+ }
+
+ private static async Task CreateCityAsync(
+ SqlConnection connection,
+ string cityName,
+ string stateProvinceCode
+ )
+ {
+ await using var command = new SqlCommand(
+ "dbo.USP_CreateCity",
+ connection
+ );
+ command.CommandType = CommandType.StoredProcedure;
+ command.Parameters.AddWithValue("@CityName", cityName);
+ command.Parameters.AddWithValue(
+ "@StateProvinceCode",
+ stateProvinceCode
+ );
+
+ await command.ExecuteNonQueryAsync();
+ }
}
\ No newline at end of file
diff --git a/src/Core/Database/Database.Seed/Program.cs b/src/Core/Database/Database.Seed/Program.cs
index e7fff96..c7d064d 100644
--- a/src/Core/Database/Database.Seed/Program.cs
+++ b/src/Core/Database/Database.Seed/Program.cs
@@ -1,24 +1,24 @@
-using DBSeed;
-using Microsoft.Data.SqlClient;
+using Microsoft.Data.SqlClient;
using DbUp;
using System.Reflection;
+using Database.Seed;
string BuildConnectionString()
{
var server = Environment.GetEnvironmentVariable("DB_SERVER")
- ?? throw new InvalidOperationException("DB_SERVER environment variable is not set");
+ ?? throw new InvalidOperationException("DB_SERVER environment variable is not set");
var dbName = 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")
- ?? throw new InvalidOperationException("DB_USER environment variable is not set");
+ ?? throw new InvalidOperationException("DB_USER environment variable is not set");
var password = Environment.GetEnvironmentVariable("DB_PASSWORD")
- ?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set");
+ ?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set");
var trustServerCertificate = Environment.GetEnvironmentVariable("DB_TRUST_SERVER_CERTIFICATE")
- ?? "True";
+ ?? "True";
var builder = new SqlConnectionStringBuilder
{
@@ -33,6 +33,7 @@ string BuildConnectionString()
return builder.ConnectionString;
}
+
try
{
var connectionString = BuildConnectionString();
@@ -72,7 +73,6 @@ try
using (connection)
{
-
ISeeder[] seeders =
[
new LocationSeeder(),
@@ -96,4 +96,4 @@ catch (Exception ex)
Console.Error.WriteLine("Seed failed:");
Console.Error.WriteLine(ex);
return 1;
-}
+}
\ No newline at end of file
diff --git a/src/Core/Database/Database.Seed/UserSeeder.cs b/src/Core/Database/Database.Seed/UserSeeder.cs
index 13465b9..b7ae4ba 100644
--- a/src/Core/Database/Database.Seed/UserSeeder.cs
+++ b/src/Core/Database/Database.Seed/UserSeeder.cs
@@ -1,273 +1,267 @@
using System.Data;
using System.Security.Cryptography;
using System.Text;
-using Domain.Core.Entities;
-using Repository.Core.Repositories;
using idunno.Password;
using Konscious.Security.Cryptography;
using Microsoft.Data.SqlClient;
-namespace DBSeed
+namespace Database.Seed;
+
+internal class UserSeeder : ISeeder
{
- internal class UserSeeder : ISeeder
+ private static readonly IReadOnlyList<(
+ string FirstName,
+ string LastName
+ )> SeedNames =
+ [
+ ("Aarya", "Mathews"),
+ ("Aiden", "Wells"),
+ ("Aleena", "Gonzalez"),
+ ("Alessandra", "Nelson"),
+ ("Amari", "Tucker"),
+ ("Ameer", "Huff"),
+ ("Amirah", "Hicks"),
+ ("Analia", "Dominguez"),
+ ("Anne", "Jenkins"),
+ ("Apollo", "Davis"),
+ ("Arianna", "White"),
+ ("Aubree", "Moore"),
+ ("Aubrielle", "Raymond"),
+ ("Aydin", "Odom"),
+ ("Bowen", "Casey"),
+ ("Brock", "Huber"),
+ ("Caiden", "Strong"),
+ ("Cecilia", "Rosales"),
+ ("Celeste", "Barber"),
+ ("Chance", "Small"),
+ ("Clara", "Roberts"),
+ ("Collins", "Brandt"),
+ ("Damir", "Wallace"),
+ ("Declan", "Crawford"),
+ ("Dennis", "Decker"),
+ ("Dylan", "Lang"),
+ ("Eliza", "Kane"),
+ ("Elle", "Poole"),
+ ("Elliott", "Miles"),
+ ("Emelia", "Lucas"),
+ ("Emilia", "Simpson"),
+ ("Emmett", "Lugo"),
+ ("Ethan", "Stephens"),
+ ("Etta", "Woods"),
+ ("Gael", "Moran"),
+ ("Grant", "Benson"),
+ ("Gwen", "James"),
+ ("Huxley", "Chen"),
+ ("Isabella", "Fisher"),
+ ("Ivan", "Mathis"),
+ ("Jamir", "McMillan"),
+ ("Jaxson", "Shields"),
+ ("Jimmy", "Richmond"),
+ ("Josiah", "Flores"),
+ ("Kaden", "Enriquez"),
+ ("Kai", "Lawson"),
+ ("Karsyn", "Adkins"),
+ ("Karsyn", "Proctor"),
+ ("Kayden", "Henson"),
+ ("Kaylie", "Spears"),
+ ("Kinslee", "Jones"),
+ ("Kora", "Guerra"),
+ ("Lane", "Skinner"),
+ ("Laylani", "Christian"),
+ ("Ledger", "Carroll"),
+ ("Leilany", "Small"),
+ ("Leland", "McCall"),
+ ("Leonard", "Calhoun"),
+ ("Levi", "Ochoa"),
+ ("Lillie", "Vang"),
+ ("Lola", "Sheppard"),
+ ("Luciana", "Poole"),
+ ("Maddox", "Hughes"),
+ ("Mara", "Blackwell"),
+ ("Marcellus", "Bartlett"),
+ ("Margo", "Koch"),
+ ("Maurice", "Gibson"),
+ ("Maxton", "Dodson"),
+ ("Mia", "Parrish"),
+ ("Millie", "Fuentes"),
+ ("Nellie", "Villanueva"),
+ ("Nicolas", "Mata"),
+ ("Nicolas", "Miller"),
+ ("Oakleigh", "Foster"),
+ ("Octavia", "Pierce"),
+ ("Paisley", "Allison"),
+ ("Quincy", "Andersen"),
+ ("Quincy", "Frazier"),
+ ("Raiden", "Roberts"),
+ ("Raquel", "Lara"),
+ ("Rudy", "McIntosh"),
+ ("Salvador", "Stein"),
+ ("Samantha", "Dickson"),
+ ("Solomon", "Richards"),
+ ("Sylvia", "Hanna"),
+ ("Talia", "Trujillo"),
+ ("Thalia", "Farrell"),
+ ("Trent", "Mayo"),
+ ("Trinity", "Cummings"),
+ ("Ty", "Perry"),
+ ("Tyler", "Romero"),
+ ("Valeria", "Pierce"),
+ ("Vance", "Neal"),
+ ("Whitney", "Bell"),
+ ("Wilder", "Graves"),
+ ("William", "Logan"),
+ ("Zara", "Wilkinson"),
+ ("Zaria", "Gibson"),
+ ("Zion", "Watkins"),
+ ("Zoie", "Armstrong"),
+ ];
+
+ public async Task SeedAsync(SqlConnection connection)
{
- private static readonly IReadOnlyList<(
- string FirstName,
- string LastName
- )> SeedNames =
- [
- ("Aarya", "Mathews"),
- ("Aiden", "Wells"),
- ("Aleena", "Gonzalez"),
- ("Alessandra", "Nelson"),
- ("Amari", "Tucker"),
- ("Ameer", "Huff"),
- ("Amirah", "Hicks"),
- ("Analia", "Dominguez"),
- ("Anne", "Jenkins"),
- ("Apollo", "Davis"),
- ("Arianna", "White"),
- ("Aubree", "Moore"),
- ("Aubrielle", "Raymond"),
- ("Aydin", "Odom"),
- ("Bowen", "Casey"),
- ("Brock", "Huber"),
- ("Caiden", "Strong"),
- ("Cecilia", "Rosales"),
- ("Celeste", "Barber"),
- ("Chance", "Small"),
- ("Clara", "Roberts"),
- ("Collins", "Brandt"),
- ("Damir", "Wallace"),
- ("Declan", "Crawford"),
- ("Dennis", "Decker"),
- ("Dylan", "Lang"),
- ("Eliza", "Kane"),
- ("Elle", "Poole"),
- ("Elliott", "Miles"),
- ("Emelia", "Lucas"),
- ("Emilia", "Simpson"),
- ("Emmett", "Lugo"),
- ("Ethan", "Stephens"),
- ("Etta", "Woods"),
- ("Gael", "Moran"),
- ("Grant", "Benson"),
- ("Gwen", "James"),
- ("Huxley", "Chen"),
- ("Isabella", "Fisher"),
- ("Ivan", "Mathis"),
- ("Jamir", "McMillan"),
- ("Jaxson", "Shields"),
- ("Jimmy", "Richmond"),
- ("Josiah", "Flores"),
- ("Kaden", "Enriquez"),
- ("Kai", "Lawson"),
- ("Karsyn", "Adkins"),
- ("Karsyn", "Proctor"),
- ("Kayden", "Henson"),
- ("Kaylie", "Spears"),
- ("Kinslee", "Jones"),
- ("Kora", "Guerra"),
- ("Lane", "Skinner"),
- ("Laylani", "Christian"),
- ("Ledger", "Carroll"),
- ("Leilany", "Small"),
- ("Leland", "McCall"),
- ("Leonard", "Calhoun"),
- ("Levi", "Ochoa"),
- ("Lillie", "Vang"),
- ("Lola", "Sheppard"),
- ("Luciana", "Poole"),
- ("Maddox", "Hughes"),
- ("Mara", "Blackwell"),
- ("Marcellus", "Bartlett"),
- ("Margo", "Koch"),
- ("Maurice", "Gibson"),
- ("Maxton", "Dodson"),
- ("Mia", "Parrish"),
- ("Millie", "Fuentes"),
- ("Nellie", "Villanueva"),
- ("Nicolas", "Mata"),
- ("Nicolas", "Miller"),
- ("Oakleigh", "Foster"),
- ("Octavia", "Pierce"),
- ("Paisley", "Allison"),
- ("Quincy", "Andersen"),
- ("Quincy", "Frazier"),
- ("Raiden", "Roberts"),
- ("Raquel", "Lara"),
- ("Rudy", "McIntosh"),
- ("Salvador", "Stein"),
- ("Samantha", "Dickson"),
- ("Solomon", "Richards"),
- ("Sylvia", "Hanna"),
- ("Talia", "Trujillo"),
- ("Thalia", "Farrell"),
- ("Trent", "Mayo"),
- ("Trinity", "Cummings"),
- ("Ty", "Perry"),
- ("Tyler", "Romero"),
- ("Valeria", "Pierce"),
- ("Vance", "Neal"),
- ("Whitney", "Bell"),
- ("Wilder", "Graves"),
- ("William", "Logan"),
- ("Zara", "Wilkinson"),
- ("Zaria", "Gibson"),
- ("Zion", "Watkins"),
- ("Zoie", "Armstrong"),
- ];
+ var generator = new PasswordGenerator();
+ var rng = new Random();
+ int createdUsers = 0;
+ int createdCredentials = 0;
+ int createdVerifications = 0;
- public async Task SeedAsync(SqlConnection connection)
{
- var generator = new PasswordGenerator();
- var rng = new Random();
- int createdUsers = 0;
- int createdCredentials = 0;
- int createdVerifications = 0;
+ const string firstName = "Test";
+ const string lastName = "User";
+ const string email = "test.user@thebiergarten.app";
+ var dob = new DateTime(1985, 03, 01);
+ var hash = GeneratePasswordHash("password");
- {
- const string firstName = "Test";
- const string lastName = "User";
- const string email = "test.user@thebiergarten.app";
- var dob = new DateTime(1985, 03, 01);
- var hash = GeneratePasswordHash("password");
-
- await RegisterUserAsync(
- connection,
- $"{firstName}.{lastName}",
- firstName,
- lastName,
- dob,
- email,
- hash
- );
- }
- foreach (var (firstName, lastName) in SeedNames)
- {
- // prepare user fields
- var username = $"{firstName[0]}.{lastName}";
- var email = $"{firstName}.{lastName}@thebiergarten.app";
- var dob = GenerateDateOfBirth(rng);
-
- // generate a password and hash it
- string pwd = generator.Generate(
- length: 64,
- numberOfDigits: 10,
- numberOfSymbols: 10
- );
- string hash = GeneratePasswordHash(pwd);
-
-
- // register the user (creates account + credential)
- var id = await RegisterUserAsync(
- connection,
- username,
- firstName,
- lastName,
- dob,
- email,
- hash
- );
- createdUsers++;
- createdCredentials++;
-
-
-
-
- // add user verification
- if (await HasUserVerificationAsync(connection, id)) continue;
-
- await AddUserVerificationAsync(connection, id);
- createdVerifications++;
- }
-
- Console.WriteLine($"Created {createdUsers} user accounts.");
- Console.WriteLine($"Added {createdCredentials} user credentials.");
- Console.WriteLine($"Added {createdVerifications} user verifications.");
- }
-
- private static async Task RegisterUserAsync(
- SqlConnection connection,
- string username,
- string firstName,
- string lastName,
- DateTime dateOfBirth,
- string email,
- string hash
- )
- {
- await using var command = new SqlCommand("dbo.USP_RegisterUser", connection);
- command.CommandType = CommandType.StoredProcedure;
-
-
- command.Parameters.Add("@Username", SqlDbType.VarChar, 64).Value = username;
- command.Parameters.Add("@FirstName", SqlDbType.NVarChar, 128).Value = firstName;
- command.Parameters.Add("@LastName", SqlDbType.NVarChar, 128).Value = lastName;
- command.Parameters.Add("@DateOfBirth", SqlDbType.DateTime).Value = dateOfBirth;
- command.Parameters.Add("@Email", SqlDbType.VarChar, 128).Value = email;
- command.Parameters.Add("@Hash", SqlDbType.NVarChar, -1).Value = hash;
-
- var result = await command.ExecuteScalarAsync();
-
-
- return (Guid)result!;
-
- }
-
- private static string GeneratePasswordHash(string pwd)
- {
- byte[] salt = RandomNumberGenerator.GetBytes(16);
-
- var argon2 = new Argon2id(Encoding.UTF8.GetBytes(pwd))
- {
- Salt = salt,
- DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1),
- MemorySize = 65536,
- Iterations = 4,
- };
-
- byte[] hash = argon2.GetBytes(32);
- return $"{Convert.ToBase64String(salt)}:{Convert.ToBase64String(hash)}";
- }
-
- private static async Task HasUserVerificationAsync(
- SqlConnection connection,
- Guid userAccountId
- )
- {
- const string sql = """
- SELECT 1
- FROM dbo.UserVerification
- WHERE UserAccountId = @UserAccountId;
- """;
- await using var command = new SqlCommand(sql, connection);
- command.Parameters.AddWithValue("@UserAccountId", userAccountId);
- var result = await command.ExecuteScalarAsync();
- return result is not null;
- }
-
- private static async Task AddUserVerificationAsync(
- SqlConnection connection,
- Guid userAccountId
- )
- {
- await using var command = new SqlCommand(
- "dbo.USP_CreateUserVerification",
- connection
+ await RegisterUserAsync(
+ connection,
+ $"{firstName}.{lastName}",
+ firstName,
+ lastName,
+ dob,
+ email,
+ hash
);
- command.CommandType = CommandType.StoredProcedure;
- command.Parameters.AddWithValue("@UserAccountID_", userAccountId);
-
- await command.ExecuteNonQueryAsync();
}
-
- private static DateTime GenerateDateOfBirth(Random random)
+ foreach (var (firstName, lastName) in SeedNames)
{
- int age = 19 + random.Next(0, 30);
- DateTime baseDate = DateTime.UtcNow.Date.AddYears(-age);
- int offsetDays = random.Next(0, 365);
- return baseDate.AddDays(-offsetDays);
+ // prepare user fields
+ var username = $"{firstName[0]}.{lastName}";
+ var email = $"{firstName}.{lastName}@thebiergarten.app";
+ var dob = GenerateDateOfBirth(rng);
+
+ // generate a password and hash it
+ string pwd = generator.Generate(
+ length: 64,
+ numberOfDigits: 10,
+ numberOfSymbols: 10
+ );
+ string hash = GeneratePasswordHash(pwd);
+
+
+ // register the user (creates account + credential)
+ var id = await RegisterUserAsync(
+ connection,
+ username,
+ firstName,
+ lastName,
+ dob,
+ email,
+ hash
+ );
+ createdUsers++;
+ createdCredentials++;
+
+
+ // add user verification
+ if (await HasUserVerificationAsync(connection, id)) continue;
+
+ await AddUserVerificationAsync(connection, id);
+ createdVerifications++;
}
+
+ Console.WriteLine($"Created {createdUsers} user accounts.");
+ Console.WriteLine($"Added {createdCredentials} user credentials.");
+ Console.WriteLine($"Added {createdVerifications} user verifications.");
}
-}
+
+ private static async Task RegisterUserAsync(
+ SqlConnection connection,
+ string username,
+ string firstName,
+ string lastName,
+ DateTime dateOfBirth,
+ string email,
+ string hash
+ )
+ {
+ await using var command = new SqlCommand("dbo.USP_RegisterUser", connection);
+ command.CommandType = CommandType.StoredProcedure;
+
+
+ command.Parameters.Add("@Username", SqlDbType.VarChar, 64).Value = username;
+ command.Parameters.Add("@FirstName", SqlDbType.NVarChar, 128).Value = firstName;
+ command.Parameters.Add("@LastName", SqlDbType.NVarChar, 128).Value = lastName;
+ command.Parameters.Add("@DateOfBirth", SqlDbType.DateTime).Value = dateOfBirth;
+ command.Parameters.Add("@Email", SqlDbType.VarChar, 128).Value = email;
+ command.Parameters.Add("@Hash", SqlDbType.NVarChar, -1).Value = hash;
+
+ var result = await command.ExecuteScalarAsync();
+
+
+ return (Guid)result!;
+ }
+
+ private static string GeneratePasswordHash(string pwd)
+ {
+ byte[] salt = RandomNumberGenerator.GetBytes(16);
+
+ var argon2 = new Argon2id(Encoding.UTF8.GetBytes(pwd))
+ {
+ Salt = salt,
+ DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1),
+ MemorySize = 65536,
+ Iterations = 4,
+ };
+
+ byte[] hash = argon2.GetBytes(32);
+ return $"{Convert.ToBase64String(salt)}:{Convert.ToBase64String(hash)}";
+ }
+
+ private static async Task HasUserVerificationAsync(
+ SqlConnection connection,
+ Guid userAccountId
+ )
+ {
+ const string sql = """
+ SELECT 1
+ FROM dbo.UserVerification
+ WHERE UserAccountId = @UserAccountId;
+ """;
+ await using var command = new SqlCommand(sql, connection);
+ command.Parameters.AddWithValue("@UserAccountId", userAccountId);
+ var result = await command.ExecuteScalarAsync();
+ return result is not null;
+ }
+
+ private static async Task AddUserVerificationAsync(
+ SqlConnection connection,
+ Guid userAccountId
+ )
+ {
+ await using var command = new SqlCommand(
+ "dbo.USP_CreateUserVerification",
+ connection
+ );
+ command.CommandType = CommandType.StoredProcedure;
+ command.Parameters.AddWithValue("@UserAccountID_", userAccountId);
+
+ await command.ExecuteNonQueryAsync();
+ }
+
+ private static DateTime GenerateDateOfBirth(Random random)
+ {
+ int age = 19 + random.Next(0, 30);
+ DateTime baseDate = DateTime.UtcNow.Date.AddYears(-age);
+ int offsetDays = random.Next(0, 365);
+ return baseDate.AddDays(-offsetDays);
+ }
+}
\ No newline at end of file
diff --git a/src/Core/Domain/Entities/UserAccount.cs b/src/Core/Domain/Entities/UserAccount.cs
index 4f10fdd..bda19c7 100644
--- a/src/Core/Domain/Entities/UserAccount.cs
+++ b/src/Core/Domain/Entities/UserAccount.cs
@@ -1,4 +1,4 @@
-namespace Domain.Core.Entities;
+namespace Domain.Entities;
public class UserAccount
{
diff --git a/src/Core/Domain/Entities/UserCredential.cs b/src/Core/Domain/Entities/UserCredential.cs
index 1dd87f8..65b46a1 100644
--- a/src/Core/Domain/Entities/UserCredential.cs
+++ b/src/Core/Domain/Entities/UserCredential.cs
@@ -1,4 +1,4 @@
-namespace Domain.Core.Entities;
+namespace Domain.Entities;
public class UserCredential
{
diff --git a/src/Core/Domain/Entities/UserVerification.cs b/src/Core/Domain/Entities/UserVerification.cs
index 777203c..3e66754 100644
--- a/src/Core/Domain/Entities/UserVerification.cs
+++ b/src/Core/Domain/Entities/UserVerification.cs
@@ -1,4 +1,4 @@
-namespace Domain.Core.Entities;
+namespace Domain.Entities;
public class UserVerification
{
diff --git a/src/Core/Service/Service.Core/Jwt/IJwtService.cs b/src/Core/Infrastructure/Infrastructure.Jwt/IJwtService.cs
similarity index 76%
rename from src/Core/Service/Service.Core/Jwt/IJwtService.cs
rename to src/Core/Infrastructure/Infrastructure.Jwt/IJwtService.cs
index 730a17c..b62d2e3 100644
--- a/src/Core/Service/Service.Core/Jwt/IJwtService.cs
+++ b/src/Core/Infrastructure/Infrastructure.Jwt/IJwtService.cs
@@ -1,6 +1,6 @@
-namespace Service.Core.Jwt;
+namespace Infrastructure.Jwt;
public interface IJwtService
{
string GenerateJwt(Guid userId, string username, DateTime expiry);
-}
\ No newline at end of file
+}
diff --git a/src/Core/Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj b/src/Core/Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj
new file mode 100644
index 0000000..cddd219
--- /dev/null
+++ b/src/Core/Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj
@@ -0,0 +1,19 @@
+
+
+ net10.0
+ enable
+ enable
+ Infrastructure.Jwt
+
+
+
+
+
+
+
diff --git a/src/Core/Service/Service.Core/Jwt/JwtService.cs b/src/Core/Infrastructure/Infrastructure.Jwt/JwtService.cs
similarity index 79%
rename from src/Core/Service/Service.Core/Jwt/JwtService.cs
rename to src/Core/Infrastructure/Infrastructure.Jwt/JwtService.cs
index 5e91144..d0fba84 100644
--- a/src/Core/Service/Service.Core/Jwt/JwtService.cs
+++ b/src/Core/Infrastructure/Infrastructure.Jwt/JwtService.cs
@@ -4,22 +4,28 @@ using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames;
-namespace Service.Core.Jwt;
+namespace Infrastructure.Jwt;
+
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)
{
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)
var claims = new List
{
new(JwtRegisteredClaimNames.Sub, userId.ToString()),
new(JwtRegisteredClaimNames.UniqueName, username),
- new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
+ new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
var tokenDescriptor = new SecurityTokenDescriptor
@@ -28,7 +34,8 @@ public class JwtService : IJwtService
Expires = expiry,
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key),
- SecurityAlgorithms.HmacSha256)
+ SecurityAlgorithms.HmacSha256
+ ),
};
return handler.CreateToken(tokenDescriptor);
diff --git a/src/Core/Service/Service.Core/Password/PasswordService.cs b/src/Core/Infrastructure/Infrastructure.PasswordHashing/Argon2Infrastructure.cs
similarity index 79%
rename from src/Core/Service/Service.Core/Password/PasswordService.cs
rename to src/Core/Infrastructure/Infrastructure.PasswordHashing/Argon2Infrastructure.cs
index c66b47c..ff47a77 100644
--- a/src/Core/Service/Service.Core/Password/PasswordService.cs
+++ b/src/Core/Infrastructure/Infrastructure.PasswordHashing/Argon2Infrastructure.cs
@@ -2,9 +2,9 @@ using System.Security.Cryptography;
using System.Text;
using Konscious.Security.Cryptography;
-namespace Service.Core.Password;
+namespace Infrastructure.PasswordHashing;
-public class PasswordService : IPasswordService
+public class Argon2Infrastructure : IPasswordInfra
{
private const int SaltSize = 16; // 128-bit
private const int HashSize = 32; // 256-bit
@@ -19,7 +19,7 @@ public class PasswordService : IPasswordService
Salt = salt,
DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1),
MemorySize = ArgonMemoryKb,
- Iterations = ArgonIterations
+ Iterations = ArgonIterations,
};
var hash = argon2.GetBytes(HashSize);
@@ -30,8 +30,12 @@ public class PasswordService : IPasswordService
{
try
{
- var parts = stored.Split(':', StringSplitOptions.RemoveEmptyEntries);
- if (parts.Length != 2) return false;
+ var parts = stored.Split(
+ ':',
+ StringSplitOptions.RemoveEmptyEntries
+ );
+ if (parts.Length != 2)
+ return false;
var salt = Convert.FromBase64String(parts[0]);
var expected = Convert.FromBase64String(parts[1]);
@@ -41,7 +45,7 @@ public class PasswordService : IPasswordService
Salt = salt,
DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1),
MemorySize = ArgonMemoryKb,
- Iterations = ArgonIterations
+ Iterations = ArgonIterations,
};
var actual = argon2.GetBytes(expected.Length);
@@ -52,4 +56,4 @@ public class PasswordService : IPasswordService
return false;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Core/Service/Service.Core/Password/IPasswordService.cs b/src/Core/Infrastructure/Infrastructure.PasswordHashing/IPasswordInfra.cs
similarity index 56%
rename from src/Core/Service/Service.Core/Password/IPasswordService.cs
rename to src/Core/Infrastructure/Infrastructure.PasswordHashing/IPasswordInfra.cs
index 809fd8b..9a2df0b 100644
--- a/src/Core/Service/Service.Core/Password/IPasswordService.cs
+++ b/src/Core/Infrastructure/Infrastructure.PasswordHashing/IPasswordInfra.cs
@@ -1,7 +1,7 @@
-namespace Service.Core.Password;
+namespace Infrastructure.PasswordHashing;
-public interface IPasswordService
+public interface IPasswordInfra
{
public string Hash(string password);
public bool Verify(string password, string stored);
-}
\ No newline at end of file
+}
diff --git a/src/Core/Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj b/src/Core/Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj
new file mode 100644
index 0000000..97afa1f
--- /dev/null
+++ b/src/Core/Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj
@@ -0,0 +1,15 @@
+
+
+ net10.0
+ enable
+ enable
+ Infrastructure.PasswordHashing
+
+
+
+
+
+
diff --git a/src/Core/Repository/Repository.Tests/Auth/AuthRepository.test.cs b/src/Core/Infrastructure/Infrastructure.Repository.Tests/Auth/AuthRepository.test.cs
similarity index 52%
rename from src/Core/Repository/Repository.Tests/Auth/AuthRepository.test.cs
rename to src/Core/Infrastructure/Infrastructure.Repository.Tests/Auth/AuthRepository.test.cs
index 91b6d4a..f8bf9d8 100644
--- a/src/Core/Repository/Repository.Tests/Auth/AuthRepository.test.cs
+++ b/src/Core/Infrastructure/Infrastructure.Repository.Tests/Auth/AuthRepository.test.cs
@@ -1,15 +1,15 @@
-using Apps72.Dev.Data.DbMocker;
-using Repository.Core.Repositories.Auth;
-using FluentAssertions;
-using Repository.Tests.Database;
using System.Data;
+using Apps72.Dev.Data.DbMocker;
+using FluentAssertions;
+using Infrastructure.Repository.Auth;
+using Repository.Tests.Database;
namespace Repository.Tests.Auth;
public class AuthRepositoryTest
{
- private static AuthRepository CreateRepo(MockDbConnection conn)
- => new(new TestConnectionFactory(conn));
+ private static AuthRepository CreateRepo(MockDbConnection conn) =>
+ new(new TestConnectionFactory(conn));
[Fact]
public async Task RegisterUserAsync_CreatesUserWithCredential_ReturnsUserAccount()
@@ -17,10 +17,12 @@ public class AuthRepositoryTest
var expectedUserId = Guid.NewGuid();
var conn = new MockDbConnection();
- conn.Mocks
- .When(cmd => cmd.CommandText == "USP_RegisterUser")
- .ReturnsTable(MockTable.WithColumns(("UserAccountId", typeof(Guid)))
- .AddRow(expectedUserId));
+ conn.Mocks.When(cmd => cmd.CommandText == "USP_RegisterUser")
+ .ReturnsTable(
+ MockTable
+ .WithColumns(("UserAccountId", typeof(Guid)))
+ .AddRow(expectedUserId)
+ );
var repo = CreateRepo(conn);
var result = await repo.RegisterUserAsync(
@@ -47,29 +49,32 @@ public class AuthRepositoryTest
var userId = Guid.NewGuid();
var conn = new MockDbConnection();
- conn.Mocks
- .When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
- .ReturnsTable(MockTable.WithColumns(
- ("UserAccountId", typeof(Guid)),
- ("Username", typeof(string)),
- ("FirstName", typeof(string)),
- ("LastName", typeof(string)),
- ("Email", typeof(string)),
- ("CreatedAt", typeof(DateTime)),
- ("UpdatedAt", typeof(DateTime?)),
- ("DateOfBirth", typeof(DateTime)),
- ("Timer", typeof(byte[]))
- ).AddRow(
- userId,
- "emailuser",
- "Email",
- "User",
- "emailuser@example.com",
- DateTime.UtcNow,
- null,
- new DateTime(1990, 5, 15),
- null
- ));
+ conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
+ .ReturnsTable(
+ MockTable
+ .WithColumns(
+ ("UserAccountId", typeof(Guid)),
+ ("Username", typeof(string)),
+ ("FirstName", typeof(string)),
+ ("LastName", typeof(string)),
+ ("Email", typeof(string)),
+ ("CreatedAt", typeof(DateTime)),
+ ("UpdatedAt", typeof(DateTime?)),
+ ("DateOfBirth", typeof(DateTime)),
+ ("Timer", typeof(byte[]))
+ )
+ .AddRow(
+ userId,
+ "emailuser",
+ "Email",
+ "User",
+ "emailuser@example.com",
+ DateTime.UtcNow,
+ null,
+ new DateTime(1990, 5, 15),
+ null
+ )
+ );
var repo = CreateRepo(conn);
var result = await repo.GetUserByEmailAsync("emailuser@example.com");
@@ -87,8 +92,7 @@ public class AuthRepositoryTest
{
var conn = new MockDbConnection();
- conn.Mocks
- .When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
+ conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
.ReturnsTable(MockTable.Empty());
var repo = CreateRepo(conn);
@@ -103,29 +107,34 @@ public class AuthRepositoryTest
var userId = Guid.NewGuid();
var conn = new MockDbConnection();
- conn.Mocks
- .When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername")
- .ReturnsTable(MockTable.WithColumns(
- ("UserAccountId", typeof(Guid)),
- ("Username", typeof(string)),
- ("FirstName", typeof(string)),
- ("LastName", typeof(string)),
- ("Email", typeof(string)),
- ("CreatedAt", typeof(DateTime)),
- ("UpdatedAt", typeof(DateTime?)),
- ("DateOfBirth", typeof(DateTime)),
- ("Timer", typeof(byte[]))
- ).AddRow(
- userId,
- "usernameuser",
- "Username",
- "User",
- "username@example.com",
- DateTime.UtcNow,
- null,
- new DateTime(1985, 8, 20),
- null
- ));
+ conn.Mocks.When(cmd =>
+ cmd.CommandText == "usp_GetUserAccountByUsername"
+ )
+ .ReturnsTable(
+ MockTable
+ .WithColumns(
+ ("UserAccountId", typeof(Guid)),
+ ("Username", typeof(string)),
+ ("FirstName", typeof(string)),
+ ("LastName", typeof(string)),
+ ("Email", typeof(string)),
+ ("CreatedAt", typeof(DateTime)),
+ ("UpdatedAt", typeof(DateTime?)),
+ ("DateOfBirth", typeof(DateTime)),
+ ("Timer", typeof(byte[]))
+ )
+ .AddRow(
+ userId,
+ "usernameuser",
+ "Username",
+ "User",
+ "username@example.com",
+ DateTime.UtcNow,
+ null,
+ new DateTime(1985, 8, 20),
+ null
+ )
+ );
var repo = CreateRepo(conn);
var result = await repo.GetUserByUsernameAsync("usernameuser");
@@ -141,8 +150,9 @@ public class AuthRepositoryTest
{
var conn = new MockDbConnection();
- conn.Mocks
- .When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername")
+ conn.Mocks.When(cmd =>
+ cmd.CommandText == "usp_GetUserAccountByUsername"
+ )
.ReturnsTable(MockTable.Empty());
var repo = CreateRepo(conn);
@@ -158,21 +168,26 @@ public class AuthRepositoryTest
var credentialId = Guid.NewGuid();
var conn = new MockDbConnection();
- conn.Mocks
- .When(cmd => cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId")
- .ReturnsTable(MockTable.WithColumns(
- ("UserCredentialId", typeof(Guid)),
- ("UserAccountId", typeof(Guid)),
- ("Hash", typeof(string)),
- ("CreatedAt", typeof(DateTime)),
- ("Timer", typeof(byte[]))
- ).AddRow(
- credentialId,
- userId,
- "hashed_password_value",
- DateTime.UtcNow,
- null
- ));
+ conn.Mocks.When(cmd =>
+ cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId"
+ )
+ .ReturnsTable(
+ MockTable
+ .WithColumns(
+ ("UserCredentialId", typeof(Guid)),
+ ("UserAccountId", typeof(Guid)),
+ ("Hash", typeof(string)),
+ ("CreatedAt", typeof(DateTime)),
+ ("Timer", typeof(byte[]))
+ )
+ .AddRow(
+ credentialId,
+ userId,
+ "hashed_password_value",
+ DateTime.UtcNow,
+ null
+ )
+ );
var repo = CreateRepo(conn);
var result = await repo.GetActiveCredentialByUserAccountIdAsync(userId);
@@ -189,8 +204,9 @@ public class AuthRepositoryTest
var userId = Guid.NewGuid();
var conn = new MockDbConnection();
- conn.Mocks
- .When(cmd => cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId")
+ conn.Mocks.When(cmd =>
+ cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId"
+ )
.ReturnsTable(MockTable.Empty());
var repo = CreateRepo(conn);
@@ -206,14 +222,14 @@ public class AuthRepositoryTest
var newPasswordHash = "new_hashed_password";
var conn = new MockDbConnection();
- conn.Mocks
- .When(cmd => cmd.CommandText == "USP_RotateUserCredential")
+ conn.Mocks.When(cmd => cmd.CommandText == "USP_RotateUserCredential")
.ReturnsScalar(1);
var repo = CreateRepo(conn);
// Should not throw
- var act = async () => await repo.RotateCredentialAsync(userId, newPasswordHash);
+ var act = async () =>
+ await repo.RotateCredentialAsync(userId, newPasswordHash);
await act.Should().NotThrowAsync();
}
}
diff --git a/src/Core/Repository/Repository.Tests/Database/TestConnectionFactory.cs b/src/Core/Infrastructure/Infrastructure.Repository.Tests/Database/TestConnectionFactory.cs
similarity index 86%
rename from src/Core/Repository/Repository.Tests/Database/TestConnectionFactory.cs
rename to src/Core/Infrastructure/Infrastructure.Repository.Tests/Database/TestConnectionFactory.cs
index dc15914..f455382 100644
--- a/src/Core/Repository/Repository.Tests/Database/TestConnectionFactory.cs
+++ b/src/Core/Infrastructure/Infrastructure.Repository.Tests/Database/TestConnectionFactory.cs
@@ -1,10 +1,11 @@
using System.Data.Common;
-using Repository.Core.Sql;
+using Infrastructure.Repository.Sql;
namespace Repository.Tests.Database;
internal class TestConnectionFactory(DbConnection conn) : ISqlConnectionFactory
{
private readonly DbConnection _conn = conn;
+
public DbConnection CreateConnection() => _conn;
}
diff --git a/src/Core/Infrastructure/Infrastructure.Repository.Tests/Dockerfile b/src/Core/Infrastructure/Infrastructure.Repository.Tests/Dockerfile
new file mode 100644
index 0000000..2013eb1
--- /dev/null
+++ b/src/Core/Infrastructure/Infrastructure.Repository.Tests/Dockerfile
@@ -0,0 +1,15 @@
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+COPY ["Domain/Domain.csproj", "Domain/"]
+COPY ["Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj", "Infrastructure/Infrastructure.Repository/"]
+COPY ["Infrastructure/Infrastructure.Repository.Tests/Repository.Tests.csproj", "Infrastructure/Infrastructure.Repository.Tests/"]
+RUN dotnet restore "Infrastructure/Infrastructure.Repository.Tests/Repository.Tests.csproj"
+COPY . .
+WORKDIR "/src/Infrastructure/Infrastructure.Repository.Tests"
+RUN dotnet build "./Repository.Tests.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+FROM build AS final
+RUN mkdir -p /app/test-results
+WORKDIR /src/Infrastructure/Infrastructure.Repository.Tests
+ENTRYPOINT ["dotnet", "test", "./Repository.Tests.csproj", "-c", "Release", "--logger", "trx;LogFileName=/app/test-results/repository-tests.trx"]
diff --git a/src/Core/Repository/Repository.Tests/Repository.Tests.csproj b/src/Core/Infrastructure/Infrastructure.Repository.Tests/Infrastructure.Repository.Tests.csproj
similarity index 64%
rename from src/Core/Repository/Repository.Tests/Repository.Tests.csproj
rename to src/Core/Infrastructure/Infrastructure.Repository.Tests/Infrastructure.Repository.Tests.csproj
index a1d64e0..a71f674 100644
--- a/src/Core/Repository/Repository.Tests/Repository.Tests.csproj
+++ b/src/Core/Infrastructure/Infrastructure.Repository.Tests/Infrastructure.Repository.Tests.csproj
@@ -4,7 +4,7 @@
enable
enable
false
- Repository.Tests
+ Infrastructure.Repository.Tests
@@ -15,9 +15,18 @@
-
-
-
+
+
+
@@ -26,6 +35,6 @@
-
+
-
\ No newline at end of file
+
diff --git a/src/Core/Infrastructure/Infrastructure.Repository.Tests/UserAccount/UserAccountRepository.test.cs b/src/Core/Infrastructure/Infrastructure.Repository.Tests/UserAccount/UserAccountRepository.test.cs
new file mode 100644
index 0000000..d395658
--- /dev/null
+++ b/src/Core/Infrastructure/Infrastructure.Repository.Tests/UserAccount/UserAccountRepository.test.cs
@@ -0,0 +1,180 @@
+using Apps72.Dev.Data.DbMocker;
+using FluentAssertions;
+using Infrastructure.Repository.UserAccount;
+using Repository.Tests.Database;
+
+namespace Repository.Tests.UserAccount;
+
+public class UserAccountRepositoryTest
+{
+ private static UserAccountRepository CreateRepo(MockDbConnection conn) =>
+ new(new TestConnectionFactory(conn));
+
+ [Fact]
+ public async Task GetByIdAsync_ReturnsRow_Mapped()
+ {
+ var conn = new MockDbConnection();
+ conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountById")
+ .ReturnsTable(
+ MockTable
+ .WithColumns(
+ ("UserAccountId", typeof(Guid)),
+ ("Username", typeof(string)),
+ ("FirstName", typeof(string)),
+ ("LastName", typeof(string)),
+ ("Email", typeof(string)),
+ ("CreatedAt", typeof(DateTime)),
+ ("UpdatedAt", typeof(DateTime?)),
+ ("DateOfBirth", typeof(DateTime)),
+ ("Timer", typeof(byte[]))
+ )
+ .AddRow(
+ 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 result = await repo.GetByIdAsync(
+ Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
+ );
+
+ result.Should().NotBeNull();
+ result!.Username.Should().Be("yerb");
+ result.Email.Should().Be("aaronpo@example.com");
+ }
+
+ [Fact]
+ public async Task GetAllAsync_ReturnsMultipleRows()
+ {
+ var conn = new MockDbConnection();
+ conn.Mocks.When(cmd => cmd.CommandText == "usp_GetAllUserAccounts")
+ .ReturnsTable(
+ MockTable
+ .WithColumns(
+ ("UserAccountId", typeof(Guid)),
+ ("Username", typeof(string)),
+ ("FirstName", typeof(string)),
+ ("LastName", typeof(string)),
+ ("Email", typeof(string)),
+ ("CreatedAt", typeof(DateTime)),
+ ("UpdatedAt", typeof(DateTime?)),
+ ("DateOfBirth", typeof(DateTime)),
+ ("Timer", typeof(byte[]))
+ )
+ .AddRow(
+ 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 results = (await repo.GetAllAsync(null, null)).ToList();
+ results.Should().HaveCount(2);
+ results
+ .Select(r => r.Username)
+ .Should()
+ .BeEquivalentTo(new[] { "a", "b" });
+ }
+
+ [Fact]
+ public async Task GetByUsername_ReturnsRow()
+ {
+ var conn = new MockDbConnection();
+ conn.Mocks.When(cmd =>
+ cmd.CommandText == "usp_GetUserAccountByUsername"
+ )
+ .ReturnsTable(
+ MockTable
+ .WithColumns(
+ ("UserAccountId", typeof(Guid)),
+ ("Username", typeof(string)),
+ ("FirstName", typeof(string)),
+ ("LastName", typeof(string)),
+ ("Email", typeof(string)),
+ ("CreatedAt", typeof(DateTime)),
+ ("UpdatedAt", typeof(DateTime?)),
+ ("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 result = await repo.GetByUsernameAsync("lookupuser");
+ result.Should().NotBeNull();
+ result!.Email.Should().Be("lookup@example.com");
+ }
+
+ [Fact]
+ public async Task GetByEmail_ReturnsRow()
+ {
+ var conn = new MockDbConnection();
+ conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
+ .ReturnsTable(
+ MockTable
+ .WithColumns(
+ ("UserAccountId", typeof(Guid)),
+ ("Username", typeof(string)),
+ ("FirstName", typeof(string)),
+ ("LastName", typeof(string)),
+ ("Email", typeof(string)),
+ ("CreatedAt", typeof(DateTime)),
+ ("UpdatedAt", typeof(DateTime?)),
+ ("DateOfBirth", typeof(DateTime)),
+ ("Timer", typeof(byte[]))
+ )
+ .AddRow(
+ Guid.NewGuid(),
+ "byemail",
+ "B",
+ "E",
+ "byemail@example.com",
+ DateTime.UtcNow,
+ null,
+ DateTime.UtcNow.Date,
+ null
+ )
+ );
+
+ var repo = CreateRepo(conn);
+ var result = await repo.GetByEmailAsync("byemail@example.com");
+ result.Should().NotBeNull();
+ result!.Username.Should().Be("byemail");
+ }
+}
diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Auth/AuthRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Auth/AuthRepository.cs
new file mode 100644
index 0000000..4672852
--- /dev/null
+++ b/src/Core/Infrastructure/Infrastructure.Repository/Auth/AuthRepository.cs
@@ -0,0 +1,195 @@
+using System.Data;
+using System.Data.Common;
+using Domain.Entities;
+using Infrastructure.Repository.Sql;
+
+namespace Infrastructure.Repository.Auth;
+
+public class AuthRepository(ISqlConnectionFactory connectionFactory)
+ : Repository(connectionFactory),
+ IAuthRepository
+{
+ public async Task RegisterUserAsync(
+ string username,
+ string firstName,
+ string lastName,
+ string email,
+ DateTime dateOfBirth,
+ string passwordHash
+ )
+ {
+ await using var connection = await CreateConnection();
+ await using var command = connection.CreateCommand();
+
+ command.CommandText = "USP_RegisterUser";
+ command.CommandType = CommandType.StoredProcedure;
+
+ AddParameter(command, "@Username", username);
+ AddParameter(command, "@FirstName", firstName);
+ AddParameter(command, "@LastName", lastName);
+ AddParameter(command, "@Email", email);
+ AddParameter(command, "@DateOfBirth", dateOfBirth);
+ AddParameter(command, "@Hash", passwordHash);
+
+ var result = await command.ExecuteScalarAsync();
+ var userAccountId = result != null ? (Guid)result : Guid.Empty;
+
+ return new Domain.Entities.UserAccount
+ {
+ UserAccountId = userAccountId,
+ Username = username,
+ FirstName = firstName,
+ LastName = lastName,
+ Email = email,
+ DateOfBirth = dateOfBirth,
+ CreatedAt = DateTime.UtcNow,
+ };
+ }
+
+ public async Task GetUserByEmailAsync(
+ string email
+ )
+ {
+ await using var connection = await CreateConnection();
+ await using var command = connection.CreateCommand();
+ command.CommandText = "usp_GetUserAccountByEmail";
+ command.CommandType = CommandType.StoredProcedure;
+
+ AddParameter(command, "@Email", email);
+
+ await using var reader = await command.ExecuteReaderAsync();
+ return await reader.ReadAsync() ? MapToEntity(reader) : null;
+ }
+
+ public async Task GetUserByUsernameAsync(
+ string username
+ )
+ {
+ await using var connection = await CreateConnection();
+ await using var command = connection.CreateCommand();
+ command.CommandText = "usp_GetUserAccountByUsername";
+ command.CommandType = CommandType.StoredProcedure;
+
+ AddParameter(command, "@Username", username);
+
+ await using var reader = await command.ExecuteReaderAsync();
+ return await reader.ReadAsync() ? MapToEntity(reader) : null;
+ }
+
+ public async Task GetActiveCredentialByUserAccountIdAsync(
+ Guid userAccountId
+ )
+ {
+ await using var connection = await CreateConnection();
+ await using var command = connection.CreateCommand();
+ command.CommandText = "USP_GetActiveUserCredentialByUserAccountId";
+ command.CommandType = CommandType.StoredProcedure;
+
+ AddParameter(command, "@UserAccountId", userAccountId);
+
+ await using var reader = await command.ExecuteReaderAsync();
+ return await reader.ReadAsync()
+ ? MapToCredentialEntity(reader)
+ : null;
+ }
+
+ public async Task RotateCredentialAsync(
+ Guid userAccountId,
+ string newPasswordHash
+ )
+ {
+ await using var connection = await CreateConnection();
+ await using var command = connection.CreateCommand();
+ command.CommandText = "USP_RotateUserCredential";
+ command.CommandType = CommandType.StoredProcedure;
+
+ AddParameter(command, "@UserAccountId_", userAccountId);
+ AddParameter(command, "@Hash", newPasswordHash);
+
+ await command.ExecuteNonQueryAsync();
+ }
+
+ ///
+ /// Maps a data reader row to a UserAccount entity.
+ ///
+ protected override Domain.Entities.UserAccount MapToEntity(
+ DbDataReader reader
+ )
+ {
+ return new Domain.Entities.UserAccount
+ {
+ UserAccountId = reader.GetGuid(
+ reader.GetOrdinal("UserAccountId")
+ ),
+ Username = reader.GetString(reader.GetOrdinal("Username")),
+ FirstName = reader.GetString(reader.GetOrdinal("FirstName")),
+ LastName = reader.GetString(reader.GetOrdinal("LastName")),
+ Email = reader.GetString(reader.GetOrdinal("Email")),
+ CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")),
+ UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt"))
+ ? null
+ : reader.GetDateTime(reader.GetOrdinal("UpdatedAt")),
+ DateOfBirth = reader.GetDateTime(
+ reader.GetOrdinal("DateOfBirth")
+ ),
+ Timer = reader.IsDBNull(reader.GetOrdinal("Timer"))
+ ? null
+ : (byte[])reader["Timer"],
+ };
+ }
+
+ ///
+ /// Maps a data reader row to a UserCredential entity.
+ ///
+ private static UserCredential MapToCredentialEntity(DbDataReader reader)
+ {
+ var entity = new UserCredential
+ {
+ UserCredentialId = reader.GetGuid(
+ reader.GetOrdinal("UserCredentialId")
+ ),
+ UserAccountId = reader.GetGuid(
+ reader.GetOrdinal("UserAccountId")
+ ),
+ Hash = reader.GetString(reader.GetOrdinal("Hash")),
+ CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")),
+ };
+
+ // Optional columns
+ var hasTimer =
+ reader
+ .GetSchemaTable()
+ ?.Rows.Cast()
+ .Any(r =>
+ string.Equals(
+ r["ColumnName"]?.ToString(),
+ "Timer",
+ StringComparison.OrdinalIgnoreCase
+ )
+ ) ?? false;
+
+ if (hasTimer)
+ {
+ entity.Timer = reader.IsDBNull(reader.GetOrdinal("Timer"))
+ ? null
+ : (byte[])reader["Timer"];
+ }
+
+ return entity;
+ }
+
+ ///
+ /// Helper method to add a parameter to a database command.
+ ///
+ private static void AddParameter(
+ DbCommand command,
+ string name,
+ object? value
+ )
+ {
+ var p = command.CreateParameter();
+ p.ParameterName = name;
+ p.Value = value ?? DBNull.Value;
+ command.Parameters.Add(p);
+ }
+}
\ No newline at end of file
diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Auth/IAuthRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Auth/IAuthRepository.cs
new file mode 100644
index 0000000..4a57360
--- /dev/null
+++ b/src/Core/Infrastructure/Infrastructure.Repository/Auth/IAuthRepository.cs
@@ -0,0 +1,67 @@
+using Domain.Entities;
+
+namespace Infrastructure.Repository.Auth;
+
+///
+/// Repository for authentication-related database operations including user registration and credential management.
+///
+public interface IAuthRepository
+{
+ ///
+ /// Registers a new user with account details and initial credential.
+ /// Uses stored procedure: USP_RegisterUser
+ ///
+ /// Unique username for the user
+ /// User's first name
+ /// User's last name
+ /// User's email address
+ /// User's date of birth
+ /// Hashed password
+ /// The newly created UserAccount with generated ID
+ Task RegisterUserAsync(
+ string username,
+ string firstName,
+ string lastName,
+ string email,
+ DateTime dateOfBirth,
+ string passwordHash
+ );
+
+ ///
+ /// Retrieves a user account by email address (typically used for login).
+ /// Uses stored procedure: usp_GetUserAccountByEmail
+ ///
+ /// Email address to search for
+ /// UserAccount if found, null otherwise
+ Task GetUserByEmailAsync(
+ string email
+ );
+
+ ///
+ /// Retrieves a user account by username (typically used for login).
+ /// Uses stored procedure: usp_GetUserAccountByUsername
+ ///
+ /// Username to search for
+ /// UserAccount if found, null otherwise
+ Task GetUserByUsernameAsync(
+ string username
+ );
+
+ ///
+ /// Retrieves the active (non-revoked) credential for a user account.
+ /// Uses stored procedure: USP_GetActiveUserCredentialByUserAccountId
+ ///
+ /// ID of the user account
+ /// Active UserCredential if found, null otherwise
+ Task GetActiveCredentialByUserAccountIdAsync(
+ Guid userAccountId
+ );
+
+ ///
+ /// Rotates a user's credential by invalidating all existing credentials and creating a new one.
+ /// Uses stored procedure: USP_RotateUserCredential
+ ///
+ /// ID of the user account
+ /// New hashed password
+ Task RotateCredentialAsync(Guid userAccountId, string newPasswordHash);
+}
\ No newline at end of file
diff --git a/src/Core/Repository/Repository.Core/Repository.Core.csproj b/src/Core/Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj
similarity index 76%
rename from src/Core/Repository/Repository.Core/Repository.Core.csproj
rename to src/Core/Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj
index 6c69c3a..178bda8 100644
--- a/src/Core/Repository/Repository.Core/Repository.Core.csproj
+++ b/src/Core/Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj
@@ -3,7 +3,7 @@
net10.0
enable
enable
- Repository.Core
+ Infrastructure.Repository
@@ -12,7 +12,10 @@
Version="160.1000.6"
/>
-
+
diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.cs
new file mode 100644
index 0000000..30672d9
--- /dev/null
+++ b/src/Core/Infrastructure/Infrastructure.Repository/Repository.cs
@@ -0,0 +1,17 @@
+using System.Data.Common;
+using Infrastructure.Repository.Sql;
+
+namespace Infrastructure.Repository;
+
+public abstract class Repository(ISqlConnectionFactory connectionFactory)
+ where T : class
+{
+ protected async Task CreateConnection()
+ {
+ var connection = connectionFactory.CreateConnection();
+ await connection.OpenAsync();
+ return connection;
+ }
+
+ protected abstract T MapToEntity(DbDataReader reader);
+}
\ No newline at end of file
diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Sql/DefaultSqlConnectionFactory.cs b/src/Core/Infrastructure/Infrastructure.Repository/Sql/DefaultSqlConnectionFactory.cs
new file mode 100644
index 0000000..8d5bf62
--- /dev/null
+++ b/src/Core/Infrastructure/Infrastructure.Repository/Sql/DefaultSqlConnectionFactory.cs
@@ -0,0 +1,49 @@
+using System.Data.Common;
+using Microsoft.Data.SqlClient;
+using Microsoft.Extensions.Configuration;
+
+namespace Infrastructure.Repository.Sql;
+
+public class DefaultSqlConnectionFactory(IConfiguration configuration)
+ : ISqlConnectionFactory
+{
+ private readonly string _connectionString = GetConnectionString(
+ configuration
+ );
+
+ private static string GetConnectionString(IConfiguration configuration)
+ {
+ // Check for full connection string first
+ var fullConnectionString = Environment.GetEnvironmentVariable(
+ "DB_CONNECTION_STRING"
+ );
+ if (!string.IsNullOrEmpty(fullConnectionString))
+ {
+ return fullConnectionString;
+ }
+
+ // Try to build from individual environment variables (preferred method for Docker)
+ try
+ {
+ return SqlConnectionStringHelper.BuildConnectionString();
+ }
+ catch (InvalidOperationException)
+ {
+ // Fall back to configuration-based connection string if env vars are not set
+ var connString = configuration.GetConnectionString("Default");
+ if (!string.IsNullOrEmpty(connString))
+ {
+ return connString;
+ }
+
+ throw new InvalidOperationException(
+ "Database connection string not configured. Set DB_CONNECTION_STRING or DB_SERVER, DB_NAME, DB_USER, DB_PASSWORD env vars or ConnectionStrings:Default."
+ );
+ }
+ }
+
+ public DbConnection CreateConnection()
+ {
+ return new SqlConnection(_connectionString);
+ }
+}
\ No newline at end of file
diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Sql/ISqlConnectionFactory.cs b/src/Core/Infrastructure/Infrastructure.Repository/Sql/ISqlConnectionFactory.cs
new file mode 100644
index 0000000..40a6eed
--- /dev/null
+++ b/src/Core/Infrastructure/Infrastructure.Repository/Sql/ISqlConnectionFactory.cs
@@ -0,0 +1,8 @@
+using System.Data.Common;
+
+namespace Infrastructure.Repository.Sql;
+
+public interface ISqlConnectionFactory
+{
+ DbConnection CreateConnection();
+}
\ No newline at end of file
diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Sql/SqlConnectionStringHelper.cs b/src/Core/Infrastructure/Infrastructure.Repository/Sql/SqlConnectionStringHelper.cs
new file mode 100644
index 0000000..27daeff
--- /dev/null
+++ b/src/Core/Infrastructure/Infrastructure.Repository/Sql/SqlConnectionStringHelper.cs
@@ -0,0 +1,61 @@
+using Microsoft.Data.SqlClient;
+
+namespace Infrastructure.Repository.Sql;
+
+public static class SqlConnectionStringHelper
+{
+ ///
+ /// Builds a SQL Server connection string from environment variables.
+ /// Expects DB_SERVER, DB_NAME, DB_USER, DB_PASSWORD, and DB_TRUST_SERVER_CERTIFICATE.
+ ///
+ /// Optional override for the database name. If null, uses DB_NAME env var.
+ /// A properly formatted SQL Server connection string.
+ public static string BuildConnectionString(string? databaseName = null)
+ {
+ var server =
+ Environment.GetEnvironmentVariable("DB_SERVER")
+ ?? throw new InvalidOperationException(
+ "DB_SERVER environment variable is not set"
+ );
+
+ var dbName =
+ databaseName
+ ?? Environment.GetEnvironmentVariable("DB_NAME")
+ ?? throw new InvalidOperationException(
+ "DB_NAME environment variable is not set"
+ );
+
+ var user =
+ Environment.GetEnvironmentVariable("DB_USER")
+ ?? throw new InvalidOperationException(
+ "DB_USER environment variable is not set"
+ );
+
+ var password =
+ Environment.GetEnvironmentVariable("DB_PASSWORD")
+ ?? throw new InvalidOperationException(
+ "DB_PASSWORD environment variable is not set"
+ );
+
+ var builder = new SqlConnectionStringBuilder
+ {
+ DataSource = server,
+ InitialCatalog = dbName,
+ UserID = user,
+ Password = password,
+ TrustServerCertificate = true,
+ Encrypt = true,
+ };
+
+ return builder.ConnectionString;
+ }
+
+ ///
+ /// Builds a connection string to the master database using environment variables.
+ ///
+ /// A connection string for the master database.
+ public static string BuildMasterConnectionString()
+ {
+ return BuildConnectionString("master");
+ }
+}
\ No newline at end of file
diff --git a/src/Core/Infrastructure/Infrastructure.Repository/UserAccount/IUserAccountRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/UserAccount/IUserAccountRepository.cs
new file mode 100644
index 0000000..774f825
--- /dev/null
+++ b/src/Core/Infrastructure/Infrastructure.Repository/UserAccount/IUserAccountRepository.cs
@@ -0,0 +1,16 @@
+namespace Infrastructure.Repository.UserAccount;
+
+public interface IUserAccountRepository
+{
+ Task GetByIdAsync(Guid id);
+ Task> GetAllAsync(
+ int? limit,
+ int? offset
+ );
+ Task UpdateAsync(Domain.Entities.UserAccount userAccount);
+ Task DeleteAsync(Guid id);
+ Task GetByUsernameAsync(
+ string username
+ );
+ Task GetByEmailAsync(string email);
+}
\ No newline at end of file
diff --git a/src/Core/Infrastructure/Infrastructure.Repository/UserAccount/UserAccountRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/UserAccount/UserAccountRepository.cs
new file mode 100644
index 0000000..d07fa3c
--- /dev/null
+++ b/src/Core/Infrastructure/Infrastructure.Repository/UserAccount/UserAccountRepository.cs
@@ -0,0 +1,149 @@
+using System.Data;
+using System.Data.Common;
+using Infrastructure.Repository.Sql;
+
+namespace Infrastructure.Repository.UserAccount;
+
+public class UserAccountRepository(ISqlConnectionFactory connectionFactory)
+ : Repository(connectionFactory),
+ IUserAccountRepository
+{
+ public async Task GetByIdAsync(
+ Guid id
+ )
+ {
+ await using var connection = await CreateConnection();
+ await using var command = connection.CreateCommand();
+ command.CommandText = "usp_GetUserAccountById";
+ command.CommandType = CommandType.StoredProcedure;
+
+ AddParameter(command, "@UserAccountId", id);
+
+ await using var reader = await command.ExecuteReaderAsync();
+ return await reader.ReadAsync() ? MapToEntity(reader) : null;
+ }
+
+ public async Task<
+ IEnumerable
+ > GetAllAsync(int? limit, int? offset)
+ {
+ await using var connection = await CreateConnection();
+ await using var command = connection.CreateCommand();
+ command.CommandText = "usp_GetAllUserAccounts";
+ command.CommandType = CommandType.StoredProcedure;
+
+ if (limit.HasValue)
+ AddParameter(command, "@Limit", limit.Value);
+
+ if (offset.HasValue)
+ AddParameter(command, "@Offset", offset.Value);
+
+ await using var reader = await command.ExecuteReaderAsync();
+ var users = new List();
+
+ while (await reader.ReadAsync())
+ {
+ users.Add(MapToEntity(reader));
+ }
+
+ return users;
+ }
+
+ public async Task UpdateAsync(
+ Domain.Entities.UserAccount userAccount
+ )
+ {
+ await using var connection = await CreateConnection();
+ await using var command = connection.CreateCommand();
+ command.CommandText = "usp_UpdateUserAccount";
+ command.CommandType = CommandType.StoredProcedure;
+
+ AddParameter(command, "@UserAccountId", userAccount.UserAccountId);
+ AddParameter(command, "@Username", userAccount.Username);
+ AddParameter(command, "@FirstName", userAccount.FirstName);
+ AddParameter(command, "@LastName", userAccount.LastName);
+ AddParameter(command, "@Email", userAccount.Email);
+ AddParameter(command, "@DateOfBirth", userAccount.DateOfBirth);
+
+ await command.ExecuteNonQueryAsync();
+ }
+
+ public async Task DeleteAsync(Guid id)
+ {
+ await using var connection = await CreateConnection();
+ await using var command = connection.CreateCommand();
+ command.CommandText = "usp_DeleteUserAccount";
+ command.CommandType = CommandType.StoredProcedure;
+
+ AddParameter(command, "@UserAccountId", id);
+ await command.ExecuteNonQueryAsync();
+ }
+
+ public async Task GetByUsernameAsync(
+ string username
+ )
+ {
+ await using var connection = await CreateConnection();
+ await using var command = connection.CreateCommand();
+ command.CommandText = "usp_GetUserAccountByUsername";
+ command.CommandType = CommandType.StoredProcedure;
+
+ AddParameter(command, "@Username", username);
+
+ await using var reader = await command.ExecuteReaderAsync();
+ return await reader.ReadAsync() ? MapToEntity(reader) : null;
+ }
+
+ public async Task GetByEmailAsync(
+ string email
+ )
+ {
+ await using var connection = await CreateConnection();
+ await using var command = connection.CreateCommand();
+ command.CommandText = "usp_GetUserAccountByEmail";
+ command.CommandType = CommandType.StoredProcedure;
+
+ AddParameter(command, "@Email", email);
+
+ await using var reader = await command.ExecuteReaderAsync();
+ return await reader.ReadAsync() ? MapToEntity(reader) : null;
+ }
+
+ protected override Domain.Entities.UserAccount MapToEntity(
+ DbDataReader reader
+ )
+ {
+ return new Domain.Entities.UserAccount
+ {
+ UserAccountId = reader.GetGuid(
+ reader.GetOrdinal("UserAccountId")
+ ),
+ Username = reader.GetString(reader.GetOrdinal("Username")),
+ FirstName = reader.GetString(reader.GetOrdinal("FirstName")),
+ LastName = reader.GetString(reader.GetOrdinal("LastName")),
+ Email = reader.GetString(reader.GetOrdinal("Email")),
+ CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")),
+ UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt"))
+ ? null
+ : reader.GetDateTime(reader.GetOrdinal("UpdatedAt")),
+ DateOfBirth = reader.GetDateTime(
+ reader.GetOrdinal("DateOfBirth")
+ ),
+ Timer = reader.IsDBNull(reader.GetOrdinal("Timer"))
+ ? null
+ : (byte[])reader["Timer"],
+ };
+ }
+
+ private static void AddParameter(
+ DbCommand command,
+ string name,
+ object? value
+ )
+ {
+ var p = command.CreateParameter();
+ p.ParameterName = name;
+ p.Value = value ?? DBNull.Value;
+ command.Parameters.Add(p);
+ }
+}
\ No newline at end of file
diff --git a/src/Core/Repository/Repository.Core/Repositories/Auth/AuthRepository.cs b/src/Core/Repository/Repository.Core/Repositories/Auth/AuthRepository.cs
deleted file mode 100644
index efc3d8a..0000000
--- a/src/Core/Repository/Repository.Core/Repositories/Auth/AuthRepository.cs
+++ /dev/null
@@ -1,168 +0,0 @@
-using System.Data;
-using System.Data.Common;
-using Domain.Core.Entities;
-using Repository.Core.Sql;
-
-namespace Repository.Core.Repositories.Auth
-{
- public class AuthRepository : Repository, IAuthRepository
- {
- public AuthRepository(ISqlConnectionFactory connectionFactory)
- : base(connectionFactory)
- {
- }
-
-
- public async Task RegisterUserAsync(
- string username,
- string firstName,
- string lastName,
- string email,
- DateTime dateOfBirth,
- string passwordHash)
- {
- await using var connection = await CreateConnection();
- await using var command = connection.CreateCommand();
-
- command.CommandText = "USP_RegisterUser";
- command.CommandType = CommandType.StoredProcedure;
-
- AddParameter(command, "@Username", username);
- AddParameter(command, "@FirstName", firstName);
- AddParameter(command, "@LastName", lastName);
- AddParameter(command, "@Email", email);
- AddParameter(command, "@DateOfBirth", dateOfBirth);
- AddParameter(command, "@Hash", passwordHash);
-
- var result = await command.ExecuteScalarAsync();
- var userAccountId = result != null ? (Guid)result : Guid.Empty;
-
- return new Domain.Core.Entities.UserAccount
- {
- UserAccountId = userAccountId,
- Username = username,
- FirstName = firstName,
- LastName = lastName,
- Email = email,
- DateOfBirth = dateOfBirth,
- CreatedAt = DateTime.UtcNow
- };
- }
-
-
- public async Task GetUserByEmailAsync(string email)
- {
- await using var connection = await CreateConnection();
- await using var command = connection.CreateCommand();
- command.CommandText = "usp_GetUserAccountByEmail";
- command.CommandType = CommandType.StoredProcedure;
-
- AddParameter(command, "@Email", email);
-
- await using var reader = await command.ExecuteReaderAsync();
- return await reader.ReadAsync() ? MapToEntity(reader) : null;
- }
-
-
- public async Task GetUserByUsernameAsync(string username)
- {
- await using var connection = await CreateConnection();
- await using var command = connection.CreateCommand();
- command.CommandText = "usp_GetUserAccountByUsername";
- command.CommandType = CommandType.StoredProcedure;
-
- AddParameter(command, "@Username", username);
-
- await using var reader = await command.ExecuteReaderAsync();
- return await reader.ReadAsync() ? MapToEntity(reader) : null;
- }
-
- public async Task GetActiveCredentialByUserAccountIdAsync(Guid userAccountId)
- {
- await using var connection = await CreateConnection();
- await using var command = connection.CreateCommand();
- command.CommandText = "USP_GetActiveUserCredentialByUserAccountId";
- command.CommandType = CommandType.StoredProcedure;
-
- AddParameter(command, "@UserAccountId", userAccountId);
-
- await using var reader = await command.ExecuteReaderAsync();
- return await reader.ReadAsync() ? MapToCredentialEntity(reader) : null;
- }
-
- public async Task RotateCredentialAsync(Guid userAccountId, string newPasswordHash)
- {
- await using var connection = await CreateConnection();
- await using var command = connection.CreateCommand();
- command.CommandText = "USP_RotateUserCredential";
- command.CommandType = CommandType.StoredProcedure;
-
- AddParameter(command, "@UserAccountId_", userAccountId);
- AddParameter(command, "@Hash", newPasswordHash);
-
- await command.ExecuteNonQueryAsync();
- }
-
- ///
- /// Maps a data reader row to a UserAccount entity.
- ///
- protected override Domain.Core.Entities.UserAccount MapToEntity(DbDataReader reader)
- {
- return new Domain.Core.Entities.UserAccount
- {
- UserAccountId = reader.GetGuid(reader.GetOrdinal("UserAccountId")),
- Username = reader.GetString(reader.GetOrdinal("Username")),
- FirstName = reader.GetString(reader.GetOrdinal("FirstName")),
- LastName = reader.GetString(reader.GetOrdinal("LastName")),
- Email = reader.GetString(reader.GetOrdinal("Email")),
- CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")),
- UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt"))
- ? null
- : reader.GetDateTime(reader.GetOrdinal("UpdatedAt")),
- DateOfBirth = reader.GetDateTime(reader.GetOrdinal("DateOfBirth")),
- Timer = reader.IsDBNull(reader.GetOrdinal("Timer"))
- ? null
- : (byte[])reader["Timer"]
- };
- }
-
- ///
- /// Maps a data reader row to a UserCredential entity.
- ///
- private static UserCredential MapToCredentialEntity(DbDataReader reader)
- {
- var entity = new UserCredential
- {
- UserCredentialId = reader.GetGuid(reader.GetOrdinal("UserCredentialId")),
- UserAccountId = reader.GetGuid(reader.GetOrdinal("UserAccountId")),
- Hash = reader.GetString(reader.GetOrdinal("Hash")),
- CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt"))
- };
-
- // Optional columns
- var hasTimer = reader.GetSchemaTable()?.Rows
- .Cast()
- .Any(r => string.Equals(r["ColumnName"]?.ToString(), "Timer",
- StringComparison.OrdinalIgnoreCase)) ??
- false;
-
- if (hasTimer)
- {
- entity.Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) ? null : (byte[])reader["Timer"];
- }
-
- return entity;
- }
-
- ///
- /// Helper method to add a parameter to a database command.
- ///
- private static void AddParameter(DbCommand command, string name, object? value)
- {
- var p = command.CreateParameter();
- p.ParameterName = name;
- p.Value = value ?? DBNull.Value;
- command.Parameters.Add(p);
- }
- }
-}
diff --git a/src/Core/Repository/Repository.Core/Repositories/Auth/IAuthRepository.cs b/src/Core/Repository/Repository.Core/Repositories/Auth/IAuthRepository.cs
deleted file mode 100644
index 2f96e96..0000000
--- a/src/Core/Repository/Repository.Core/Repositories/Auth/IAuthRepository.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using Domain.Core.Entities;
-
-namespace Repository.Core.Repositories.Auth
-{
- ///
- /// Repository for authentication-related database operations including user registration and credential management.
- ///
- public interface IAuthRepository
- {
- ///
- /// Registers a new user with account details and initial credential.
- /// Uses stored procedure: USP_RegisterUser
- ///
- /// Unique username for the user
- /// User's first name
- /// User's last name
- /// User's email address
- /// User's date of birth
- /// Hashed password
- /// The newly created UserAccount with generated ID
- Task RegisterUserAsync(
- string username,
- string firstName,
- string lastName,
- string email,
- DateTime dateOfBirth,
- string passwordHash);
-
- ///
- /// Retrieves a user account by email address (typically used for login).
- /// Uses stored procedure: usp_GetUserAccountByEmail
- ///
- /// Email address to search for
- /// UserAccount if found, null otherwise
- Task GetUserByEmailAsync(string email);
-
- ///
- /// Retrieves a user account by username (typically used for login).
- /// Uses stored procedure: usp_GetUserAccountByUsername
- ///
- /// Username to search for
- /// UserAccount if found, null otherwise
- Task GetUserByUsernameAsync(string username);
-
- ///
- /// Retrieves the active (non-revoked) credential for a user account.
- /// Uses stored procedure: USP_GetActiveUserCredentialByUserAccountId
- ///
- /// ID of the user account
- /// Active UserCredential if found, null otherwise
- Task GetActiveCredentialByUserAccountIdAsync(Guid userAccountId);
-
- ///
- /// Rotates a user's credential by invalidating all existing credentials and creating a new one.
- /// Uses stored procedure: USP_RotateUserCredential
- ///
- /// ID of the user account
- /// New hashed password
- Task RotateCredentialAsync(Guid userAccountId, string newPasswordHash);
- }
-}
diff --git a/src/Core/Repository/Repository.Core/Repositories/Repository.cs b/src/Core/Repository/Repository.Core/Repositories/Repository.cs
deleted file mode 100644
index 6ac9eab..0000000
--- a/src/Core/Repository/Repository.Core/Repositories/Repository.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System.Data.Common;
-using Repository.Core.Sql;
-
-namespace Repository.Core.Repositories
-{
- public abstract class Repository(ISqlConnectionFactory connectionFactory)
- where T : class
- {
- protected async Task CreateConnection()
- {
- var connection = connectionFactory.CreateConnection();
- await connection.OpenAsync();
- return connection;
- }
-
- protected abstract T MapToEntity(DbDataReader reader);
- }
-}
diff --git a/src/Core/Repository/Repository.Core/Repositories/UserAccount/IUserAccountRepository.cs b/src/Core/Repository/Repository.Core/Repositories/UserAccount/IUserAccountRepository.cs
deleted file mode 100644
index d0bed2c..0000000
--- a/src/Core/Repository/Repository.Core/Repositories/UserAccount/IUserAccountRepository.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using Domain.Core.Entities;
-
-
-namespace Repository.Core.Repositories.UserAccount
-{
- public interface IUserAccountRepository
- {
- Task GetByIdAsync(Guid id);
- Task> GetAllAsync(int? limit, int? offset);
- Task UpdateAsync(Domain.Core.Entities.UserAccount userAccount);
- Task DeleteAsync(Guid id);
- Task GetByUsernameAsync(string username);
- Task GetByEmailAsync(string email);
- }
-}
diff --git a/src/Core/Repository/Repository.Core/Repositories/UserAccount/UserAccountRepository.cs b/src/Core/Repository/Repository.Core/Repositories/UserAccount/UserAccountRepository.cs
deleted file mode 100644
index 548522b..0000000
--- a/src/Core/Repository/Repository.Core/Repositories/UserAccount/UserAccountRepository.cs
+++ /dev/null
@@ -1,130 +0,0 @@
-using System.Data;
-using System.Data.Common;
-using Domain.Core.Entities;
-using Repository.Core.Sql;
-
-namespace Repository.Core.Repositories.UserAccount
-{
- public class UserAccountRepository(ISqlConnectionFactory connectionFactory)
- : Repository(connectionFactory), IUserAccountRepository
- {
- public async Task GetByIdAsync(Guid id)
- {
- await using var connection = await CreateConnection();
- await using var command = connection.CreateCommand();
- command.CommandText = "usp_GetUserAccountById";
- command.CommandType = CommandType.StoredProcedure;
-
- AddParameter(command, "@UserAccountId", id);
-
- await using var reader = await command.ExecuteReaderAsync();
- return await reader.ReadAsync() ? MapToEntity(reader) : null;
- }
-
- public async Task> GetAllAsync(int? limit, int? offset)
- {
- await using var connection = await CreateConnection();
- await using var command = connection.CreateCommand();
- command.CommandText = "usp_GetAllUserAccounts";
- command.CommandType = CommandType.StoredProcedure;
-
- if (limit.HasValue)
- AddParameter(command, "@Limit", limit.Value);
-
- if (offset.HasValue)
- AddParameter(command, "@Offset", offset.Value);
-
- await using var reader = await command.ExecuteReaderAsync();
- var users = new List();
-
- while (await reader.ReadAsync())
- {
- users.Add(MapToEntity(reader));
- }
-
- return users;
- }
-
- public async Task UpdateAsync(Domain.Core.Entities.UserAccount userAccount)
- {
- await using var connection = await CreateConnection();
- await using var command = connection.CreateCommand();
- command.CommandText = "usp_UpdateUserAccount";
- command.CommandType = CommandType.StoredProcedure;
-
- AddParameter(command, "@UserAccountId", userAccount.UserAccountId);
- AddParameter(command, "@Username", userAccount.Username);
- AddParameter(command, "@FirstName", userAccount.FirstName);
- AddParameter(command, "@LastName", userAccount.LastName);
- AddParameter(command, "@Email", userAccount.Email);
- AddParameter(command, "@DateOfBirth", userAccount.DateOfBirth);
-
- await command.ExecuteNonQueryAsync();
- }
-
- public async Task DeleteAsync(Guid id)
- {
- await using var connection = await CreateConnection();
- await using var command = connection.CreateCommand();
- command.CommandText = "usp_DeleteUserAccount";
- command.CommandType = CommandType.StoredProcedure;
-
- AddParameter(command, "@UserAccountId", id);
- await command.ExecuteNonQueryAsync();
- }
-
- public async Task GetByUsernameAsync(string username)
- {
- await using var connection = await CreateConnection();
- await using var command = connection.CreateCommand();
- command.CommandText = "usp_GetUserAccountByUsername";
- command.CommandType = CommandType.StoredProcedure;
-
- AddParameter(command, "@Username", username);
-
- await using var reader = await command.ExecuteReaderAsync();
- return await reader.ReadAsync() ? MapToEntity(reader) : null;
- }
-
- public async Task GetByEmailAsync(string email)
- {
- await using var connection = await CreateConnection();
- await using var command = connection.CreateCommand();
- command.CommandText = "usp_GetUserAccountByEmail";
- command.CommandType = CommandType.StoredProcedure;
-
- AddParameter(command, "@Email", email);
-
- await using var reader = await command.ExecuteReaderAsync();
- return await reader.ReadAsync() ? MapToEntity(reader) : null;
- }
-
- protected override Domain.Core.Entities.UserAccount MapToEntity(DbDataReader reader)
- {
- return new Domain.Core.Entities.UserAccount
- {
- UserAccountId = reader.GetGuid(reader.GetOrdinal("UserAccountId")),
- Username = reader.GetString(reader.GetOrdinal("Username")),
- FirstName = reader.GetString(reader.GetOrdinal("FirstName")),
- LastName = reader.GetString(reader.GetOrdinal("LastName")),
- Email = reader.GetString(reader.GetOrdinal("Email")),
- CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")),
- UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt"))
- ? null
- : reader.GetDateTime(reader.GetOrdinal("UpdatedAt")),
- DateOfBirth = reader.GetDateTime(reader.GetOrdinal("DateOfBirth")),
- Timer = reader.IsDBNull(reader.GetOrdinal("Timer"))
- ? null
- : (byte[])reader["Timer"]
- };
- }
-
- private static void AddParameter(DbCommand command, string name, object? value)
- {
- var p = command.CreateParameter();
- p.ParameterName = name;
- p.Value = value ?? DBNull.Value;
- command.Parameters.Add(p);
- }
- }
-}
diff --git a/src/Core/Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs b/src/Core/Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs
deleted file mode 100644
index b8cdec6..0000000
--- a/src/Core/Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using System.Data.Common;
-using Microsoft.Data.SqlClient;
-using Microsoft.Extensions.Configuration;
-
-
-namespace Repository.Core.Sql
-{
- public class DefaultSqlConnectionFactory(IConfiguration configuration) : ISqlConnectionFactory
- {
- private readonly string _connectionString = GetConnectionString(configuration);
-
- private static string GetConnectionString(IConfiguration configuration)
- {
- // Check for full connection string first
- var fullConnectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING");
- if (!string.IsNullOrEmpty(fullConnectionString))
- {
- return fullConnectionString;
- }
-
- // Try to build from individual environment variables (preferred method for Docker)
- try
- {
- return SqlConnectionStringHelper.BuildConnectionString();
- }
- catch (InvalidOperationException)
- {
- // Fall back to configuration-based connection string if env vars are not set
- var connString = configuration.GetConnectionString("Default");
- if (!string.IsNullOrEmpty(connString))
- {
- return connString;
- }
-
- throw new InvalidOperationException(
- "Database connection string not configured. Set DB_CONNECTION_STRING or DB_SERVER, DB_NAME, DB_USER, DB_PASSWORD env vars or ConnectionStrings:Default."
- );
- }
- }
-
- public DbConnection CreateConnection()
- {
- return new SqlConnection(_connectionString);
- }
- }
-}
diff --git a/src/Core/Repository/Repository.Core/Sql/ISqlConnectionFactory.cs b/src/Core/Repository/Repository.Core/Sql/ISqlConnectionFactory.cs
deleted file mode 100644
index c8be898..0000000
--- a/src/Core/Repository/Repository.Core/Sql/ISqlConnectionFactory.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using System.Data.Common;
-
-namespace Repository.Core.Sql
-{
- public interface ISqlConnectionFactory
- {
- DbConnection CreateConnection();
- }
-}
diff --git a/src/Core/Repository/Repository.Core/Sql/SqlConnectionStringHelper.cs b/src/Core/Repository/Repository.Core/Sql/SqlConnectionStringHelper.cs
deleted file mode 100644
index 7e50707..0000000
--- a/src/Core/Repository/Repository.Core/Sql/SqlConnectionStringHelper.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-using Microsoft.Data.SqlClient;
-
-namespace Repository.Core.Sql
-{
- public static class SqlConnectionStringHelper
- {
- ///
- /// Builds a SQL Server connection string from environment variables.
- /// Expects DB_SERVER, DB_NAME, DB_USER, DB_PASSWORD, and DB_TRUST_SERVER_CERTIFICATE.
- ///
- /// Optional override for the database name. If null, uses DB_NAME env var.
- /// A properly formatted SQL Server connection string.
- public static string BuildConnectionString(string? databaseName = null)
- {
- var server = Environment.GetEnvironmentVariable("DB_SERVER")
- ?? throw new InvalidOperationException("DB_SERVER environment variable is not set");
-
- var dbName = databaseName
- ?? Environment.GetEnvironmentVariable("DB_NAME")
- ?? throw new InvalidOperationException("DB_NAME environment variable is not set");
-
- var user = Environment.GetEnvironmentVariable("DB_USER")
- ?? throw new InvalidOperationException("DB_USER environment variable is not set");
-
- var password = Environment.GetEnvironmentVariable("DB_PASSWORD")
- ?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set");
-
- var builder = new SqlConnectionStringBuilder
- {
- DataSource = server,
- InitialCatalog = dbName,
- UserID = user,
- Password = password,
- TrustServerCertificate = true,
- Encrypt = true
- };
-
- return builder.ConnectionString;
- }
-
- ///
- /// Builds a connection string to the master database using environment variables.
- ///
- /// A connection string for the master database.
- public static string BuildMasterConnectionString()
- {
- return BuildConnectionString("master");
- }
- }
-}
diff --git a/src/Core/Repository/Repository.Tests/Dockerfile b/src/Core/Repository/Repository.Tests/Dockerfile
deleted file mode 100644
index 4b83399..0000000
--- a/src/Core/Repository/Repository.Tests/Dockerfile
+++ /dev/null
@@ -1,14 +0,0 @@
-FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
-ARG BUILD_CONFIGURATION=Release
-WORKDIR /src
-COPY ["Repository/Repository.Core/Repository.Core.csproj", "Repository/Repository.Core/"]
-COPY ["Repository/Repository.Tests/Repository.Tests.csproj", "Repository/Repository.Tests/"]
-RUN dotnet restore "Repository/Repository.Tests/Repository.Tests.csproj"
-COPY . .
-WORKDIR "/src/Repository/Repository.Tests"
-RUN dotnet build "./Repository.Tests.csproj" -c $BUILD_CONFIGURATION -o /app/build
-
-FROM build AS final
-RUN mkdir -p /app/test-results
-WORKDIR /src/Repository/Repository.Tests
-ENTRYPOINT ["dotnet", "test", "./Repository.Tests.csproj", "-c", "Release", "--logger", "trx;LogFileName=/app/test-results/repository-tests.trx"]
diff --git a/src/Core/Repository/Repository.Tests/UserAccount/UserAccountRepository.test.cs b/src/Core/Repository/Repository.Tests/UserAccount/UserAccountRepository.test.cs
deleted file mode 100644
index 5cf9a64..0000000
--- a/src/Core/Repository/Repository.Tests/UserAccount/UserAccountRepository.test.cs
+++ /dev/null
@@ -1,119 +0,0 @@
-using Apps72.Dev.Data.DbMocker;
-using Repository.Core.Repositories.UserAccount;
-using FluentAssertions;
-using Repository.Tests.Database;
-
-namespace Repository.Tests.UserAccount;
-
-public class UserAccountRepositoryTest
-{
- private static UserAccountRepository CreateRepo(MockDbConnection conn)
- => new(new TestConnectionFactory(conn));
-
- [Fact]
- public async Task GetByIdAsync_ReturnsRow_Mapped()
- {
- var conn = new MockDbConnection();
- conn.Mocks
- .When(cmd => cmd.CommandText == "usp_GetUserAccountById")
- .ReturnsTable(MockTable.WithColumns(
- ("UserAccountId", typeof(Guid)),
- ("Username", typeof(string)),
- ("FirstName", typeof(string)),
- ("LastName", typeof(string)),
- ("Email", typeof(string)),
- ("CreatedAt", typeof(DateTime)),
- ("UpdatedAt", typeof(DateTime?)),
- ("DateOfBirth", typeof(DateTime)),
- ("Timer", typeof(byte[]))
- ).AddRow(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 result = await repo.GetByIdAsync(Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"));
-
- result.Should().NotBeNull();
- result!.Username.Should().Be("yerb");
- result.Email.Should().Be("aaronpo@example.com");
- }
-
- [Fact]
- public async Task GetAllAsync_ReturnsMultipleRows()
- {
- var conn = new MockDbConnection();
- conn.Mocks
- .When(cmd => cmd.CommandText == "usp_GetAllUserAccounts")
- .ReturnsTable(MockTable.WithColumns(
- ("UserAccountId", typeof(Guid)),
- ("Username", typeof(string)),
- ("FirstName", typeof(string)),
- ("LastName", typeof(string)),
- ("Email", typeof(string)),
- ("CreatedAt", typeof(DateTime)),
- ("UpdatedAt", typeof(DateTime?)),
- ("DateOfBirth", typeof(DateTime)),
- ("Timer", typeof(byte[]))
- ).AddRow(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 results = (await repo.GetAllAsync(null, null)).ToList();
- results.Should().HaveCount(2);
- results.Select(r => r.Username).Should().BeEquivalentTo(new[] { "a", "b" });
- }
-
-
- [Fact]
- public async Task GetByUsername_ReturnsRow()
- {
- var conn = new MockDbConnection();
- conn.Mocks
- .When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername")
- .ReturnsTable(MockTable.WithColumns(
- ("UserAccountId", typeof(Guid)),
- ("Username", typeof(string)),
- ("FirstName", typeof(string)),
- ("LastName", typeof(string)),
- ("Email", typeof(string)),
- ("CreatedAt", typeof(DateTime)),
- ("UpdatedAt", typeof(DateTime?)),
- ("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 result = await repo.GetByUsernameAsync("lookupuser");
- result.Should().NotBeNull();
- result!.Email.Should().Be("lookup@example.com");
- }
-
- [Fact]
- public async Task GetByEmail_ReturnsRow()
- {
- var conn = new MockDbConnection();
- conn.Mocks
- .When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
- .ReturnsTable(MockTable.WithColumns(
- ("UserAccountId", typeof(Guid)),
- ("Username", typeof(string)),
- ("FirstName", typeof(string)),
- ("LastName", typeof(string)),
- ("Email", typeof(string)),
- ("CreatedAt", typeof(DateTime)),
- ("UpdatedAt", typeof(DateTime?)),
- ("DateOfBirth", typeof(DateTime)),
- ("Timer", typeof(byte[]))
- ).AddRow(Guid.NewGuid(), "byemail", "B", "E", "byemail@example.com", DateTime.UtcNow, null,
- DateTime.UtcNow.Date, null));
-
- var repo = CreateRepo(conn);
- var result = await repo.GetByEmailAsync("byemail@example.com");
- result.Should().NotBeNull();
- result!.Username.Should().Be("byemail");
- }
-}
diff --git a/src/Core/Service/Service.Core/Auth/AuthService.cs b/src/Core/Service/Service.Core/Auth/AuthService.cs
index 9d13629..8756dc3 100644
--- a/src/Core/Service/Service.Core/Auth/AuthService.cs
+++ b/src/Core/Service/Service.Core/Auth/AuthService.cs
@@ -1,12 +1,12 @@
-using Domain.Core.Entities;
-using Repository.Core.Repositories.Auth;
-using Service.Core.Password;
+using Domain.Entities;
+using Infrastructure.PasswordHashing;
+using Infrastructure.Repository.Auth;
namespace Service.Core.Auth;
public class AuthService(
IAuthRepository authRepo,
- IPasswordService passwordService
+ IPasswordInfra passwordInfra
) : IAuthService
{
public async Task RegisterAsync(UserAccount userAccount, string password)
@@ -19,7 +19,7 @@ public class AuthService(
}
// password hashing
- var hashed = passwordService.Hash(password);
+ var hashed = passwordInfra.Hash(password);
// Register user with hashed password
return await authRepo.RegisterUserAsync(
@@ -43,6 +43,6 @@ public class AuthService(
var activeCred = await authRepo.GetActiveCredentialByUserAccountIdAsync(user.UserAccountId);
if (activeCred is null) return null;
- return !passwordService.Verify(password, activeCred.Hash) ? null : user;
+ return !passwordInfra.Verify(password, activeCred.Hash) ? null : user;
}
}
diff --git a/src/Core/Service/Service.Core/Auth/IAuthService.cs b/src/Core/Service/Service.Core/Auth/IAuthService.cs
index 3e6e17c..8e99efb 100644
--- a/src/Core/Service/Service.Core/Auth/IAuthService.cs
+++ b/src/Core/Service/Service.Core/Auth/IAuthService.cs
@@ -1,4 +1,4 @@
-using Domain.Core.Entities;
+using Domain.Entities;
namespace Service.Core.Auth;
diff --git a/src/Core/Service/Service.Core/Service.Core.csproj b/src/Core/Service/Service.Core/Service.Core.csproj
index 9ce9012..c01d95b 100644
--- a/src/Core/Service/Service.Core/Service.Core.csproj
+++ b/src/Core/Service/Service.Core/Service.Core.csproj
@@ -12,6 +12,8 @@
-
+
+
diff --git a/src/Core/Service/Service.Core/User/IUserService.cs b/src/Core/Service/Service.Core/User/IUserService.cs
index 3bed122..a515cab 100644
--- a/src/Core/Service/Service.Core/User/IUserService.cs
+++ b/src/Core/Service/Service.Core/User/IUserService.cs
@@ -1,4 +1,4 @@
-using Domain.Core.Entities;
+using Domain.Entities;
namespace Service.Core.User;
diff --git a/src/Core/Service/Service.Core/User/UserService.cs b/src/Core/Service/Service.Core/User/UserService.cs
index fb249cd..1959553 100644
--- a/src/Core/Service/Service.Core/User/UserService.cs
+++ b/src/Core/Service/Service.Core/User/UserService.cs
@@ -1,5 +1,5 @@
-using Domain.Core.Entities;
-using Repository.Core.Repositories.UserAccount;
+using Domain.Entities;
+using Infrastructure.Repository.UserAccount;
namespace Service.Core.User;