mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 02:39:03 +00:00
Refactor repository structure
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: Infrastructure/Infrastructure.Repository/Repository.Tests/Dockerfile
|
dockerfile: Infrastructure/Infrastructure.Repository.Tests/Dockerfile
|
||||||
args:
|
args:
|
||||||
BUILD_CONFIGURATION: Release
|
BUILD_CONFIGURATION: Release
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -19,8 +19,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Domain\Domain.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.csproj" />
|
||||||
<ProjectReference
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
|
||||||
Include="..\..\Infrastructure\Infrastructure.Repository\Repository.Core\Repository.Core.csproj" />
|
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.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>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ 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 ["Domain/Domain.csproj", "Domain/"]
|
COPY ["Domain/Domain.csproj", "Domain/"]
|
||||||
COPY ["Infrastructure/Infrastructure.Repository/Repository.Core/Repository.Core.csproj", "Infrastructure/Infrastructure.Repository/Repository.Core/"]
|
COPY ["Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj", "Infrastructure/Infrastructure.Repository/"]
|
||||||
COPY ["Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj", "Infrastructure/Infrastructure.Jwt/"]
|
COPY ["Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj", "Infrastructure/Infrastructure.Jwt/"]
|
||||||
COPY ["Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj", "Infrastructure/Infrastructure.PasswordHashing/"]
|
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/"]
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ using FluentValidation;
|
|||||||
using FluentValidation.AspNetCore;
|
using FluentValidation.AspNetCore;
|
||||||
using Infrastructure.Jwt;
|
using Infrastructure.Jwt;
|
||||||
using Infrastructure.PasswordHashing;
|
using Infrastructure.PasswordHashing;
|
||||||
|
using Infrastructure.Repository.Auth;
|
||||||
|
using Infrastructure.Repository.Sql;
|
||||||
|
using Infrastructure.Repository.UserAccount;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Repository.Core.Repositories.Auth;
|
|
||||||
using Repository.Core.Repositories.UserAccount;
|
|
||||||
using Repository.Core.Sql;
|
|
||||||
using Service.Core.Auth;
|
using Service.Core.Auth;
|
||||||
using Service.Core.User;
|
using Service.Core.User;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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 ["Domain/Domain.csproj", "Domain/"]
|
COPY ["Domain/Domain.csproj", "Domain/"]
|
||||||
COPY ["Infrastructure/Infrastructure.Repository/Repository.Core/Repository.Core.csproj", "Infrastructure/Infrastructure.Repository/Repository.Core/"]
|
COPY ["Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj", "Infrastructure/Infrastructure.Repository/"]
|
||||||
COPY ["Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj", "Infrastructure/Infrastructure.Jwt/"]
|
COPY ["Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj", "Infrastructure/Infrastructure.Jwt/"]
|
||||||
COPY ["Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj", "Infrastructure/Infrastructure.PasswordHashing/"]
|
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/"]
|
||||||
|
|||||||
@@ -12,11 +12,9 @@
|
|||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Infrastructure/">
|
<Folder Name="/Infrastructure/">
|
||||||
<Project Path="Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj" />
|
<Project Path="Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj" />
|
||||||
<Project
|
<Project Path="Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj" />
|
||||||
Path="Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj" />
|
<Project Path="Infrastructure/Infrastructure.Repository.Tests/Repository.Tests.csproj" />
|
||||||
<Project Path="Infrastructure/Infrastructure.Repository/Repository.Core/Repository.Core.csproj" />
|
<Project Path="Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj" />
|
||||||
<Project
|
|
||||||
Path="Infrastructure/Infrastructure.Repository/Repository.Tests/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,7 +19,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Domain\Domain.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.csproj" />
|
||||||
<ProjectReference
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
|
||||||
Include="..\..\Infrastructure\Infrastructure.Repository\Repository.Core\Repository.Core.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ using System.Data;
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Domain.Core.Entities;
|
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;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Data;
|
using System.Data;
|
||||||
using Apps72.Dev.Data.DbMocker;
|
using Apps72.Dev.Data.DbMocker;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Repository.Core.Repositories.Auth;
|
using Infrastructure.Repository.Auth;
|
||||||
using Repository.Tests.Database;
|
using Repository.Tests.Database;
|
||||||
|
|
||||||
namespace Repository.Tests.Auth;
|
namespace Repository.Tests.Auth;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Data.Common;
|
||||||
|
using Infrastructure.Repository.Sql;
|
||||||
|
|
||||||
|
namespace Repository.Tests.Database;
|
||||||
|
|
||||||
|
internal class TestConnectionFactory(DbConnection conn) : ISqlConnectionFactory
|
||||||
|
{
|
||||||
|
private readonly DbConnection _conn = 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>
|
||||||
@@ -35,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>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Apps72.Dev.Data.DbMocker;
|
using Apps72.Dev.Data.DbMocker;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Repository.Core.Repositories.UserAccount;
|
using Infrastructure.Repository.UserAccount;
|
||||||
using Repository.Tests.Database;
|
using Repository.Tests.Database;
|
||||||
|
|
||||||
namespace Repository.Tests.UserAccount;
|
namespace Repository.Tests.UserAccount;
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
using System.Data;
|
||||||
|
using System.Data.Common;
|
||||||
|
using Domain.Core.Entities;
|
||||||
|
using Infrastructure.Repository.Sql;
|
||||||
|
|
||||||
|
namespace Infrastructure.Repository.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using Domain.Core.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.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);
|
||||||
|
}
|
||||||
@@ -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" />
|
||||||
@@ -18,6 +18,6 @@
|
|||||||
/>
|
/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\Domain\Domain.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,199 +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,68 +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,19 +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,151 +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,50 +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,62 +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,15 +0,0 @@
|
|||||||
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/Repository.Core/Repository.Core.csproj", "Infrastructure/Infrastructure.Repository/Repository.Core/"]
|
|
||||||
COPY ["Infrastructure/Infrastructure.Repository/Repository.Tests/Repository.Tests.csproj", "Infrastructure/Infrastructure.Repository/Repository.Tests/"]
|
|
||||||
RUN dotnet restore "Infrastructure/Infrastructure.Repository/Repository.Tests/Repository.Tests.csproj"
|
|
||||||
COPY . .
|
|
||||||
WORKDIR "/src/Infrastructure/Infrastructure.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/Infrastructure/Infrastructure.Repository/Repository.Tests
|
|
||||||
ENTRYPOINT ["dotnet", "test", "./Repository.Tests.csproj", "-c", "Release", "--logger", "trx;LogFileName=/app/test-results/repository-tests.trx"]
|
|
||||||
@@ -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.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);
|
||||||
|
}
|
||||||
@@ -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.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,6 +1,6 @@
|
|||||||
using Domain.Core.Entities;
|
using Domain.Core.Entities;
|
||||||
using Infrastructure.PasswordHashing;
|
using Infrastructure.PasswordHashing;
|
||||||
using Repository.Core.Repositories.Auth;
|
using Infrastructure.Repository.Auth;
|
||||||
|
|
||||||
namespace Service.Core.Auth;
|
namespace Service.Core.Auth;
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Domain\Domain.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.csproj" />
|
||||||
<ProjectReference
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
|
||||||
Include="..\..\Infrastructure\Infrastructure.Repository\Repository.Core\Repository.Core.csproj" />
|
|
||||||
<ProjectReference
|
<ProjectReference
|
||||||
Include="..\..\Infrastructure\Infrastructure.PasswordHashing\Infrastructure.PasswordHashing.csproj" />
|
Include="..\..\Infrastructure\Infrastructure.PasswordHashing\Infrastructure.PasswordHashing.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Domain.Core.Entities;
|
using Domain.Core.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