mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 02:39:03 +00:00
Merge pull request #141 from aaronpo97/refactor/add-infrastructure-project-dir
Refactor/add infrastructure project dir
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -483,9 +483,6 @@ FodyWeavers.xsd
|
|||||||
|
|
||||||
*.feature.cs
|
*.feature.cs
|
||||||
|
|
||||||
|
|
||||||
database
|
|
||||||
|
|
||||||
.env
|
.env
|
||||||
.env.dev
|
.env.dev
|
||||||
.env.test
|
.env.test
|
||||||
|
|||||||
99
README.md
99
README.md
@@ -2,46 +2,6 @@
|
|||||||
|
|
||||||
A social platform for craft beer enthusiasts to discover breweries, share reviews, and connect with fellow beer lovers.
|
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
|
## Project Status
|
||||||
|
|
||||||
This project is in active development, transitioning from a full-stack Next.js application to a **multi-project monorepo** with:
|
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
|
- **Frontend**: Next.js with TypeScript
|
||||||
- **Architecture**: SQL-first approach using stored procedures
|
- **Architecture**: SQL-first approach using stored procedures
|
||||||
|
|
||||||
**Current State** (February 2025):
|
**Current State** (February 2026):
|
||||||
- Core authentication and user management APIs functional
|
- Core authentication and user management APIs functional
|
||||||
- Database schema and migrations established
|
- Database schema and migrations established
|
||||||
- Repository and service layers implemented
|
- Domain, Infrastructure, Repository, and Service layers implemented
|
||||||
- Frontend integration with .NET API in progress
|
- Frontend integration with .NET API in progress
|
||||||
- Migrating remaining features from Next.js serverless functions
|
- 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/
|
src/Core/
|
||||||
├── API/
|
├── API/
|
||||||
│ ├── API.Core/ # ASP.NET Core Web API with Swagger/OpenAPI
|
│ ├── API.Core/ # ASP.NET Core Web API with Swagger/OpenAPI
|
||||||
│ └── API.Specs/ # Integration tests using Reqnroll (BDD)
|
│ └── API.Specs/ # Integration tests using Reqnroll (BDD)
|
||||||
├── Database/
|
├── Database/
|
||||||
│ ├── Database.Migrations/ # DbUp migrations (embedded SQL scripts)
|
│ ├── Database.Migrations/ # DbUp migrations (embedded SQL scripts)
|
||||||
│ └── Database.Seed/ # Database seeding for development/testing
|
│ └── Database.Seed/ # Database seeding for development/testing
|
||||||
├── Repository/
|
├── Domain/
|
||||||
│ ├── Repository.Core/ # Data access layer (stored procedure-based)
|
│ └── Domain.csproj # Domain entities and models
|
||||||
│ └── Repository.Tests/ # Unit tests for repositories
|
│ └── 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/
|
||||||
└── 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
|
### Key Components
|
||||||
@@ -86,6 +54,7 @@ Website/ # Next.js frontend application
|
|||||||
- Controllers: `AuthController`, `UserController`
|
- Controllers: `AuthController`, `UserController`
|
||||||
- Configured with Swagger UI for API exploration
|
- Configured with Swagger UI for API exploration
|
||||||
- Health checks and structured logging
|
- Health checks and structured logging
|
||||||
|
- Middleware for error handling and request processing
|
||||||
|
|
||||||
**Database Layer**
|
**Database Layer**
|
||||||
- SQL Server with stored procedures for all data operations
|
- 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
|
- Comprehensive schema including users, breweries, beers, locations, and social features
|
||||||
- Seeders for development data (users, locations across US/Canada/Mexico)
|
- 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
|
- Abstraction over SQL Server using ADO.NET
|
||||||
- `ISqlConnectionFactory` for connection management
|
- `ISqlConnectionFactory` for connection management
|
||||||
- Repositories: `AuthRepository`, `UserAccountRepository`
|
- Repositories: `AuthRepository`, `UserAccountRepository`
|
||||||
@@ -101,9 +82,9 @@ Website/ # Next.js frontend application
|
|||||||
|
|
||||||
**Service Layer** (`Service.Core`)
|
**Service Layer** (`Service.Core`)
|
||||||
- Business logic and orchestration
|
- Business logic and orchestration
|
||||||
- Services: `AuthService`, `UserService`, `JwtService`
|
- Services: `AuthService`, `UserService`
|
||||||
- Password hashing with Argon2id
|
- Integration with infrastructure components
|
||||||
- JWT token generation
|
- Transaction management and business rule enforcement
|
||||||
|
|
||||||
**Frontend** (`Website`)
|
**Frontend** (`Website`)
|
||||||
- Next.js 14+ with TypeScript
|
- 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;"
|
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
|
#### JWT Authentication
|
||||||
|
|
||||||
@@ -578,7 +559,7 @@ docker compose -f docker-compose.test.yaml up --abort-on-container-exit
|
|||||||
|
|
||||||
This runs:
|
This runs:
|
||||||
- **API.Specs** - BDD integration tests
|
- **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/`.
|
Test results are output to `./test-results/`.
|
||||||
|
|
||||||
@@ -590,10 +571,10 @@ cd src/Core
|
|||||||
dotnet test API/API.Specs/API.Specs.csproj
|
dotnet test API/API.Specs/API.Specs.csproj
|
||||||
```
|
```
|
||||||
|
|
||||||
**Unit Tests (Repository.Tests)**
|
**Unit Tests (Infrastructure.Repository.Tests)**
|
||||||
```bash
|
```bash
|
||||||
cd src/Core
|
cd src/Core
|
||||||
dotnet test Repository/Repository.Tests/Repository.Tests.csproj
|
dotnet test Infrastructure/Infrastructure.Repository/Infrastructure.Repository.Tests/Repository.Tests.csproj
|
||||||
```
|
```
|
||||||
|
|
||||||
### Test Features
|
### Test Features
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ services:
|
|||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
build:
|
build:
|
||||||
context: ./src/Core
|
context: ./src/Core
|
||||||
dockerfile: Repository/Repository.Tests/Dockerfile
|
dockerfile: Infrastructure/Infrastructure.Repository.Tests/Dockerfile
|
||||||
args:
|
args:
|
||||||
BUILD_CONFIGURATION: Release
|
BUILD_CONFIGURATION: Release
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Domain\Domain.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.csproj" />
|
||||||
<ProjectReference Include="..\..\Repository\Repository.Core\Repository.Core.csproj" />
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj" />
|
||||||
<ProjectReference Include="..\..\Service\Service.Core\Service.Core.csproj" />
|
<ProjectReference Include="..\..\Service\Service.Core\Service.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
using API.Core.Contracts.Auth;
|
using API.Core.Contracts.Auth;
|
||||||
using API.Core.Contracts.Common;
|
using API.Core.Contracts.Common;
|
||||||
using Domain.Core.Entities;
|
using Domain.Entities;
|
||||||
|
using Infrastructure.Jwt;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Service.Core.Auth;
|
using Service.Core.Auth;
|
||||||
using Service.Core.Jwt;
|
|
||||||
|
|
||||||
namespace API.Core.Controllers
|
namespace API.Core.Controllers
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Domain.Core.Entities;
|
using Domain.Entities;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Service.Core.User;
|
using Service.Core.User;
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
|||||||
ARG BUILD_CONFIGURATION=Release
|
ARG BUILD_CONFIGURATION=Release
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY ["API/API.Core/API.Core.csproj", "API/API.Core/"]
|
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/"]
|
COPY ["Service/Service.Core/Service.Core.csproj", "Service/Service.Core/"]
|
||||||
RUN dotnet restore "API/API.Core/API.Core.csproj"
|
RUN dotnet restore "API/API.Core/API.Core.csproj"
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,45 @@
|
|||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using Repository.Core.Repositories.Auth;
|
using FluentValidation.AspNetCore;
|
||||||
using Repository.Core.Repositories.UserAccount;
|
using Infrastructure.Jwt;
|
||||||
using Repository.Core.Sql;
|
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.Auth;
|
||||||
using Service.Core.Jwt;
|
|
||||||
using Service.Core.Password;
|
|
||||||
using Service.Core.User;
|
using Service.Core.User;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
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.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen();
|
||||||
builder.Services.AddOpenApi();
|
builder.Services.AddOpenApi();
|
||||||
|
|
||||||
// Add FluentValidation
|
// Add FluentValidation
|
||||||
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
|
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
|
||||||
|
builder.Services.AddFluentValidationAutoValidation();
|
||||||
|
|
||||||
// Add health checks
|
// Add health checks
|
||||||
builder.Services.AddHealthChecks();
|
builder.Services.AddHealthChecks();
|
||||||
@@ -35,7 +59,7 @@ builder.Services.AddScoped<IUserService, UserService>();
|
|||||||
builder.Services.AddScoped<IAuthRepository, AuthRepository>();
|
builder.Services.AddScoped<IAuthRepository, AuthRepository>();
|
||||||
builder.Services.AddScoped<IAuthService, AuthService>();
|
builder.Services.AddScoped<IAuthService, AuthService>();
|
||||||
builder.Services.AddScoped<IJwtService, JwtService>();
|
builder.Services.AddScoped<IJwtService, JwtService>();
|
||||||
builder.Services.AddScoped<IPasswordService, PasswordService>();
|
builder.Services.AddScoped<IPasswordInfra, Argon2Infrastructure>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
@@ -59,3 +83,6 @@ lifetime.ApplicationStopping.Register(() =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
|
// Make Program class accessible to test projects
|
||||||
|
public partial class Program { }
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ ARG BUILD_CONFIGURATION=Release
|
|||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY ["API/API.Core/API.Core.csproj", "API/API.Core/"]
|
COPY ["API/API.Core/API.Core.csproj", "API/API.Core/"]
|
||||||
COPY ["API/API.Specs/API.Specs.csproj", "API/API.Specs/"]
|
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/"]
|
COPY ["Service/Service.Core/Service.Core.csproj", "Service/Service.Core/"]
|
||||||
RUN dotnet restore "API/API.Specs/API.Specs.csproj"
|
RUN dotnet restore "API/API.Specs/API.Specs.csproj"
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ Feature: User Registration
|
|||||||
| Username | FirstName | LastName | Email | DateOfBirth | Password |
|
| Username | FirstName | LastName | Email | DateOfBirth | Password |
|
||||||
| newuser | New | User | newuser@example.com | 1990-01-01 | weakpass |
|
| newuser | New | User | newuser@example.com | 1990-01-01 | weakpass |
|
||||||
Then the response has HTTP status 400
|
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)
|
Scenario: Cannot register a user younger than 19 years of age (regulatory requirement)
|
||||||
Given the API is running
|
Given the API is running
|
||||||
|
|||||||
@@ -10,9 +10,11 @@
|
|||||||
<Folder Name="/Domain/">
|
<Folder Name="/Domain/">
|
||||||
<Project Path="Domain/Domain.csproj" />
|
<Project Path="Domain/Domain.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Repository/">
|
<Folder Name="/Infrastructure/">
|
||||||
<Project Path="Repository/Repository.Core/Repository.Core.csproj" />
|
<Project Path="Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj" />
|
||||||
<Project Path="Repository/Repository.Tests/Repository.Tests.csproj" />
|
<Project Path="Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj" />
|
||||||
|
<Project Path="Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj" />
|
||||||
|
<Project Path="Infrastructure\Infrastructure.Repository.Tests\Infrastructure.Repository.Tests.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Service/">
|
<Folder Name="/Service/">
|
||||||
<Project Path="Service/Service.Core/Service.Core.csproj" />
|
<Project Path="Service/Service.Core/Service.Core.csproj" />
|
||||||
|
|||||||
@@ -19,6 +19,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Domain\Domain.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.csproj" />
|
||||||
<ProjectReference Include="..\..\Repository\Repository.Core\Repository.Core.csproj" />
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
using Microsoft.Data.SqlClient;
|
using Microsoft.Data.SqlClient;
|
||||||
|
|
||||||
namespace DBSeed
|
namespace Database.Seed;
|
||||||
|
|
||||||
|
internal interface ISeeder
|
||||||
{
|
{
|
||||||
internal interface ISeeder
|
Task SeedAsync(SqlConnection connection);
|
||||||
{
|
|
||||||
Task SeedAsync(SqlConnection connection);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,328 +1,326 @@
|
|||||||
using System.Data;
|
using System.Data;
|
||||||
using Microsoft.Data.SqlClient;
|
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<(
|
get;
|
||||||
string CountryName,
|
} =
|
||||||
string CountryCode
|
[
|
||||||
)> Countries =
|
("Alabama", "US-AL", "US"),
|
||||||
[
|
("Alaska", "US-AK", "US"),
|
||||||
("Canada", "CA"),
|
("Arizona", "US-AZ", "US"),
|
||||||
("Mexico", "MX"),
|
("Arkansas", "US-AR", "US"),
|
||||||
("United States", "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;
|
await CreateCountryAsync(connection, countryName, countryCode);
|
||||||
} =
|
|
||||||
[
|
|
||||||
("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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task CreateCountryAsync(
|
foreach (
|
||||||
SqlConnection connection,
|
var (stateProvinceName, stateProvinceCode, countryCode) in States
|
||||||
string countryName,
|
|
||||||
string countryCode
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
await using var command = new SqlCommand(
|
await CreateStateProvinceAsync(
|
||||||
"dbo.USP_CreateCountry",
|
connection,
|
||||||
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(
|
foreach (var (stateProvinceCode, cityName) in Cities)
|
||||||
SqlConnection connection,
|
|
||||||
string stateProvinceName,
|
|
||||||
string stateProvinceCode,
|
|
||||||
string countryCode
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
await using var command = new SqlCommand(
|
await CreateCityAsync(connection, cityName, stateProvinceCode);
|
||||||
"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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
using DBSeed;
|
using Microsoft.Data.SqlClient;
|
||||||
using Microsoft.Data.SqlClient;
|
|
||||||
using DbUp;
|
using DbUp;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using Database.Seed;
|
||||||
|
|
||||||
string BuildConnectionString()
|
string BuildConnectionString()
|
||||||
{
|
{
|
||||||
var server = Environment.GetEnvironmentVariable("DB_SERVER")
|
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")
|
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")
|
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")
|
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")
|
var trustServerCertificate = Environment.GetEnvironmentVariable("DB_TRUST_SERVER_CERTIFICATE")
|
||||||
?? "True";
|
?? "True";
|
||||||
|
|
||||||
var builder = new SqlConnectionStringBuilder
|
var builder = new SqlConnectionStringBuilder
|
||||||
{
|
{
|
||||||
@@ -33,6 +33,7 @@ string BuildConnectionString()
|
|||||||
return builder.ConnectionString;
|
return builder.ConnectionString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var connectionString = BuildConnectionString();
|
var connectionString = BuildConnectionString();
|
||||||
@@ -72,7 +73,6 @@ try
|
|||||||
|
|
||||||
using (connection)
|
using (connection)
|
||||||
{
|
{
|
||||||
|
|
||||||
ISeeder[] seeders =
|
ISeeder[] seeders =
|
||||||
[
|
[
|
||||||
new LocationSeeder(),
|
new LocationSeeder(),
|
||||||
@@ -96,4 +96,4 @@ catch (Exception ex)
|
|||||||
Console.Error.WriteLine("Seed failed:");
|
Console.Error.WriteLine("Seed failed:");
|
||||||
Console.Error.WriteLine(ex);
|
Console.Error.WriteLine(ex);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -1,273 +1,267 @@
|
|||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Domain.Core.Entities;
|
|
||||||
using Repository.Core.Repositories;
|
|
||||||
using idunno.Password;
|
using idunno.Password;
|
||||||
using Konscious.Security.Cryptography;
|
using Konscious.Security.Cryptography;
|
||||||
using Microsoft.Data.SqlClient;
|
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<(
|
var generator = new PasswordGenerator();
|
||||||
string FirstName,
|
var rng = new Random();
|
||||||
string LastName
|
int createdUsers = 0;
|
||||||
)> SeedNames =
|
int createdCredentials = 0;
|
||||||
[
|
int createdVerifications = 0;
|
||||||
("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)
|
|
||||||
{
|
{
|
||||||
var generator = new PasswordGenerator();
|
const string firstName = "Test";
|
||||||
var rng = new Random();
|
const string lastName = "User";
|
||||||
int createdUsers = 0;
|
const string email = "test.user@thebiergarten.app";
|
||||||
int createdCredentials = 0;
|
var dob = new DateTime(1985, 03, 01);
|
||||||
int createdVerifications = 0;
|
var hash = GeneratePasswordHash("password");
|
||||||
|
|
||||||
{
|
await RegisterUserAsync(
|
||||||
const string firstName = "Test";
|
connection,
|
||||||
const string lastName = "User";
|
$"{firstName}.{lastName}",
|
||||||
const string email = "test.user@thebiergarten.app";
|
firstName,
|
||||||
var dob = new DateTime(1985, 03, 01);
|
lastName,
|
||||||
var hash = GeneratePasswordHash("password");
|
dob,
|
||||||
|
email,
|
||||||
await RegisterUserAsync(
|
hash
|
||||||
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<Guid> 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<bool> 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();
|
|
||||||
}
|
}
|
||||||
|
foreach (var (firstName, lastName) in SeedNames)
|
||||||
private static DateTime GenerateDateOfBirth(Random random)
|
|
||||||
{
|
{
|
||||||
int age = 19 + random.Next(0, 30);
|
// prepare user fields
|
||||||
DateTime baseDate = DateTime.UtcNow.Date.AddYears(-age);
|
var username = $"{firstName[0]}.{lastName}";
|
||||||
int offsetDays = random.Next(0, 365);
|
var email = $"{firstName}.{lastName}@thebiergarten.app";
|
||||||
return baseDate.AddDays(-offsetDays);
|
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<Guid> 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<bool> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Domain.Core.Entities;
|
namespace Domain.Entities;
|
||||||
|
|
||||||
public class UserAccount
|
public class UserAccount
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Domain.Core.Entities;
|
namespace Domain.Entities;
|
||||||
|
|
||||||
public class UserCredential
|
public class UserCredential
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Domain.Core.Entities;
|
namespace Domain.Entities;
|
||||||
|
|
||||||
public class UserVerification
|
public class UserVerification
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Service.Core.Jwt;
|
namespace Infrastructure.Jwt;
|
||||||
|
|
||||||
public interface IJwtService
|
public interface IJwtService
|
||||||
{
|
{
|
||||||
string GenerateJwt(Guid userId, string username, DateTime expiry);
|
string GenerateJwt(Guid userId, string username, DateTime expiry);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>Infrastructure.Jwt</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference
|
||||||
|
Include="Microsoft.IdentityModel.JsonWebTokens"
|
||||||
|
Version="8.2.1"
|
||||||
|
/>
|
||||||
|
<PackageReference
|
||||||
|
Include="System.IdentityModel.Tokens.Jwt"
|
||||||
|
Version="8.2.1"
|
||||||
|
/>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -4,22 +4,28 @@ using Microsoft.IdentityModel.JsonWebTokens;
|
|||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames;
|
using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames;
|
||||||
|
|
||||||
namespace Service.Core.Jwt;
|
namespace Infrastructure.Jwt;
|
||||||
|
|
||||||
public class JwtService : IJwtService
|
public class JwtService : IJwtService
|
||||||
{
|
{
|
||||||
private readonly string? _secret = Environment.GetEnvironmentVariable("JWT_SECRET");
|
private readonly string? _secret = Environment.GetEnvironmentVariable(
|
||||||
|
"JWT_SECRET"
|
||||||
|
);
|
||||||
|
|
||||||
public string GenerateJwt(Guid userId, string username, DateTime expiry)
|
public string GenerateJwt(Guid userId, string username, DateTime expiry)
|
||||||
{
|
{
|
||||||
var handler = new JsonWebTokenHandler();
|
var handler = new JsonWebTokenHandler();
|
||||||
|
|
||||||
var key = Encoding.UTF8.GetBytes(_secret ?? throw new InvalidOperationException("secret not set"));
|
var key = Encoding.UTF8.GetBytes(
|
||||||
|
_secret ?? throw new InvalidOperationException("secret not set")
|
||||||
|
);
|
||||||
|
|
||||||
// Base claims (always present)
|
// Base claims (always present)
|
||||||
var claims = new List<Claim>
|
var claims = new List<Claim>
|
||||||
{
|
{
|
||||||
new(JwtRegisteredClaimNames.Sub, userId.ToString()),
|
new(JwtRegisteredClaimNames.Sub, userId.ToString()),
|
||||||
new(JwtRegisteredClaimNames.UniqueName, username),
|
new(JwtRegisteredClaimNames.UniqueName, username),
|
||||||
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
|
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||||
};
|
};
|
||||||
|
|
||||||
var tokenDescriptor = new SecurityTokenDescriptor
|
var tokenDescriptor = new SecurityTokenDescriptor
|
||||||
@@ -28,7 +34,8 @@ public class JwtService : IJwtService
|
|||||||
Expires = expiry,
|
Expires = expiry,
|
||||||
SigningCredentials = new SigningCredentials(
|
SigningCredentials = new SigningCredentials(
|
||||||
new SymmetricSecurityKey(key),
|
new SymmetricSecurityKey(key),
|
||||||
SecurityAlgorithms.HmacSha256)
|
SecurityAlgorithms.HmacSha256
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
return handler.CreateToken(tokenDescriptor);
|
return handler.CreateToken(tokenDescriptor);
|
||||||
@@ -2,9 +2,9 @@ using System.Security.Cryptography;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Konscious.Security.Cryptography;
|
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 SaltSize = 16; // 128-bit
|
||||||
private const int HashSize = 32; // 256-bit
|
private const int HashSize = 32; // 256-bit
|
||||||
@@ -19,7 +19,7 @@ public class PasswordService : IPasswordService
|
|||||||
Salt = salt,
|
Salt = salt,
|
||||||
DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1),
|
DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1),
|
||||||
MemorySize = ArgonMemoryKb,
|
MemorySize = ArgonMemoryKb,
|
||||||
Iterations = ArgonIterations
|
Iterations = ArgonIterations,
|
||||||
};
|
};
|
||||||
|
|
||||||
var hash = argon2.GetBytes(HashSize);
|
var hash = argon2.GetBytes(HashSize);
|
||||||
@@ -30,8 +30,12 @@ public class PasswordService : IPasswordService
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var parts = stored.Split(':', StringSplitOptions.RemoveEmptyEntries);
|
var parts = stored.Split(
|
||||||
if (parts.Length != 2) return false;
|
':',
|
||||||
|
StringSplitOptions.RemoveEmptyEntries
|
||||||
|
);
|
||||||
|
if (parts.Length != 2)
|
||||||
|
return false;
|
||||||
|
|
||||||
var salt = Convert.FromBase64String(parts[0]);
|
var salt = Convert.FromBase64String(parts[0]);
|
||||||
var expected = Convert.FromBase64String(parts[1]);
|
var expected = Convert.FromBase64String(parts[1]);
|
||||||
@@ -41,7 +45,7 @@ public class PasswordService : IPasswordService
|
|||||||
Salt = salt,
|
Salt = salt,
|
||||||
DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1),
|
DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1),
|
||||||
MemorySize = ArgonMemoryKb,
|
MemorySize = ArgonMemoryKb,
|
||||||
Iterations = ArgonIterations
|
Iterations = ArgonIterations,
|
||||||
};
|
};
|
||||||
|
|
||||||
var actual = argon2.GetBytes(expected.Length);
|
var actual = argon2.GetBytes(expected.Length);
|
||||||
@@ -52,4 +56,4 @@ public class PasswordService : IPasswordService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
namespace Service.Core.Password;
|
namespace Infrastructure.PasswordHashing;
|
||||||
|
|
||||||
public interface IPasswordService
|
public interface IPasswordInfra
|
||||||
{
|
{
|
||||||
public string Hash(string password);
|
public string Hash(string password);
|
||||||
public bool Verify(string password, string stored);
|
public bool Verify(string password, string stored);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>Infrastructure.PasswordHashing</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference
|
||||||
|
Include="Konscious.Security.Cryptography.Argon2"
|
||||||
|
Version="1.3.1"
|
||||||
|
/>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
using Apps72.Dev.Data.DbMocker;
|
|
||||||
using Repository.Core.Repositories.Auth;
|
|
||||||
using FluentAssertions;
|
|
||||||
using Repository.Tests.Database;
|
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
using Apps72.Dev.Data.DbMocker;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Infrastructure.Repository.Auth;
|
||||||
|
using Repository.Tests.Database;
|
||||||
|
|
||||||
namespace Repository.Tests.Auth;
|
namespace Repository.Tests.Auth;
|
||||||
|
|
||||||
public class AuthRepositoryTest
|
public class AuthRepositoryTest
|
||||||
{
|
{
|
||||||
private static AuthRepository CreateRepo(MockDbConnection conn)
|
private static AuthRepository CreateRepo(MockDbConnection conn) =>
|
||||||
=> new(new TestConnectionFactory(conn));
|
new(new TestConnectionFactory(conn));
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RegisterUserAsync_CreatesUserWithCredential_ReturnsUserAccount()
|
public async Task RegisterUserAsync_CreatesUserWithCredential_ReturnsUserAccount()
|
||||||
@@ -17,10 +17,12 @@ public class AuthRepositoryTest
|
|||||||
var expectedUserId = Guid.NewGuid();
|
var expectedUserId = Guid.NewGuid();
|
||||||
var conn = new MockDbConnection();
|
var conn = new MockDbConnection();
|
||||||
|
|
||||||
conn.Mocks
|
conn.Mocks.When(cmd => cmd.CommandText == "USP_RegisterUser")
|
||||||
.When(cmd => cmd.CommandText == "USP_RegisterUser")
|
.ReturnsTable(
|
||||||
.ReturnsTable(MockTable.WithColumns(("UserAccountId", typeof(Guid)))
|
MockTable
|
||||||
.AddRow(expectedUserId));
|
.WithColumns(("UserAccountId", typeof(Guid)))
|
||||||
|
.AddRow(expectedUserId)
|
||||||
|
);
|
||||||
|
|
||||||
var repo = CreateRepo(conn);
|
var repo = CreateRepo(conn);
|
||||||
var result = await repo.RegisterUserAsync(
|
var result = await repo.RegisterUserAsync(
|
||||||
@@ -47,29 +49,32 @@ public class AuthRepositoryTest
|
|||||||
var userId = Guid.NewGuid();
|
var userId = Guid.NewGuid();
|
||||||
var conn = new MockDbConnection();
|
var conn = new MockDbConnection();
|
||||||
|
|
||||||
conn.Mocks
|
conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
|
||||||
.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
|
.ReturnsTable(
|
||||||
.ReturnsTable(MockTable.WithColumns(
|
MockTable
|
||||||
("UserAccountId", typeof(Guid)),
|
.WithColumns(
|
||||||
("Username", typeof(string)),
|
("UserAccountId", typeof(Guid)),
|
||||||
("FirstName", typeof(string)),
|
("Username", typeof(string)),
|
||||||
("LastName", typeof(string)),
|
("FirstName", typeof(string)),
|
||||||
("Email", typeof(string)),
|
("LastName", typeof(string)),
|
||||||
("CreatedAt", typeof(DateTime)),
|
("Email", typeof(string)),
|
||||||
("UpdatedAt", typeof(DateTime?)),
|
("CreatedAt", typeof(DateTime)),
|
||||||
("DateOfBirth", typeof(DateTime)),
|
("UpdatedAt", typeof(DateTime?)),
|
||||||
("Timer", typeof(byte[]))
|
("DateOfBirth", typeof(DateTime)),
|
||||||
).AddRow(
|
("Timer", typeof(byte[]))
|
||||||
userId,
|
)
|
||||||
"emailuser",
|
.AddRow(
|
||||||
"Email",
|
userId,
|
||||||
"User",
|
"emailuser",
|
||||||
"emailuser@example.com",
|
"Email",
|
||||||
DateTime.UtcNow,
|
"User",
|
||||||
null,
|
"emailuser@example.com",
|
||||||
new DateTime(1990, 5, 15),
|
DateTime.UtcNow,
|
||||||
null
|
null,
|
||||||
));
|
new DateTime(1990, 5, 15),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
var repo = CreateRepo(conn);
|
var repo = CreateRepo(conn);
|
||||||
var result = await repo.GetUserByEmailAsync("emailuser@example.com");
|
var result = await repo.GetUserByEmailAsync("emailuser@example.com");
|
||||||
@@ -87,8 +92,7 @@ public class AuthRepositoryTest
|
|||||||
{
|
{
|
||||||
var conn = new MockDbConnection();
|
var conn = new MockDbConnection();
|
||||||
|
|
||||||
conn.Mocks
|
conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
|
||||||
.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
|
|
||||||
.ReturnsTable(MockTable.Empty());
|
.ReturnsTable(MockTable.Empty());
|
||||||
|
|
||||||
var repo = CreateRepo(conn);
|
var repo = CreateRepo(conn);
|
||||||
@@ -103,29 +107,34 @@ public class AuthRepositoryTest
|
|||||||
var userId = Guid.NewGuid();
|
var userId = Guid.NewGuid();
|
||||||
var conn = new MockDbConnection();
|
var conn = new MockDbConnection();
|
||||||
|
|
||||||
conn.Mocks
|
conn.Mocks.When(cmd =>
|
||||||
.When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername")
|
cmd.CommandText == "usp_GetUserAccountByUsername"
|
||||||
.ReturnsTable(MockTable.WithColumns(
|
)
|
||||||
("UserAccountId", typeof(Guid)),
|
.ReturnsTable(
|
||||||
("Username", typeof(string)),
|
MockTable
|
||||||
("FirstName", typeof(string)),
|
.WithColumns(
|
||||||
("LastName", typeof(string)),
|
("UserAccountId", typeof(Guid)),
|
||||||
("Email", typeof(string)),
|
("Username", typeof(string)),
|
||||||
("CreatedAt", typeof(DateTime)),
|
("FirstName", typeof(string)),
|
||||||
("UpdatedAt", typeof(DateTime?)),
|
("LastName", typeof(string)),
|
||||||
("DateOfBirth", typeof(DateTime)),
|
("Email", typeof(string)),
|
||||||
("Timer", typeof(byte[]))
|
("CreatedAt", typeof(DateTime)),
|
||||||
).AddRow(
|
("UpdatedAt", typeof(DateTime?)),
|
||||||
userId,
|
("DateOfBirth", typeof(DateTime)),
|
||||||
"usernameuser",
|
("Timer", typeof(byte[]))
|
||||||
"Username",
|
)
|
||||||
"User",
|
.AddRow(
|
||||||
"username@example.com",
|
userId,
|
||||||
DateTime.UtcNow,
|
"usernameuser",
|
||||||
null,
|
"Username",
|
||||||
new DateTime(1985, 8, 20),
|
"User",
|
||||||
null
|
"username@example.com",
|
||||||
));
|
DateTime.UtcNow,
|
||||||
|
null,
|
||||||
|
new DateTime(1985, 8, 20),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
var repo = CreateRepo(conn);
|
var repo = CreateRepo(conn);
|
||||||
var result = await repo.GetUserByUsernameAsync("usernameuser");
|
var result = await repo.GetUserByUsernameAsync("usernameuser");
|
||||||
@@ -141,8 +150,9 @@ public class AuthRepositoryTest
|
|||||||
{
|
{
|
||||||
var conn = new MockDbConnection();
|
var conn = new MockDbConnection();
|
||||||
|
|
||||||
conn.Mocks
|
conn.Mocks.When(cmd =>
|
||||||
.When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername")
|
cmd.CommandText == "usp_GetUserAccountByUsername"
|
||||||
|
)
|
||||||
.ReturnsTable(MockTable.Empty());
|
.ReturnsTable(MockTable.Empty());
|
||||||
|
|
||||||
var repo = CreateRepo(conn);
|
var repo = CreateRepo(conn);
|
||||||
@@ -158,21 +168,26 @@ public class AuthRepositoryTest
|
|||||||
var credentialId = Guid.NewGuid();
|
var credentialId = Guid.NewGuid();
|
||||||
var conn = new MockDbConnection();
|
var conn = new MockDbConnection();
|
||||||
|
|
||||||
conn.Mocks
|
conn.Mocks.When(cmd =>
|
||||||
.When(cmd => cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId")
|
cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId"
|
||||||
.ReturnsTable(MockTable.WithColumns(
|
)
|
||||||
("UserCredentialId", typeof(Guid)),
|
.ReturnsTable(
|
||||||
("UserAccountId", typeof(Guid)),
|
MockTable
|
||||||
("Hash", typeof(string)),
|
.WithColumns(
|
||||||
("CreatedAt", typeof(DateTime)),
|
("UserCredentialId", typeof(Guid)),
|
||||||
("Timer", typeof(byte[]))
|
("UserAccountId", typeof(Guid)),
|
||||||
).AddRow(
|
("Hash", typeof(string)),
|
||||||
credentialId,
|
("CreatedAt", typeof(DateTime)),
|
||||||
userId,
|
("Timer", typeof(byte[]))
|
||||||
"hashed_password_value",
|
)
|
||||||
DateTime.UtcNow,
|
.AddRow(
|
||||||
null
|
credentialId,
|
||||||
));
|
userId,
|
||||||
|
"hashed_password_value",
|
||||||
|
DateTime.UtcNow,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
var repo = CreateRepo(conn);
|
var repo = CreateRepo(conn);
|
||||||
var result = await repo.GetActiveCredentialByUserAccountIdAsync(userId);
|
var result = await repo.GetActiveCredentialByUserAccountIdAsync(userId);
|
||||||
@@ -189,8 +204,9 @@ public class AuthRepositoryTest
|
|||||||
var userId = Guid.NewGuid();
|
var userId = Guid.NewGuid();
|
||||||
var conn = new MockDbConnection();
|
var conn = new MockDbConnection();
|
||||||
|
|
||||||
conn.Mocks
|
conn.Mocks.When(cmd =>
|
||||||
.When(cmd => cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId")
|
cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId"
|
||||||
|
)
|
||||||
.ReturnsTable(MockTable.Empty());
|
.ReturnsTable(MockTable.Empty());
|
||||||
|
|
||||||
var repo = CreateRepo(conn);
|
var repo = CreateRepo(conn);
|
||||||
@@ -206,14 +222,14 @@ public class AuthRepositoryTest
|
|||||||
var newPasswordHash = "new_hashed_password";
|
var newPasswordHash = "new_hashed_password";
|
||||||
var conn = new MockDbConnection();
|
var conn = new MockDbConnection();
|
||||||
|
|
||||||
conn.Mocks
|
conn.Mocks.When(cmd => cmd.CommandText == "USP_RotateUserCredential")
|
||||||
.When(cmd => cmd.CommandText == "USP_RotateUserCredential")
|
|
||||||
.ReturnsScalar(1);
|
.ReturnsScalar(1);
|
||||||
|
|
||||||
var repo = CreateRepo(conn);
|
var repo = CreateRepo(conn);
|
||||||
|
|
||||||
// Should not throw
|
// Should not throw
|
||||||
var act = async () => await repo.RotateCredentialAsync(userId, newPasswordHash);
|
var act = async () =>
|
||||||
|
await repo.RotateCredentialAsync(userId, newPasswordHash);
|
||||||
await act.Should().NotThrowAsync();
|
await act.Should().NotThrowAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
using Repository.Core.Sql;
|
using Infrastructure.Repository.Sql;
|
||||||
|
|
||||||
namespace Repository.Tests.Database;
|
namespace Repository.Tests.Database;
|
||||||
|
|
||||||
internal class TestConnectionFactory(DbConnection conn) : ISqlConnectionFactory
|
internal class TestConnectionFactory(DbConnection conn) : ISqlConnectionFactory
|
||||||
{
|
{
|
||||||
private readonly DbConnection _conn = conn;
|
private readonly DbConnection _conn = conn;
|
||||||
|
|
||||||
public DbConnection CreateConnection() => _conn;
|
public DbConnection CreateConnection() => _conn;
|
||||||
}
|
}
|
||||||
@@ -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"]
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
<RootNamespace>Repository.Tests</RootNamespace>
|
<RootNamespace>Infrastructure.Repository.Tests</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -15,9 +15,18 @@
|
|||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
<PackageReference Include="FluentAssertions" Version="6.9.0" />
|
<PackageReference Include="FluentAssertions" Version="6.9.0" />
|
||||||
<PackageReference Include="DbMocker" Version="1.26.0" />
|
<PackageReference Include="DbMocker" Version="1.26.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
|
<PackageReference
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
|
Include="Microsoft.Extensions.Configuration"
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.0" />
|
Version="9.0.0"
|
||||||
|
/>
|
||||||
|
<PackageReference
|
||||||
|
Include="Microsoft.Extensions.Configuration.Abstractions"
|
||||||
|
Version="9.0.0"
|
||||||
|
/>
|
||||||
|
<PackageReference
|
||||||
|
Include="Microsoft.Extensions.Configuration.Binder"
|
||||||
|
Version="9.0.0"
|
||||||
|
/>
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@@ -26,6 +35,6 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Repository.Core\Repository.Core.csproj" />
|
<ProjectReference Include="..\Infrastructure.Repository\Infrastructure.Repository.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Domain.Entities.UserAccount>(connectionFactory),
|
||||||
|
IAuthRepository
|
||||||
|
{
|
||||||
|
public async Task<Domain.Entities.UserAccount> 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<Domain.Entities.UserAccount?> 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<Domain.Entities.UserAccount?> 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<UserCredential?> 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps a data reader row to a UserAccount entity.
|
||||||
|
/// </summary>
|
||||||
|
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"],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps a data reader row to a UserCredential entity.
|
||||||
|
/// </summary>
|
||||||
|
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<System.Data.DataRow>()
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper method to add a parameter to a database command.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using Domain.Entities;
|
||||||
|
|
||||||
|
namespace Infrastructure.Repository.Auth;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Repository for authentication-related database operations including user registration and credential management.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAuthRepository
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a new user with account details and initial credential.
|
||||||
|
/// Uses stored procedure: USP_RegisterUser
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="username">Unique username for the user</param>
|
||||||
|
/// <param name="firstName">User's first name</param>
|
||||||
|
/// <param name="lastName">User's last name</param>
|
||||||
|
/// <param name="email">User's email address</param>
|
||||||
|
/// <param name="dateOfBirth">User's date of birth</param>
|
||||||
|
/// <param name="passwordHash">Hashed password</param>
|
||||||
|
/// <returns>The newly created UserAccount with generated ID</returns>
|
||||||
|
Task<Domain.Entities.UserAccount> RegisterUserAsync(
|
||||||
|
string username,
|
||||||
|
string firstName,
|
||||||
|
string lastName,
|
||||||
|
string email,
|
||||||
|
DateTime dateOfBirth,
|
||||||
|
string passwordHash
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a user account by email address (typically used for login).
|
||||||
|
/// Uses stored procedure: usp_GetUserAccountByEmail
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="email">Email address to search for</param>
|
||||||
|
/// <returns>UserAccount if found, null otherwise</returns>
|
||||||
|
Task<Domain.Entities.UserAccount?> GetUserByEmailAsync(
|
||||||
|
string email
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a user account by username (typically used for login).
|
||||||
|
/// Uses stored procedure: usp_GetUserAccountByUsername
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="username">Username to search for</param>
|
||||||
|
/// <returns>UserAccount if found, null otherwise</returns>
|
||||||
|
Task<Domain.Entities.UserAccount?> GetUserByUsernameAsync(
|
||||||
|
string username
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the active (non-revoked) credential for a user account.
|
||||||
|
/// Uses stored procedure: USP_GetActiveUserCredentialByUserAccountId
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userAccountId">ID of the user account</param>
|
||||||
|
/// <returns>Active UserCredential if found, null otherwise</returns>
|
||||||
|
Task<UserCredential?> GetActiveCredentialByUserAccountIdAsync(
|
||||||
|
Guid userAccountId
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rotates a user's credential by invalidating all existing credentials and creating a new one.
|
||||||
|
/// Uses stored procedure: USP_RotateUserCredential
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userAccountId">ID of the user account</param>
|
||||||
|
/// <param name="newPasswordHash">New hashed password</param>
|
||||||
|
Task RotateCredentialAsync(Guid userAccountId, string newPasswordHash);
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<RootNamespace>Repository.Core</RootNamespace>
|
<RootNamespace>Infrastructure.Repository</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
|
||||||
@@ -12,7 +12,10 @@
|
|||||||
Version="160.1000.6"
|
Version="160.1000.6"
|
||||||
/>
|
/>
|
||||||
<PackageReference Include="System.Data.SqlClient" Version="4.9.0" />
|
<PackageReference Include="System.Data.SqlClient" Version="4.9.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
<PackageReference
|
||||||
|
Include="Microsoft.Extensions.Configuration.Abstractions"
|
||||||
|
Version="8.0.0"
|
||||||
|
/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Domain\Domain.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.csproj" />
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Data.Common;
|
||||||
|
using Infrastructure.Repository.Sql;
|
||||||
|
|
||||||
|
namespace Infrastructure.Repository;
|
||||||
|
|
||||||
|
public abstract class Repository<T>(ISqlConnectionFactory connectionFactory)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
protected async Task<DbConnection> CreateConnection()
|
||||||
|
{
|
||||||
|
var connection = connectionFactory.CreateConnection();
|
||||||
|
await connection.OpenAsync();
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract T MapToEntity(DbDataReader reader);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Data.Common;
|
||||||
|
|
||||||
|
namespace Infrastructure.Repository.Sql;
|
||||||
|
|
||||||
|
public interface ISqlConnectionFactory
|
||||||
|
{
|
||||||
|
DbConnection CreateConnection();
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
|
||||||
|
namespace Infrastructure.Repository.Sql;
|
||||||
|
|
||||||
|
public static class SqlConnectionStringHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a SQL Server connection string from environment variables.
|
||||||
|
/// Expects DB_SERVER, DB_NAME, DB_USER, DB_PASSWORD, and DB_TRUST_SERVER_CERTIFICATE.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="databaseName">Optional override for the database name. If null, uses DB_NAME env var.</param>
|
||||||
|
/// <returns>A properly formatted SQL Server connection string.</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a connection string to the master database using environment variables.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A connection string for the master database.</returns>
|
||||||
|
public static string BuildMasterConnectionString()
|
||||||
|
{
|
||||||
|
return BuildConnectionString("master");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace Infrastructure.Repository.UserAccount;
|
||||||
|
|
||||||
|
public interface IUserAccountRepository
|
||||||
|
{
|
||||||
|
Task<Domain.Entities.UserAccount?> GetByIdAsync(Guid id);
|
||||||
|
Task<IEnumerable<Domain.Entities.UserAccount>> GetAllAsync(
|
||||||
|
int? limit,
|
||||||
|
int? offset
|
||||||
|
);
|
||||||
|
Task UpdateAsync(Domain.Entities.UserAccount userAccount);
|
||||||
|
Task DeleteAsync(Guid id);
|
||||||
|
Task<Domain.Entities.UserAccount?> GetByUsernameAsync(
|
||||||
|
string username
|
||||||
|
);
|
||||||
|
Task<Domain.Entities.UserAccount?> GetByEmailAsync(string email);
|
||||||
|
}
|
||||||
@@ -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<Domain.Entities.UserAccount>(connectionFactory),
|
||||||
|
IUserAccountRepository
|
||||||
|
{
|
||||||
|
public async Task<Domain.Entities.UserAccount?> 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<Domain.Entities.UserAccount>
|
||||||
|
> 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<Domain.Entities.UserAccount>();
|
||||||
|
|
||||||
|
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<Domain.Entities.UserAccount?> 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<Domain.Entities.UserAccount?> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Domain.Core.Entities.UserAccount>, IAuthRepository
|
|
||||||
{
|
|
||||||
public AuthRepository(ISqlConnectionFactory connectionFactory)
|
|
||||||
: base(connectionFactory)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public async Task<Domain.Core.Entities.UserAccount> 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<Domain.Core.Entities.UserAccount?> 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<Domain.Core.Entities.UserAccount?> 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<UserCredential?> 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maps a data reader row to a UserAccount entity.
|
|
||||||
/// </summary>
|
|
||||||
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"]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maps a data reader row to a UserCredential entity.
|
|
||||||
/// </summary>
|
|
||||||
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<System.Data.DataRow>()
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Helper method to add a parameter to a database command.
|
|
||||||
/// </summary>
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
using Domain.Core.Entities;
|
|
||||||
|
|
||||||
namespace Repository.Core.Repositories.Auth
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Repository for authentication-related database operations including user registration and credential management.
|
|
||||||
/// </summary>
|
|
||||||
public interface IAuthRepository
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Registers a new user with account details and initial credential.
|
|
||||||
/// Uses stored procedure: USP_RegisterUser
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="username">Unique username for the user</param>
|
|
||||||
/// <param name="firstName">User's first name</param>
|
|
||||||
/// <param name="lastName">User's last name</param>
|
|
||||||
/// <param name="email">User's email address</param>
|
|
||||||
/// <param name="dateOfBirth">User's date of birth</param>
|
|
||||||
/// <param name="passwordHash">Hashed password</param>
|
|
||||||
/// <returns>The newly created UserAccount with generated ID</returns>
|
|
||||||
Task<Domain.Core.Entities.UserAccount> RegisterUserAsync(
|
|
||||||
string username,
|
|
||||||
string firstName,
|
|
||||||
string lastName,
|
|
||||||
string email,
|
|
||||||
DateTime dateOfBirth,
|
|
||||||
string passwordHash);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves a user account by email address (typically used for login).
|
|
||||||
/// Uses stored procedure: usp_GetUserAccountByEmail
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="email">Email address to search for</param>
|
|
||||||
/// <returns>UserAccount if found, null otherwise</returns>
|
|
||||||
Task<Domain.Core.Entities.UserAccount?> GetUserByEmailAsync(string email);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves a user account by username (typically used for login).
|
|
||||||
/// Uses stored procedure: usp_GetUserAccountByUsername
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="username">Username to search for</param>
|
|
||||||
/// <returns>UserAccount if found, null otherwise</returns>
|
|
||||||
Task<Domain.Core.Entities.UserAccount?> GetUserByUsernameAsync(string username);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves the active (non-revoked) credential for a user account.
|
|
||||||
/// Uses stored procedure: USP_GetActiveUserCredentialByUserAccountId
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userAccountId">ID of the user account</param>
|
|
||||||
/// <returns>Active UserCredential if found, null otherwise</returns>
|
|
||||||
Task<UserCredential?> GetActiveCredentialByUserAccountIdAsync(Guid userAccountId);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Rotates a user's credential by invalidating all existing credentials and creating a new one.
|
|
||||||
/// Uses stored procedure: USP_RotateUserCredential
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userAccountId">ID of the user account</param>
|
|
||||||
/// <param name="newPasswordHash">New hashed password</param>
|
|
||||||
Task RotateCredentialAsync(Guid userAccountId, string newPasswordHash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using System.Data.Common;
|
|
||||||
using Repository.Core.Sql;
|
|
||||||
|
|
||||||
namespace Repository.Core.Repositories
|
|
||||||
{
|
|
||||||
public abstract class Repository<T>(ISqlConnectionFactory connectionFactory)
|
|
||||||
where T : class
|
|
||||||
{
|
|
||||||
protected async Task<DbConnection> CreateConnection()
|
|
||||||
{
|
|
||||||
var connection = connectionFactory.CreateConnection();
|
|
||||||
await connection.OpenAsync();
|
|
||||||
return connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract T MapToEntity(DbDataReader reader);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
using Domain.Core.Entities;
|
|
||||||
|
|
||||||
|
|
||||||
namespace Repository.Core.Repositories.UserAccount
|
|
||||||
{
|
|
||||||
public interface IUserAccountRepository
|
|
||||||
{
|
|
||||||
Task<Domain.Core.Entities.UserAccount?> GetByIdAsync(Guid id);
|
|
||||||
Task<IEnumerable<Domain.Core.Entities.UserAccount>> GetAllAsync(int? limit, int? offset);
|
|
||||||
Task UpdateAsync(Domain.Core.Entities.UserAccount userAccount);
|
|
||||||
Task DeleteAsync(Guid id);
|
|
||||||
Task<Domain.Core.Entities.UserAccount?> GetByUsernameAsync(string username);
|
|
||||||
Task<Domain.Core.Entities.UserAccount?> GetByEmailAsync(string email);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<Domain.Core.Entities.UserAccount>(connectionFactory), IUserAccountRepository
|
|
||||||
{
|
|
||||||
public async Task<Domain.Core.Entities.UserAccount?> 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<Domain.Core.Entities.UserAccount>> 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<Domain.Core.Entities.UserAccount>();
|
|
||||||
|
|
||||||
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<Domain.Core.Entities.UserAccount?> 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<Domain.Core.Entities.UserAccount?> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
using System.Data.Common;
|
|
||||||
|
|
||||||
namespace Repository.Core.Sql
|
|
||||||
{
|
|
||||||
public interface ISqlConnectionFactory
|
|
||||||
{
|
|
||||||
DbConnection CreateConnection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
using Microsoft.Data.SqlClient;
|
|
||||||
|
|
||||||
namespace Repository.Core.Sql
|
|
||||||
{
|
|
||||||
public static class SqlConnectionStringHelper
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Builds a SQL Server connection string from environment variables.
|
|
||||||
/// Expects DB_SERVER, DB_NAME, DB_USER, DB_PASSWORD, and DB_TRUST_SERVER_CERTIFICATE.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="databaseName">Optional override for the database name. If null, uses DB_NAME env var.</param>
|
|
||||||
/// <returns>A properly formatted SQL Server connection string.</returns>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Builds a connection string to the master database using environment variables.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A connection string for the master database.</returns>
|
|
||||||
public static string BuildMasterConnectionString()
|
|
||||||
{
|
|
||||||
return BuildConnectionString("master");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"]
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
using Domain.Core.Entities;
|
using Domain.Entities;
|
||||||
using Repository.Core.Repositories.Auth;
|
using Infrastructure.PasswordHashing;
|
||||||
using Service.Core.Password;
|
using Infrastructure.Repository.Auth;
|
||||||
|
|
||||||
namespace Service.Core.Auth;
|
namespace Service.Core.Auth;
|
||||||
|
|
||||||
public class AuthService(
|
public class AuthService(
|
||||||
IAuthRepository authRepo,
|
IAuthRepository authRepo,
|
||||||
IPasswordService passwordService
|
IPasswordInfra passwordInfra
|
||||||
) : IAuthService
|
) : IAuthService
|
||||||
{
|
{
|
||||||
public async Task<UserAccount> RegisterAsync(UserAccount userAccount, string password)
|
public async Task<UserAccount> RegisterAsync(UserAccount userAccount, string password)
|
||||||
@@ -19,7 +19,7 @@ public class AuthService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// password hashing
|
// password hashing
|
||||||
var hashed = passwordService.Hash(password);
|
var hashed = passwordInfra.Hash(password);
|
||||||
|
|
||||||
// Register user with hashed password
|
// Register user with hashed password
|
||||||
return await authRepo.RegisterUserAsync(
|
return await authRepo.RegisterUserAsync(
|
||||||
@@ -43,6 +43,6 @@ public class AuthService(
|
|||||||
var activeCred = await authRepo.GetActiveCredentialByUserAccountIdAsync(user.UserAccountId);
|
var activeCred = await authRepo.GetActiveCredentialByUserAccountIdAsync(user.UserAccountId);
|
||||||
|
|
||||||
if (activeCred is null) return null;
|
if (activeCred is null) return null;
|
||||||
return !passwordService.Verify(password, activeCred.Hash) ? null : user;
|
return !passwordInfra.Verify(password, activeCred.Hash) ? null : user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Domain.Core.Entities;
|
using Domain.Entities;
|
||||||
|
|
||||||
namespace Service.Core.Auth;
|
namespace Service.Core.Auth;
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Domain\Domain.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.csproj" />
|
||||||
<ProjectReference Include="..\..\Repository\Repository.Core\Repository.Core.csproj" />
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
|
||||||
|
<ProjectReference
|
||||||
|
Include="..\..\Infrastructure\Infrastructure.PasswordHashing\Infrastructure.PasswordHashing.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Domain.Core.Entities;
|
using Domain.Entities;
|
||||||
|
|
||||||
namespace Service.Core.User;
|
namespace Service.Core.User;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Domain.Core.Entities;
|
using Domain.Entities;
|
||||||
using Repository.Core.Repositories.UserAccount;
|
using Infrastructure.Repository.UserAccount;
|
||||||
|
|
||||||
namespace Service.Core.User;
|
namespace Service.Core.User;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user