mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-15 18:14:44 +00:00
Compare commits
2 Commits
03ca966708
...
2b82d1918b
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b82d1918b | |||
| 8a047e8d9f |
@@ -65,7 +65,7 @@ MediatR/DI, never over HTTP:
|
|||||||
└─────────────────────────┴─────────────────────────┴────────────────────┘
|
└─────────────────────────┴─────────────────────────┴────────────────────┘
|
||||||
↓
|
↓
|
||||||
┌─────────────────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
│ Infrastructure.Sql, Infrastructure.Jwt, Infrastructure.PasswordHashing, │
|
│ Database.Connection, Infrastructure.Jwt, Infrastructure.PasswordHashing,│
|
||||||
│ Infrastructure.Email, Infrastructure.Email.Templates │
|
│ Infrastructure.Email, Infrastructure.Email.Templates │
|
||||||
└─────────────────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────────────────┘
|
||||||
↓
|
↓
|
||||||
@@ -176,8 +176,8 @@ whichever slices need them
|
|||||||
|
|
||||||
**Components**:
|
**Components**:
|
||||||
|
|
||||||
- **Infrastructure.Sql**: generic ADO.NET connection/command plumbing
|
- **Database.Connection**: generic ADO.NET connection/command plumbing
|
||||||
(`ISqlConnectionFactory`, the abstract `Repository<T>` base class), not
|
(`DefaultSqlConnectionFactory`, the abstract `DapperRepository` base class), not
|
||||||
domain-specific; each slice's own `Repository/` folder builds on this
|
domain-specific; each slice's own `Repository/` folder builds on this
|
||||||
- **Infrastructure.Jwt**: JWT token generation and validation
|
- **Infrastructure.Jwt**: JWT token generation and validation
|
||||||
- **Infrastructure.PasswordHashing**: Argon2id password hashing
|
- **Infrastructure.PasswordHashing**: Argon2id password hashing
|
||||||
@@ -266,7 +266,7 @@ only:
|
|||||||
- `Features.Auth/Repository/IAuthRepository.cs`
|
- `Features.Auth/Repository/IAuthRepository.cs`
|
||||||
- `Features.Breweries/Repository/IBreweryRepository.cs`
|
- `Features.Breweries/Repository/IBreweryRepository.cs`
|
||||||
- `Features.UserManagement/Repository/IUserAccountRepository.cs`
|
- `Features.UserManagement/Repository/IUserAccountRepository.cs`
|
||||||
- `Infrastructure.Sql/DefaultSqlConnectionFactory.cs`: the generic connection
|
- `Database.Connection/DefaultSqlConnectionFactory.cs`: the generic connection
|
||||||
factory every slice's repository builds on
|
factory every slice's repository builds on
|
||||||
|
|
||||||
**Benefits**:
|
**Benefits**:
|
||||||
@@ -295,7 +295,7 @@ method that registers its repository and slice-internal services
|
|||||||
**Lifetimes**:
|
**Lifetimes**:
|
||||||
|
|
||||||
- Scoped: Repositories, slice-internal services (per request)
|
- Scoped: Repositories, slice-internal services (per request)
|
||||||
- Singleton: `ISqlConnectionFactory`
|
- Singleton: `DefaultSqlConnectionFactory`
|
||||||
- Transient: Utilities, helpers
|
- Transient: Utilities, helpers
|
||||||
|
|
||||||
#### Direct SQL via Dapper/ADO.NET
|
#### Direct SQL via Dapper/ADO.NET
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
||||||
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Sql\Infrastructure.Sql.csproj" />
|
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj" />
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj" />
|
||||||
<ProjectReference Include="..\..\Features\Features.Breweries\Features.Breweries.csproj" />
|
<ProjectReference Include="..\..\Features\Features.Breweries\Features.Breweries.csproj" />
|
||||||
<ProjectReference Include="..\..\Features\Features.UserManagement\Features.UserManagement.csproj" />
|
<ProjectReference Include="..\..\Features\Features.UserManagement\Features.UserManagement.csproj" />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
|
using Infrastructure.Configuration;
|
||||||
using Infrastructure.Jwt;
|
using Infrastructure.Jwt;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
@@ -32,9 +33,9 @@ public class JwtAuthenticationHandler(
|
|||||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||||
{
|
{
|
||||||
// Use the same access-token secret source as TokenService to avoid mismatched validation.
|
// Use the same access-token secret source as TokenService to avoid mismatched validation.
|
||||||
string? secret = configuration["ACCESS_TOKEN_SECRET"];
|
string? secret = configuration[ConfigurationKeys.AccessTokenSecret];
|
||||||
if (string.IsNullOrWhiteSpace(secret))
|
if (string.IsNullOrWhiteSpace(secret))
|
||||||
secret = configuration["Jwt:SecretKey"];
|
secret = configuration[ConfigurationKeys.JwtSecretKeyFallback];
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(secret))
|
if (string.IsNullOrWhiteSpace(secret))
|
||||||
return AuthenticateResult.Fail("JWT secret is not configured");
|
return AuthenticateResult.Fail("JWT secret is not configured");
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ WORKDIR /src
|
|||||||
COPY ["Biergarten.API.slnx", "./"]
|
COPY ["Biergarten.API.slnx", "./"]
|
||||||
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 ["Database/Database.Connection/Database.Connection.csproj", "Database/Database.Connection/"]
|
||||||
COPY ["Database/Database.Migrations/Database.Migrations.csproj", "Database/Database.Migrations/"]
|
COPY ["Database/Database.Migrations/Database.Migrations.csproj", "Database/Database.Migrations/"]
|
||||||
COPY ["Database/Database.Seed/Database.Seed.csproj", "Database/Database.Seed/"]
|
COPY ["Database/Database.Seed/Database.Seed.csproj", "Database/Database.Seed/"]
|
||||||
COPY ["Domain/Domain.Entities/Domain.Entities.csproj", "Domain/Domain.Entities/"]
|
COPY ["Domain/Domain.Entities/Domain.Entities.csproj", "Domain/Domain.Entities/"]
|
||||||
@@ -35,7 +36,6 @@ COPY ["Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj", "Infras
|
|||||||
COPY ["Infrastructure/Infrastructure.Email.Templates/Infrastructure.Email.Templates.csproj", "Infrastructure/Infrastructure.Email.Templates/"]
|
COPY ["Infrastructure/Infrastructure.Email.Templates/Infrastructure.Email.Templates.csproj", "Infrastructure/Infrastructure.Email.Templates/"]
|
||||||
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 ["Infrastructure/Infrastructure.Sql/Infrastructure.Sql.csproj", "Infrastructure/Infrastructure.Sql/"]
|
|
||||||
COPY ["Shared/Shared.Application/Shared.Application.csproj", "Shared/Shared.Application/"]
|
COPY ["Shared/Shared.Application/Shared.Application.csproj", "Shared/Shared.Application/"]
|
||||||
COPY ["Shared/Shared.Contracts/Shared.Contracts.csproj", "Shared/Shared.Contracts/"]
|
COPY ["Shared/Shared.Contracts/Shared.Contracts.csproj", "Shared/Shared.Contracts/"]
|
||||||
RUN dotnet restore "Biergarten.API.slnx"
|
RUN dotnet restore "Biergarten.API.slnx"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using API.Core;
|
using API.Core;
|
||||||
using API.Core.Authentication;
|
using API.Core.Authentication;
|
||||||
|
using Database.Connection.DependencyInjection;
|
||||||
using Features.Auth.Controllers;
|
using Features.Auth.Controllers;
|
||||||
using Features.Auth.DependencyInjection;
|
using Features.Auth.DependencyInjection;
|
||||||
using Features.Breweries.Controllers;
|
using Features.Breweries.Controllers;
|
||||||
@@ -11,7 +12,6 @@ using Features.UserManagement.DependencyInjection;
|
|||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using FluentValidation.AspNetCore;
|
using FluentValidation.AspNetCore;
|
||||||
using Infrastructure.Jwt;
|
using Infrastructure.Jwt;
|
||||||
using Infrastructure.Sql.DependencyInjection;
|
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
using Shared.Application.Behaviors;
|
using Shared.Application.Behaviors;
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ builder.Logging.AddConsole();
|
|||||||
if (!builder.Environment.IsProduction())
|
if (!builder.Environment.IsProduction())
|
||||||
builder.Logging.AddDebug();
|
builder.Logging.AddDebug();
|
||||||
|
|
||||||
builder.Services.AddInfrastructureSql();
|
builder.Services.AddDatabaseConnection();
|
||||||
|
|
||||||
builder.Services.AddFeaturesBreweries();
|
builder.Services.AddFeaturesBreweries();
|
||||||
builder.Services.AddFeaturesUserManagement();
|
builder.Services.AddFeaturesUserManagement();
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\API.Core\API.Core.csproj" />
|
<ProjectReference Include="..\API.Core\API.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email\Infrastructure.Email.csproj" />
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email\Infrastructure.Email.csproj" />
|
||||||
<ProjectReference Include="..\..\Features\Features.Emails\Features.Emails.csproj" />
|
<ProjectReference Include="..\..\Features\Features.Emails\Features.Emails.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ WORKDIR /src
|
|||||||
COPY ["Biergarten.API.slnx", "./"]
|
COPY ["Biergarten.API.slnx", "./"]
|
||||||
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 ["Database/Database.Connection/Database.Connection.csproj", "Database/Database.Connection/"]
|
||||||
COPY ["Database/Database.Migrations/Database.Migrations.csproj", "Database/Database.Migrations/"]
|
COPY ["Database/Database.Migrations/Database.Migrations.csproj", "Database/Database.Migrations/"]
|
||||||
COPY ["Database/Database.Seed/Database.Seed.csproj", "Database/Database.Seed/"]
|
COPY ["Database/Database.Seed/Database.Seed.csproj", "Database/Database.Seed/"]
|
||||||
COPY ["Domain/Domain.Entities/Domain.Entities.csproj", "Domain/Domain.Entities/"]
|
COPY ["Domain/Domain.Entities/Domain.Entities.csproj", "Domain/Domain.Entities/"]
|
||||||
@@ -25,7 +26,6 @@ COPY ["Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj", "Infras
|
|||||||
COPY ["Infrastructure/Infrastructure.Email.Templates/Infrastructure.Email.Templates.csproj", "Infrastructure/Infrastructure.Email.Templates/"]
|
COPY ["Infrastructure/Infrastructure.Email.Templates/Infrastructure.Email.Templates.csproj", "Infrastructure/Infrastructure.Email.Templates/"]
|
||||||
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 ["Infrastructure/Infrastructure.Sql/Infrastructure.Sql.csproj", "Infrastructure/Infrastructure.Sql/"]
|
|
||||||
COPY ["Shared/Shared.Application/Shared.Application.csproj", "Shared/Shared.Application/"]
|
COPY ["Shared/Shared.Application/Shared.Application.csproj", "Shared/Shared.Application/"]
|
||||||
COPY ["Shared/Shared.Contracts/Shared.Contracts.csproj", "Shared/Shared.Contracts/"]
|
COPY ["Shared/Shared.Contracts/Shared.Contracts.csproj", "Shared/Shared.Contracts/"]
|
||||||
RUN dotnet restore "Biergarten.API.slnx"
|
RUN dotnet restore "Biergarten.API.slnx"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
using Infrastructure.Configuration;
|
||||||
using Infrastructure.Jwt;
|
using Infrastructure.Jwt;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -430,7 +431,7 @@ public class AuthSteps(ScenarioContext scenario)
|
|||||||
? user
|
? user
|
||||||
: throw new InvalidOperationException("registered username not found in scenario");
|
: throw new InvalidOperationException("registered username not found in scenario");
|
||||||
|
|
||||||
string secret = GetRequiredConfigValue("ACCESS_TOKEN_SECRET");
|
string secret = GetRequiredConfigValue(ConfigurationKeys.AccessTokenSecret);
|
||||||
scenario["accessToken"] = GenerateJwtToken(
|
scenario["accessToken"] = GenerateJwtToken(
|
||||||
userId,
|
userId,
|
||||||
username,
|
username,
|
||||||
@@ -449,7 +450,7 @@ public class AuthSteps(ScenarioContext scenario)
|
|||||||
? user
|
? user
|
||||||
: throw new InvalidOperationException("registered username not found in scenario");
|
: throw new InvalidOperationException("registered username not found in scenario");
|
||||||
|
|
||||||
string secret = GetRequiredConfigValue("CONFIRMATION_TOKEN_SECRET");
|
string secret = GetRequiredConfigValue(ConfigurationKeys.ConfirmationTokenSecret);
|
||||||
scenario["confirmationToken"] = GenerateJwtToken(
|
scenario["confirmationToken"] = GenerateJwtToken(
|
||||||
userId,
|
userId,
|
||||||
username,
|
username,
|
||||||
@@ -468,7 +469,7 @@ public class AuthSteps(ScenarioContext scenario)
|
|||||||
? user
|
? user
|
||||||
: throw new InvalidOperationException("registered username not found in scenario");
|
: throw new InvalidOperationException("registered username not found in scenario");
|
||||||
|
|
||||||
string secret = GetRequiredConfigValue("CONFIRMATION_TOKEN_SECRET");
|
string secret = GetRequiredConfigValue(ConfigurationKeys.ConfirmationTokenSecret);
|
||||||
scenario["confirmationToken"] = GenerateJwtToken(
|
scenario["confirmationToken"] = GenerateJwtToken(
|
||||||
userId,
|
userId,
|
||||||
username,
|
username,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<Folder Name="/Database/">
|
<Folder Name="/Database/">
|
||||||
<Project Path="Database/Database.Migrations/Database.Migrations.csproj" />
|
<Project Path="Database/Database.Migrations/Database.Migrations.csproj" />
|
||||||
<Project Path="Database/Database.Seed/Database.Seed.csproj" />
|
<Project Path="Database/Database.Seed/Database.Seed.csproj" />
|
||||||
|
<Project Path="Database/Database.Connection/Database.Connection.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Domain/">
|
<Folder Name="/Domain/">
|
||||||
<Project Path="Domain/Domain.Entities/Domain.Entities.csproj" />
|
<Project Path="Domain/Domain.Entities/Domain.Entities.csproj" />
|
||||||
@@ -16,7 +17,6 @@
|
|||||||
<Project Path="Infrastructure/Infrastructure.Email.Templates/Infrastructure.Email.Templates.csproj" />
|
<Project Path="Infrastructure/Infrastructure.Email.Templates/Infrastructure.Email.Templates.csproj" />
|
||||||
<Project Path="Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj" />
|
<Project Path="Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj" />
|
||||||
<Project Path="Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj" />
|
<Project Path="Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj" />
|
||||||
<Project Path="Infrastructure/Infrastructure.Sql/Infrastructure.Sql.csproj" />
|
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Features/">
|
<Folder Name="/Features/">
|
||||||
<Project Path="Features/Features.Breweries/Features.Breweries.csproj" />
|
<Project Path="Features/Features.Breweries/Features.Breweries.csproj" />
|
||||||
|
|||||||
@@ -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>Infrastructure.Sql</RootNamespace>
|
<RootNamespace>Database.Connection</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Dapper" Version="2.1.79" />
|
<PackageReference Include="Dapper" Version="2.1.79" />
|
||||||
@@ -11,11 +11,14 @@
|
|||||||
<PackageReference Include="Microsoft.SqlServer.Types" Version="160.1000.6" />
|
<PackageReference Include="Microsoft.SqlServer.Types" Version="160.1000.6" />
|
||||||
<PackageReference
|
<PackageReference
|
||||||
Include="Microsoft.Extensions.Configuration.Abstractions"
|
Include="Microsoft.Extensions.Configuration.Abstractions"
|
||||||
Version="9.0.0"
|
Version="10.0.1"
|
||||||
/>
|
/>
|
||||||
<PackageReference
|
<PackageReference
|
||||||
Include="Microsoft.Extensions.DependencyInjection.Abstractions"
|
Include="Microsoft.Extensions.DependencyInjection.Abstractions"
|
||||||
Version="10.0.0"
|
Version="10.0.0"
|
||||||
/>
|
/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using Infrastructure.Configuration;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace Database.Connection;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default <see cref="DefaultSqlConnectionFactory" /> implementation that creates SQL Server connections,
|
||||||
|
/// resolving the connection string from environment variables or application configuration.
|
||||||
|
/// </summary>
|
||||||
|
public class DefaultSqlConnectionFactory(IConfiguration configuration) : ISqlConnectionFactory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The resolved SQL Server connection string, preferring (in order): the <c>DB_CONNECTION_STRING</c>
|
||||||
|
/// configuration value, and a connection string built from individual <c>DB_*</c> configuration values.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="InvalidOperationException">Thrown when no connection string can be resolved from any source.</exception>
|
||||||
|
public string ConnectionString
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
string? fullConnectionString = configuration[ConfigurationKeys.DbConnectionString];
|
||||||
|
if (!string.IsNullOrEmpty(fullConnectionString))
|
||||||
|
return fullConnectionString;
|
||||||
|
|
||||||
|
// Try to build from individual configuration values (preferred method for Docker)
|
||||||
|
if (
|
||||||
|
SqlConnectionStringHelper.TryGetStandardDbConnectionString(
|
||||||
|
configuration,
|
||||||
|
out var connectionString
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return connectionString;
|
||||||
|
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Database connection string not configured. Set DB_CONNECTION_STRING or DB_SERVER, DB_NAME, DB_USER, DB_PASSWORD env vars."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public SqlConnection CreateConnection()
|
||||||
|
{
|
||||||
|
return new SqlConnection(ConnectionString);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Infrastructure.Sql.DependencyInjection;
|
namespace Database.Connection.DependencyInjection;
|
||||||
|
|
||||||
/// <summary>Extension methods for registering SQL infrastructure services with dependency injection.</summary>
|
/// <summary>Extension methods for registering database connection services with dependency injection.</summary>
|
||||||
public static class InfrastructureSqlServiceCollectionExtensions
|
public static class DatabaseConnectionServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
/// <summary>Registers <see cref="DefaultSqlConnectionFactory" /> as a singleton <see cref="ISqlConnectionFactory" />.</summary>
|
/// <summary>Registers <see cref="DefaultSqlConnectionFactory" /> as a singleton <see cref="ISqlConnectionFactory" />.</summary>
|
||||||
/// <returns>The same <paramref name="services" /> instance, for chaining.</returns>
|
/// <returns>The same <paramref name="services" /> instance, for chaining.</returns>
|
||||||
public static IServiceCollection AddInfrastructureSql(this IServiceCollection services)
|
public static IServiceCollection AddDatabaseConnection(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddSingleton<ISqlConnectionFactory, DefaultSqlConnectionFactory>();
|
services.AddSingleton<ISqlConnectionFactory, DefaultSqlConnectionFactory>();
|
||||||
return services;
|
return services;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
|
||||||
|
namespace Database.Connection;
|
||||||
|
|
||||||
|
public interface ISqlConnectionFactory
|
||||||
|
{
|
||||||
|
public string ConnectionString { get; }
|
||||||
|
|
||||||
|
public SqlConnection CreateConnection();
|
||||||
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
|
|
||||||
namespace Infrastructure.Sql;
|
namespace Database.Connection;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base class for Dapper-based repositories, providing shared connection creation for derived
|
/// Base class for Dapper-based repositories, providing shared connection creation for derived
|
||||||
/// repository implementations.
|
/// repository implementations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class Repository<T>(ISqlConnectionFactory connectionFactory)
|
public abstract class DapperRepository(ISqlConnectionFactory connectionFactory)
|
||||||
where T : class
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates and opens a new database connection using the configured <see cref="ISqlConnectionFactory" />.
|
/// Creates and opens a new database connection using the configured <see cref="ISqlConnectionFactory" />.
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
using Infrastructure.Configuration;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace Database.Connection;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper for building SQL Server connection strings from configuration
|
||||||
|
/// (backed by the <c>DB_*</c> environment variables).
|
||||||
|
/// </summary>
|
||||||
|
public static class SqlConnectionStringHelper
|
||||||
|
{
|
||||||
|
private static SqlConnectionStringBuilder MakeConfiguredConnectionBuilder(
|
||||||
|
IConfiguration configuration
|
||||||
|
) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
DataSource = ConfigurationHelpers.GetKeyOrThrow(
|
||||||
|
configuration,
|
||||||
|
ConfigurationKeys.DbServer
|
||||||
|
),
|
||||||
|
UserID = ConfigurationHelpers.GetKeyOrThrow(configuration, ConfigurationKeys.DbUser),
|
||||||
|
Password = ConfigurationHelpers.GetKeyOrThrow(
|
||||||
|
configuration,
|
||||||
|
ConfigurationKeys.DbPassword
|
||||||
|
),
|
||||||
|
TrustServerCertificate = ResolveTrustServerCertificate(configuration),
|
||||||
|
Encrypt = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a SQL Server connection string from configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Returns <c>false</c> if DB_SERVER, DB_NAME, DB_USER, or DB_PASSWORD is unset.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="configuration">The configuration to read the <c>DB_*</c> values from.</param>
|
||||||
|
/// <param name="connectionString">The resolved connection string, if successful.</param>
|
||||||
|
public static bool TryGetStandardDbConnectionString(
|
||||||
|
IConfiguration configuration,
|
||||||
|
out string connectionString
|
||||||
|
)
|
||||||
|
{
|
||||||
|
connectionString = string.Empty;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SqlConnectionStringBuilder builder = MakeConfiguredConnectionBuilder(configuration);
|
||||||
|
builder.InitialCatalog = ConfigurationHelpers.GetKeyOrThrow(
|
||||||
|
configuration,
|
||||||
|
ConfigurationKeys.DbName
|
||||||
|
);
|
||||||
|
connectionString = builder.ConnectionString;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetMasterConnectionString(
|
||||||
|
IConfiguration configuration,
|
||||||
|
out string connectionString
|
||||||
|
)
|
||||||
|
{
|
||||||
|
connectionString = string.Empty;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var builder = MakeConfiguredConnectionBuilder(configuration);
|
||||||
|
builder.InitialCatalog = "master";
|
||||||
|
|
||||||
|
connectionString = builder.ConnectionString;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetDatabaseNameFromConnectionString(
|
||||||
|
string connectionString,
|
||||||
|
out string databaseName
|
||||||
|
)
|
||||||
|
{
|
||||||
|
databaseName = string.Empty;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var builder = new SqlConnectionStringBuilder(connectionString);
|
||||||
|
databaseName = builder.InitialCatalog;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves the <c>DB_TRUST_SERVER_CERTIFICATE</c> configuration value, defaulting to
|
||||||
|
/// <c>true</c> when it is unset or not a valid boolean.
|
||||||
|
/// </summary>
|
||||||
|
private static bool ResolveTrustServerCertificate(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
return !bool.TryParse(
|
||||||
|
configuration[ConfigurationKeys.DbTrustServerCertificate],
|
||||||
|
out bool trustServerCertificate
|
||||||
|
) || trustServerCertificate;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,8 @@
|
|||||||
/>
|
/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Sql\Infrastructure.Sql.csproj" />
|
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="scripts/**/*.sql" />
|
<EmbeddedResource Include="scripts/**/*.sql" />
|
||||||
|
|||||||
113
web/backend/Database/Database.Migrations/DbMigrator.cs
Normal file
113
web/backend/Database/Database.Migrations/DbMigrator.cs
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using Dapper;
|
||||||
|
using Database.Connection;
|
||||||
|
using DbUp;
|
||||||
|
using DbUp.Engine;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
|
||||||
|
namespace Database.Migrations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owns the configuration and state for a single database migration run,
|
||||||
|
/// and performs the drop/create/deploy steps against SQL Server.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class DbMigrator(
|
||||||
|
ISqlConnectionFactory standardConnectionStandardConnectionFactory,
|
||||||
|
ISqlConnectionFactory masterConnectionConnectionFactory,
|
||||||
|
bool clearDatabase)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the full migration workflow: optional drop, create-if-not-exists,
|
||||||
|
/// then deploy DbUp scripts. Throws if the upgrade fails.
|
||||||
|
/// </summary>
|
||||||
|
public async Task RunAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (clearDatabase)
|
||||||
|
{
|
||||||
|
await DropDatabaseIfExistsAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
await CreateDatabaseIfNotExistsAsync(cancellationToken);
|
||||||
|
|
||||||
|
DatabaseUpgradeResult result = DeployMigrations();
|
||||||
|
|
||||||
|
if (!result.Successful)
|
||||||
|
{
|
||||||
|
throw result.Error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DropDatabaseIfExistsAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
string databaseName = ResolveDatabaseName();
|
||||||
|
|
||||||
|
await using SqlConnection connection = masterConnectionConnectionFactory.CreateConnection();
|
||||||
|
await connection.OpenAsync(cancellationToken);
|
||||||
|
|
||||||
|
await connection.ExecuteAsync(
|
||||||
|
new CommandDefinition(
|
||||||
|
"""
|
||||||
|
DECLARE @statement nvarchar(max) =
|
||||||
|
N'ALTER DATABASE ' + QUOTENAME(@databaseName) + N' SET SINGLE_USER WITH ROLLBACK IMMEDIATE;'
|
||||||
|
+ N' DROP DATABASE ' + QUOTENAME(@databaseName) + N';';
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM sys.databases WHERE name = @databaseName)
|
||||||
|
EXEC sys.sp_executesql @statement;
|
||||||
|
""",
|
||||||
|
new { databaseName },
|
||||||
|
cancellationToken: cancellationToken
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
Console.WriteLine($"Database '{databaseName}' dropped, or did not exist.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateDatabaseIfNotExistsAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
string databaseName = ResolveDatabaseName();
|
||||||
|
|
||||||
|
await using SqlConnection connection = masterConnectionConnectionFactory.CreateConnection();
|
||||||
|
await connection.OpenAsync(cancellationToken);
|
||||||
|
|
||||||
|
await connection.ExecuteAsync(
|
||||||
|
new CommandDefinition(
|
||||||
|
"""
|
||||||
|
DECLARE @statement nvarchar(max);
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM sys.databases WHERE name = @databaseName)
|
||||||
|
BEGIN
|
||||||
|
SET @statement = N'CREATE DATABASE ' + QUOTENAME(@databaseName) + N';';
|
||||||
|
EXEC sys.sp_executesql @statement;
|
||||||
|
END
|
||||||
|
""",
|
||||||
|
new { databaseName },
|
||||||
|
cancellationToken: cancellationToken
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
Console.WriteLine($"Database '{databaseName}' is present.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private DatabaseUpgradeResult DeployMigrations()
|
||||||
|
{
|
||||||
|
UpgradeEngine upgrader = DeployChanges
|
||||||
|
.To.SqlDatabase(standardConnectionStandardConnectionFactory.ConnectionString)
|
||||||
|
.WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly())
|
||||||
|
.WithTransactionPerScript()
|
||||||
|
.LogToConsole()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
return upgrader.PerformUpgrade();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolveDatabaseName()
|
||||||
|
{
|
||||||
|
return !SqlConnectionStringHelper.TryGetDatabaseNameFromConnectionString(
|
||||||
|
standardConnectionStandardConnectionFactory.ConnectionString,
|
||||||
|
out var databaseName
|
||||||
|
)
|
||||||
|
? throw new InvalidOperationException(
|
||||||
|
"Unable to resolve a database name from the connection string."
|
||||||
|
)
|
||||||
|
: databaseName;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
|||||||
ARG BUILD_CONFIGURATION=Release
|
ARG BUILD_CONFIGURATION=Release
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
# Copy everything from the context (src/Core), matching Database.Seed/Dockerfile's context so
|
# Copy everything from the context (src/Core), matching Database.Seed/Dockerfile's context so
|
||||||
# ProjectReference paths outside Database/ (e.g. Infrastructure.Sql) resolve.
|
# ProjectReference paths resolve.
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN dotnet restore "./Database/Database.Migrations/Database.Migrations.csproj"
|
RUN dotnet restore "./Database/Database.Migrations/Database.Migrations.csproj"
|
||||||
RUN dotnet build "./Database/Database.Migrations/Database.Migrations.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
RUN dotnet build "./Database/Database.Migrations/Database.Migrations.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using Database.Connection;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace Database.Migrations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="ISqlConnectionFactory" /> implementation that connects to the <c>master</c> database,
|
||||||
|
/// used for create and drop operations on the target application database.
|
||||||
|
/// </summary>
|
||||||
|
public class MasterSqlConnectionFactory(IConfiguration configuration) : ISqlConnectionFactory
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string ConnectionString
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
SqlConnectionStringHelper.TryGetMasterConnectionString(
|
||||||
|
configuration,
|
||||||
|
out var connectionString
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return connectionString;
|
||||||
|
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Database connection string not configured. Set DB_SERVER, DB_USER, DB_PASSWORD env vars."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public SqlConnection CreateConnection()
|
||||||
|
{
|
||||||
|
return new SqlConnection(ConnectionString);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,17 @@
|
|||||||
using System.Data;
|
using Database.Connection;
|
||||||
using System.Reflection;
|
using Infrastructure.Configuration;
|
||||||
using DbUp;
|
|
||||||
using DbUp.Engine;
|
|
||||||
using Infrastructure.Sql;
|
|
||||||
using Microsoft.Data.SqlClient;
|
using Microsoft.Data.SqlClient;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace Database.Migrations;
|
namespace Database.Migrations;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Entry point for the database migration runner. Reads connection details from configuration
|
/// Application entry point. Responsible only for wiring up configuration
|
||||||
/// (backed by environment variables), optionally drops and recreates the target database, and
|
/// and delegating the migration run to <see cref="DbMigrator"/>.
|
||||||
/// applies all SQL migration scripts embedded in this assembly using DbUp.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Program
|
internal static class Program
|
||||||
{
|
{
|
||||||
/// <summary>
|
private static async Task Main(string[] args)
|
||||||
/// Migration runner entry point. Drops the target database when <c>CLEAR_DATABASE</c> is
|
|
||||||
/// set to <c>true</c>, ensures the database exists, then deploys all pending migrations.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>
|
|
||||||
/// <c>0</c> if migrations completed successfully; <c>1</c> if they failed or an error occurred.
|
|
||||||
/// </returns>
|
|
||||||
public static async Task<int> Main()
|
|
||||||
{
|
{
|
||||||
Console.WriteLine("Starting database migrations...");
|
Console.WriteLine("Starting database migrations...");
|
||||||
|
|
||||||
@@ -31,163 +20,24 @@ public static class Program
|
|||||||
IConfiguration configuration = new ConfigurationBuilder()
|
IConfiguration configuration = new ConfigurationBuilder()
|
||||||
.AddEnvironmentVariables()
|
.AddEnvironmentVariables()
|
||||||
.Build();
|
.Build();
|
||||||
DatabaseSettings settings = DatabaseSettings.FromConfiguration(configuration);
|
|
||||||
|
|
||||||
if (settings.ClearDatabase)
|
bool clearDatabase = string.Equals(
|
||||||
{
|
configuration[ConfigurationKeys.ClearDatabase],
|
||||||
Console.WriteLine(
|
"true",
|
||||||
$"CLEAR_DATABASE is enabled. Dropping database '{settings.DatabaseName}'..."
|
StringComparison.OrdinalIgnoreCase
|
||||||
);
|
);
|
||||||
await DropDatabaseIfExistsAsync(settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
await CreateDatabaseIfNotExistsAsync(settings);
|
DbMigrator migrator = new(
|
||||||
|
new DefaultSqlConnectionFactory(configuration),
|
||||||
return DeployMigrations(settings.ConnectionString) ? 0 : 1;
|
new MasterSqlConnectionFactory(configuration),
|
||||||
|
clearDatabase
|
||||||
|
);
|
||||||
|
await migrator.RunAsync();
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is InvalidOperationException or SqlException)
|
catch (Exception ex) when (ex is InvalidOperationException or SqlException)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("An error occurred during database migrations:");
|
Console.Error.WriteLine("An error occurred during database migrations:");
|
||||||
Console.Error.WriteLine(ex.Message);
|
Console.Error.WriteLine(ex.Message);
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Drops the target database if it exists, first forcing it into single-user mode with
|
|
||||||
/// rollback immediate so that any existing connections are terminated.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="settings">The resolved database settings.</param>
|
|
||||||
/// <exception cref="SqlException">Thrown if the connection or the drop operation fails.</exception>
|
|
||||||
private static async Task DropDatabaseIfExistsAsync(DatabaseSettings settings)
|
|
||||||
{
|
|
||||||
const string sql = """
|
|
||||||
DECLARE @statement nvarchar(max);
|
|
||||||
|
|
||||||
IF EXISTS (SELECT 1 FROM sys.databases WHERE name = @databaseName)
|
|
||||||
BEGIN
|
|
||||||
SET @statement =
|
|
||||||
N'ALTER DATABASE ' + QUOTENAME(@databaseName)
|
|
||||||
+ N' SET SINGLE_USER WITH ROLLBACK IMMEDIATE;';
|
|
||||||
EXEC sys.sp_executesql @statement;
|
|
||||||
|
|
||||||
SET @statement = N'DROP DATABASE ' + QUOTENAME(@databaseName) + N';';
|
|
||||||
EXEC sys.sp_executesql @statement;
|
|
||||||
END
|
|
||||||
""";
|
|
||||||
|
|
||||||
await ExecuteNonQueryAsync(settings.MasterConnectionString, sql, settings.DatabaseName);
|
|
||||||
Console.WriteLine($"Database '{settings.DatabaseName}' dropped, or did not exist.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates the target database on the <c>master</c> connection if it does not already exist.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="settings">The resolved database settings.</param>
|
|
||||||
/// <exception cref="SqlException">Thrown if the connection or the create operation fails.</exception>
|
|
||||||
private static async Task CreateDatabaseIfNotExistsAsync(DatabaseSettings settings)
|
|
||||||
{
|
|
||||||
const string sql = """
|
|
||||||
DECLARE @statement nvarchar(max);
|
|
||||||
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM sys.databases WHERE name = @databaseName)
|
|
||||||
BEGIN
|
|
||||||
SET @statement = N'CREATE DATABASE ' + QUOTENAME(@databaseName) + N';';
|
|
||||||
EXEC sys.sp_executesql @statement;
|
|
||||||
END
|
|
||||||
""";
|
|
||||||
|
|
||||||
await ExecuteNonQueryAsync(settings.MasterConnectionString, sql, settings.DatabaseName);
|
|
||||||
Console.WriteLine($"Database '{settings.DatabaseName}' is present.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <param name="databaseName">The value bound to the <c>@databaseName</c> parameter.</param>
|
|
||||||
private static async Task ExecuteNonQueryAsync(
|
|
||||||
string connectionString,
|
|
||||||
string sql,
|
|
||||||
string databaseName
|
|
||||||
)
|
|
||||||
{
|
|
||||||
await using SqlConnection connection = new(connectionString);
|
|
||||||
await connection.OpenAsync();
|
|
||||||
|
|
||||||
await using SqlCommand command = new(sql, connection);
|
|
||||||
command.Parameters.Add("@databaseName", SqlDbType.NVarChar, 128).Value = databaseName;
|
|
||||||
|
|
||||||
await command.ExecuteNonQueryAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Applies all pending SQL migration scripts embedded in this assembly to the target
|
|
||||||
/// database using DbUp, logging progress to the console.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="connectionString">The connection string for the target application database.</param>
|
|
||||||
/// <returns><c>true</c> if the upgrade completed successfully; otherwise <c>false</c>.</returns>
|
|
||||||
private static bool DeployMigrations(string connectionString)
|
|
||||||
{
|
|
||||||
UpgradeEngine upgrader = DeployChanges
|
|
||||||
.To.SqlDatabase(connectionString)
|
|
||||||
.WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly())
|
|
||||||
.WithTransactionPerScript()
|
|
||||||
.LogToConsole()
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
DatabaseUpgradeResult result = upgrader.PerformUpgrade();
|
|
||||||
|
|
||||||
if (result.Successful)
|
|
||||||
{
|
|
||||||
Console.WriteLine("Database migrations completed successfully.");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.Error.WriteLine($"Database migrations failed: {result.Error}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Connection details and runtime options resolved from environment variables.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="ConnectionString">The connection string for the target application database.</param>
|
|
||||||
/// <param name="MasterConnectionString">
|
|
||||||
/// The connection string for the <c>master</c> database, used for create and drop operations.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="DatabaseName">The name of the target application database.</param>
|
|
||||||
/// <param name="ClearDatabase">Whether the target database should be dropped before migrating.</param>
|
|
||||||
private sealed record DatabaseSettings(
|
|
||||||
string ConnectionString,
|
|
||||||
string MasterConnectionString,
|
|
||||||
string DatabaseName,
|
|
||||||
bool ClearDatabase
|
|
||||||
)
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Builds the settings from the <c>DB_SERVER</c>, <c>DB_NAME</c>, <c>DB_USER</c>,
|
|
||||||
/// <c>DB_PASSWORD</c>, <c>DB_TRUST_SERVER_CERTIFICATE</c>, and <c>CLEAR_DATABASE</c>
|
|
||||||
/// configuration values.
|
|
||||||
/// </summary>
|
|
||||||
/// <exception cref="InvalidOperationException">
|
|
||||||
/// Thrown when a required configuration value is not set.
|
|
||||||
/// </exception>
|
|
||||||
public static DatabaseSettings FromConfiguration(IConfiguration configuration)
|
|
||||||
{
|
|
||||||
string databaseName =
|
|
||||||
configuration["DB_NAME"]
|
|
||||||
?? throw new InvalidOperationException(
|
|
||||||
"The DB_NAME environment variable is not set."
|
|
||||||
);
|
|
||||||
|
|
||||||
bool clearDatabase = string.Equals(
|
|
||||||
configuration["CLEAR_DATABASE"],
|
|
||||||
"true",
|
|
||||||
StringComparison.OrdinalIgnoreCase
|
|
||||||
);
|
|
||||||
|
|
||||||
return new DatabaseSettings(
|
|
||||||
SqlConnectionStringHelper.BuildConnectionString(configuration, databaseName),
|
|
||||||
SqlConnectionStringHelper.BuildMasterConnectionString(configuration),
|
|
||||||
databaseName,
|
|
||||||
clearDatabase
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Sql\Infrastructure.Sql.csproj" />
|
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
||||||
<ProjectReference Include="..\..\Features\Features.Breweries\Features.Breweries.csproj" />
|
<ProjectReference Include="..\..\Features\Features.Breweries\Features.Breweries.csproj" />
|
||||||
<ProjectReference Include="..\..\Features\Features.Auth\Features.Auth.csproj" />
|
<ProjectReference Include="..\..\Features\Features.Auth\Features.Auth.csproj" />
|
||||||
<ProjectReference Include="..\..\Features\Features.Locations\Features.Locations.csproj" />
|
<ProjectReference Include="..\..\Features\Features.Locations\Features.Locations.csproj" />
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
#region
|
#region
|
||||||
|
|
||||||
using Infrastructure.Sql;
|
using Database.Connection;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
namespace Database.Seed.DatabaseHelpers;
|
namespace Database.Seed.DatabaseHelpers;
|
||||||
|
|
||||||
public class ConnectionStrings
|
public static class ConnectionStrings
|
||||||
{
|
{
|
||||||
public static string GetSqlServerConnectionString(IConfiguration configuration) =>
|
public static string GetSqlServerConnectionString(IConfiguration configuration) =>
|
||||||
SqlConnectionStringHelper.BuildConnectionString(configuration);
|
SqlConnectionStringHelper.TryGetStandardDbConnectionString(
|
||||||
|
configuration,
|
||||||
|
out string connectionString
|
||||||
|
)
|
||||||
|
? connectionString
|
||||||
|
: throw new InvalidOperationException(
|
||||||
|
"Database connection string not configured. Set DB_SERVER, DB_NAME, DB_USER, DB_PASSWORD env vars."
|
||||||
|
);
|
||||||
|
|
||||||
public static string SqliteConnectionString =>
|
public static string SqliteConnectionString =>
|
||||||
"Data Source=SeedData/biergarten_seed_2026-08-24T01-52-09.841693Z.sqlite";
|
"Data Source=SeedData/biergarten_seed_2026-08-24T01-52-09.841693Z.sqlite";
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Database.Connection.DependencyInjection;
|
||||||
using Database.Seed.DatabaseHelpers;
|
using Database.Seed.DatabaseHelpers;
|
||||||
using Database.Seed.Geocoding;
|
using Database.Seed.Geocoding;
|
||||||
using Database.Seed.PipelineData;
|
using Database.Seed.PipelineData;
|
||||||
@@ -11,7 +12,6 @@ using Features.Locations.DependencyInjection;
|
|||||||
using Features.Locations.Dtos;
|
using Features.Locations.Dtos;
|
||||||
using Features.Locations.Repository;
|
using Features.Locations.Repository;
|
||||||
using idunno.Password;
|
using idunno.Password;
|
||||||
using Infrastructure.Sql.DependencyInjection;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -30,7 +30,7 @@ static async Task<int> RunAsync()
|
|||||||
|
|
||||||
ServiceCollection services = [];
|
ServiceCollection services = [];
|
||||||
services.AddSingleton(configuration);
|
services.AddSingleton(configuration);
|
||||||
services.AddInfrastructureSql();
|
services.AddDatabaseConnection();
|
||||||
services.AddFeaturesBreweries();
|
services.AddFeaturesBreweries();
|
||||||
services.AddFeaturesLocations();
|
services.AddFeaturesLocations();
|
||||||
services.AddFeaturesAuth();
|
services.AddFeaturesAuth();
|
||||||
|
|||||||
@@ -21,5 +21,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Features.Auth\Features.Auth.csproj" />
|
<ProjectReference Include="..\Features.Auth\Features.Auth.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Features.Auth.Identity;
|
|||||||
using Features.Auth.Services;
|
using Features.Auth.Services;
|
||||||
using Features.Auth.Tests.TestSupport;
|
using Features.Auth.Tests.TestSupport;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
using Infrastructure.Configuration;
|
||||||
using Infrastructure.Jwt;
|
using Infrastructure.Jwt;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
@@ -27,9 +28,11 @@ public class TokenServiceRefreshTests
|
|||||||
.AddInMemoryCollection(
|
.AddInMemoryCollection(
|
||||||
new Dictionary<string, string?>
|
new Dictionary<string, string?>
|
||||||
{
|
{
|
||||||
["ACCESS_TOKEN_SECRET"] = "test-access-secret-that-is-very-long-1234567890",
|
[ConfigurationKeys.AccessTokenSecret] =
|
||||||
["REFRESH_TOKEN_SECRET"] = "test-refresh-secret-that-is-very-long-1234567890",
|
"test-access-secret-that-is-very-long-1234567890",
|
||||||
["CONFIRMATION_TOKEN_SECRET"] =
|
[ConfigurationKeys.RefreshTokenSecret] =
|
||||||
|
"test-refresh-secret-that-is-very-long-1234567890",
|
||||||
|
[ConfigurationKeys.ConfirmationTokenSecret] =
|
||||||
"test-confirmation-secret-that-is-very-long-1234567890",
|
"test-confirmation-secret-that-is-very-long-1234567890",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Domain.Exceptions;
|
|||||||
using Features.Auth.Services;
|
using Features.Auth.Services;
|
||||||
using Features.Auth.Tests.TestSupport;
|
using Features.Auth.Tests.TestSupport;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
using Infrastructure.Configuration;
|
||||||
using Infrastructure.Jwt;
|
using Infrastructure.Jwt;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Moq;
|
using Moq;
|
||||||
@@ -23,9 +24,11 @@ public class TokenServiceValidationTests
|
|||||||
.AddInMemoryCollection(
|
.AddInMemoryCollection(
|
||||||
new Dictionary<string, string?>
|
new Dictionary<string, string?>
|
||||||
{
|
{
|
||||||
["ACCESS_TOKEN_SECRET"] = "test-access-secret-that-is-very-long-1234567890",
|
[ConfigurationKeys.AccessTokenSecret] =
|
||||||
["REFRESH_TOKEN_SECRET"] = "test-refresh-secret-that-is-very-long-1234567890",
|
"test-access-secret-that-is-very-long-1234567890",
|
||||||
["CONFIRMATION_TOKEN_SECRET"] =
|
[ConfigurationKeys.RefreshTokenSecret] =
|
||||||
|
"test-refresh-secret-that-is-very-long-1234567890",
|
||||||
|
[ConfigurationKeys.ConfirmationTokenSecret] =
|
||||||
"test-confirmation-secret-that-is-very-long-1234567890",
|
"test-confirmation-secret-that-is-very-long-1234567890",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -38,42 +41,6 @@ public class TokenServiceValidationTests
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ValidateAccessTokenAsync_WithValidToken_ReturnsValidatedToken()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
Guid userId = Guid.NewGuid();
|
|
||||||
const string username = "testuser";
|
|
||||||
const string token = "valid-access-token";
|
|
||||||
|
|
||||||
List<Claim> claims = new()
|
|
||||||
{
|
|
||||||
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
|
|
||||||
new Claim(JwtRegisteredClaimNames.UniqueName, username),
|
|
||||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
|
||||||
};
|
|
||||||
|
|
||||||
ClaimsIdentity claimsIdentity = new(claims);
|
|
||||||
ClaimsPrincipal principal = new(claimsIdentity);
|
|
||||||
|
|
||||||
_tokenInfraMock
|
|
||||||
.Setup(x => x.ValidateJwtAsync(token, It.IsAny<string>()))
|
|
||||||
.ReturnsAsync(principal);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
ValidatedToken result = await _tokenService.ValidateAccessTokenAsync(token);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
result.Should().NotBeNull();
|
|
||||||
result.UserId.Should().Be(userId);
|
|
||||||
result.Username.Should().Be(username);
|
|
||||||
result.Principal.Should().NotBeNull();
|
|
||||||
result
|
|
||||||
.Principal.FindFirst(JwtRegisteredClaimNames.Sub)
|
|
||||||
?.Value.Should()
|
|
||||||
.Be(userId.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidateRefreshTokenAsync_WithValidToken_ReturnsValidatedToken()
|
public async Task ValidateRefreshTokenAsync_WithValidToken_ReturnsValidatedToken()
|
||||||
{
|
{
|
||||||
@@ -137,24 +104,7 @@ public class TokenServiceValidationTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidateAccessTokenAsync_WithInvalidToken_ThrowsUnauthorizedException()
|
public async Task ValidateRefreshTokenAsync_WithExpiredToken_ThrowsUnauthorizedException()
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
const string token = "invalid-token";
|
|
||||||
|
|
||||||
_tokenInfraMock
|
|
||||||
.Setup(x => x.ValidateJwtAsync(token, It.IsAny<string>()))
|
|
||||||
.ThrowsAsync(new UnauthorizedException("Invalid token"));
|
|
||||||
|
|
||||||
// Act & Assert
|
|
||||||
await FluentActions
|
|
||||||
.Invoking(async () => await _tokenService.ValidateAccessTokenAsync(token))
|
|
||||||
.Should()
|
|
||||||
.ThrowAsync<UnauthorizedException>();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ValidateAccessTokenAsync_WithExpiredToken_ThrowsUnauthorizedException()
|
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
const string token = "expired-token";
|
const string token = "expired-token";
|
||||||
@@ -165,13 +115,13 @@ public class TokenServiceValidationTests
|
|||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
await FluentActions
|
await FluentActions
|
||||||
.Invoking(async () => await _tokenService.ValidateAccessTokenAsync(token))
|
.Invoking(async () => await _tokenService.ValidateRefreshTokenAsync(token))
|
||||||
.Should()
|
.Should()
|
||||||
.ThrowAsync<UnauthorizedException>();
|
.ThrowAsync<UnauthorizedException>();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidateAccessTokenAsync_WithMissingUserIdClaim_ThrowsUnauthorizedException()
|
public async Task ValidateRefreshTokenAsync_WithMissingUserIdClaim_ThrowsUnauthorizedException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
const string username = "testuser";
|
const string username = "testuser";
|
||||||
@@ -193,14 +143,14 @@ public class TokenServiceValidationTests
|
|||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
await FluentActions
|
await FluentActions
|
||||||
.Invoking(async () => await _tokenService.ValidateAccessTokenAsync(token))
|
.Invoking(async () => await _tokenService.ValidateRefreshTokenAsync(token))
|
||||||
.Should()
|
.Should()
|
||||||
.ThrowAsync<UnauthorizedException>()
|
.ThrowAsync<UnauthorizedException>()
|
||||||
.WithMessage("*missing required claims*");
|
.WithMessage("*missing required claims*");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidateAccessTokenAsync_WithMissingUsernameClaim_ThrowsUnauthorizedException()
|
public async Task ValidateRefreshTokenAsync_WithMissingUsernameClaim_ThrowsUnauthorizedException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
Guid userId = Guid.NewGuid();
|
Guid userId = Guid.NewGuid();
|
||||||
@@ -222,14 +172,14 @@ public class TokenServiceValidationTests
|
|||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
await FluentActions
|
await FluentActions
|
||||||
.Invoking(async () => await _tokenService.ValidateAccessTokenAsync(token))
|
.Invoking(async () => await _tokenService.ValidateRefreshTokenAsync(token))
|
||||||
.Should()
|
.Should()
|
||||||
.ThrowAsync<UnauthorizedException>()
|
.ThrowAsync<UnauthorizedException>()
|
||||||
.WithMessage("*missing required claims*");
|
.WithMessage("*missing required claims*");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidateAccessTokenAsync_WithMalformedUserId_ThrowsUnauthorizedException()
|
public async Task ValidateRefreshTokenAsync_WithMalformedUserId_ThrowsUnauthorizedException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
const string username = "testuser";
|
const string username = "testuser";
|
||||||
@@ -252,7 +202,7 @@ public class TokenServiceValidationTests
|
|||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
await FluentActions
|
await FluentActions
|
||||||
.Invoking(async () => await _tokenService.ValidateAccessTokenAsync(token))
|
.Invoking(async () => await _tokenService.ValidateRefreshTokenAsync(token))
|
||||||
.Should()
|
.Should()
|
||||||
.ThrowAsync<UnauthorizedException>()
|
.ThrowAsync<UnauthorizedException>()
|
||||||
.WithMessage("*malformed user ID*");
|
.WithMessage("*malformed user ID*");
|
||||||
|
|||||||
@@ -14,9 +14,10 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
||||||
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj" />
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.PasswordHashing\Infrastructure.PasswordHashing.csproj" />
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.PasswordHashing\Infrastructure.PasswordHashing.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Sql\Infrastructure.Sql.csproj" />
|
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
||||||
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
|
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
|
||||||
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
|
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
using Infrastructure.Sql;
|
using Database.Connection;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.Data.SqlClient;
|
using Microsoft.Data.SqlClient;
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ namespace Features.Auth.Identity;
|
|||||||
/// existing unique indexes are already case-insensitive under SQL Server's default collation).
|
/// existing unique indexes are already case-insensitive under SQL Server's default collation).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class DapperUserStore(ISqlConnectionFactory connectionFactory)
|
public sealed class DapperUserStore(ISqlConnectionFactory connectionFactory)
|
||||||
: Repository<ApplicationUser>(connectionFactory),
|
: DapperRepository(connectionFactory),
|
||||||
IUserStore<ApplicationUser>,
|
IUserStore<ApplicationUser>,
|
||||||
IUserPasswordStore<ApplicationUser>,
|
IUserPasswordStore<ApplicationUser>,
|
||||||
IUserEmailStore<ApplicationUser>
|
IUserEmailStore<ApplicationUser>
|
||||||
|
|||||||
@@ -66,12 +66,6 @@ public interface ITokenService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
string GenerateConfirmationToken(Guid userId, string username);
|
string GenerateConfirmationToken(Guid userId, string username);
|
||||||
|
|
||||||
/// <summary>Validates an access token's signature and expiration and extracts the caller's identity.</summary>
|
|
||||||
/// <exception cref="Domain.Exceptions.UnauthorizedException">
|
|
||||||
/// Thrown when the token is missing required claims, has a malformed user ID, or otherwise fails validation.
|
|
||||||
/// </exception>
|
|
||||||
Task<ValidatedToken> ValidateAccessTokenAsync(string token);
|
|
||||||
|
|
||||||
/// <summary>Validates a refresh token's signature and expiration and extracts the caller's identity.</summary>
|
/// <summary>Validates a refresh token's signature and expiration and extracts the caller's identity.</summary>
|
||||||
/// <exception cref="Domain.Exceptions.UnauthorizedException">
|
/// <exception cref="Domain.Exceptions.UnauthorizedException">
|
||||||
/// Thrown when the token is missing required claims, has a malformed user ID, or otherwise fails validation.
|
/// Thrown when the token is missing required claims, has a malformed user ID, or otherwise fails validation.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.IdentityModel.Tokens.Jwt;
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Domain.Exceptions;
|
using Domain.Exceptions;
|
||||||
using Features.Auth.Identity;
|
using Features.Auth.Identity;
|
||||||
|
using Infrastructure.Configuration;
|
||||||
using Infrastructure.Jwt;
|
using Infrastructure.Jwt;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
@@ -33,22 +34,19 @@ public class TokenService : ITokenService
|
|||||||
_tokenInfrastructure = tokenInfrastructure;
|
_tokenInfrastructure = tokenInfrastructure;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
|
|
||||||
_accessTokenSecret =
|
_accessTokenSecret = ConfigurationHelpers.GetKeyOrThrow(
|
||||||
configuration["ACCESS_TOKEN_SECRET"]
|
configuration,
|
||||||
?? throw new InvalidOperationException(
|
ConfigurationKeys.AccessTokenSecret
|
||||||
"ACCESS_TOKEN_SECRET environment variable is not set"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
_refreshTokenSecret =
|
_refreshTokenSecret = ConfigurationHelpers.GetKeyOrThrow(
|
||||||
configuration["REFRESH_TOKEN_SECRET"]
|
configuration,
|
||||||
?? throw new InvalidOperationException(
|
ConfigurationKeys.RefreshTokenSecret
|
||||||
"REFRESH_TOKEN_SECRET environment variable is not set"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
_confirmationTokenSecret =
|
_confirmationTokenSecret = ConfigurationHelpers.GetKeyOrThrow(
|
||||||
configuration["CONFIRMATION_TOKEN_SECRET"]
|
configuration,
|
||||||
?? throw new InvalidOperationException(
|
ConfigurationKeys.ConfirmationTokenSecret
|
||||||
"CONFIRMATION_TOKEN_SECRET environment variable is not set"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,12 +80,6 @@ public class TokenService : ITokenService
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<ValidatedToken> ValidateAccessTokenAsync(string token)
|
|
||||||
{
|
|
||||||
return await ValidateTokenInternalAsync(token, _accessTokenSecret, "access");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<ValidatedToken> ValidateRefreshTokenAsync(string token)
|
public async Task<ValidatedToken> ValidateRefreshTokenAsync(string token)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
||||||
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Sql\Infrastructure.Sql.csproj" />
|
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
||||||
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
|
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
|
||||||
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
|
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
||||||
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Sql\Infrastructure.Sql.csproj" />
|
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
||||||
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
|
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
|
||||||
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
|
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
|
using Database.Connection;
|
||||||
using Domain.Entities;
|
using Domain.Entities;
|
||||||
using Domain.Exceptions;
|
using Domain.Exceptions;
|
||||||
using Infrastructure.Sql;
|
|
||||||
|
|
||||||
namespace Features.Breweries.Repository;
|
namespace Features.Breweries.Repository;
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ namespace Features.Breweries.Repository;
|
|||||||
/// yields the UDT's serialized bytes as a plain <c>varbinary</c> value that Dapper can bind directly.
|
/// yields the UDT's serialized bytes as a plain <c>varbinary</c> value that Dapper can bind directly.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||||
: Repository<BreweryPost>(connectionFactory),
|
: Database.Connection.DapperRepository(connectionFactory),
|
||||||
IBreweryRepository
|
IBreweryRepository
|
||||||
{
|
{
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email\Infrastructure.Email.csproj" />
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email\Infrastructure.Email.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email.Templates\Infrastructure.Email.Templates.csproj" />
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email.Templates\Infrastructure.Email.Templates.csproj" />
|
||||||
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
|
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Infrastructure.Configuration;
|
||||||
using Infrastructure.Email;
|
using Infrastructure.Email;
|
||||||
using Infrastructure.Email.Templates.Rendering;
|
using Infrastructure.Email.Templates.Rendering;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
@@ -21,8 +22,7 @@ public class EmailDispatcher(
|
|||||||
/// Thrown when the <c>WEBSITE_BASE_URL</c> configuration value is not set.
|
/// Thrown when the <c>WEBSITE_BASE_URL</c> configuration value is not set.
|
||||||
/// </exception>
|
/// </exception>
|
||||||
private string WebsiteBaseUrl =>
|
private string WebsiteBaseUrl =>
|
||||||
configuration["WEBSITE_BASE_URL"]
|
ConfigurationHelpers.GetKeyOrThrow(configuration, ConfigurationKeys.WebsiteBaseUrl);
|
||||||
?? throw new InvalidOperationException("WEBSITE_BASE_URL environment variable is not set");
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task SendRegistrationEmailAsync(
|
public async Task SendRegistrationEmailAsync(
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
||||||
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Sql\Infrastructure.Sql.csproj" />
|
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
||||||
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
|
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
|
||||||
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
|
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
using Domain.Entities;
|
using Database.Connection;
|
||||||
using Domain.Exceptions;
|
using Domain.Exceptions;
|
||||||
using Features.Locations.Dtos;
|
using Features.Locations.Dtos;
|
||||||
using Infrastructure.Sql;
|
|
||||||
|
|
||||||
namespace Features.Locations.Repository;
|
namespace Features.Locations.Repository;
|
||||||
|
|
||||||
@@ -11,7 +10,7 @@ namespace Features.Locations.Repository;
|
|||||||
/// Dapper-based implementation of <see cref="ILocationRepository" />.
|
/// Dapper-based implementation of <see cref="ILocationRepository" />.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LocationRepository(ISqlConnectionFactory connectionFactory)
|
public class LocationRepository(ISqlConnectionFactory connectionFactory)
|
||||||
: Repository<City>(connectionFactory),
|
: Database.Connection.DapperRepository(connectionFactory),
|
||||||
ILocationRepository
|
ILocationRepository
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
||||||
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
|
||||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Sql\Infrastructure.Sql.csproj" />
|
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
||||||
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
|
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
|
||||||
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
|
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
|
using Database.Connection;
|
||||||
using Domain.Entities;
|
using Domain.Entities;
|
||||||
using Domain.Exceptions;
|
using Domain.Exceptions;
|
||||||
using Infrastructure.Sql;
|
|
||||||
|
|
||||||
namespace Features.UserManagement.Repository;
|
namespace Features.UserManagement.Repository;
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ namespace Features.UserManagement.Repository;
|
|||||||
/// Dapper-based implementation of <see cref="IUserAccountRepository" />.
|
/// Dapper-based implementation of <see cref="IUserAccountRepository" />.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class UserAccountRepository(ISqlConnectionFactory connectionFactory)
|
public class UserAccountRepository(ISqlConnectionFactory connectionFactory)
|
||||||
: Repository<UserAccount>(connectionFactory),
|
: Database.Connection.DapperRepository(connectionFactory),
|
||||||
IUserAccountRepository
|
IUserAccountRepository
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace Infrastructure.Configuration;
|
||||||
|
|
||||||
|
public static class ConfigurationHelpers
|
||||||
|
{
|
||||||
|
public static string GetKeyOrThrow(IConfiguration configuration, string key) =>
|
||||||
|
configuration[key]
|
||||||
|
?? throw new InvalidOperationException($"The {key} environment variable is not set.");
|
||||||
|
|
||||||
|
public static bool GetBooleanKeyOrThrow(IConfiguration configuration, string key)
|
||||||
|
{
|
||||||
|
string value = GetKeyOrThrow(configuration, key);
|
||||||
|
|
||||||
|
return bool.TryParse(value, out bool result)
|
||||||
|
? result
|
||||||
|
: throw new InvalidOperationException(
|
||||||
|
$"The {key} environment variable is set to '{value}', which is not a valid boolean."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int GetIntKeyOrThrow(IConfiguration configuration, string key)
|
||||||
|
{
|
||||||
|
string value = GetKeyOrThrow(configuration, key);
|
||||||
|
|
||||||
|
return int.TryParse(value, out int result)
|
||||||
|
? result
|
||||||
|
: throw new InvalidOperationException(
|
||||||
|
$"The {key} environment variable is set to '{value}', which is not a valid integer."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
namespace Infrastructure.Configuration;
|
||||||
|
|
||||||
|
public static class ConfigurationKeys
|
||||||
|
{
|
||||||
|
public const string AccessTokenSecret = "ACCESS_TOKEN_SECRET";
|
||||||
|
public const string RefreshTokenSecret = "REFRESH_TOKEN_SECRET";
|
||||||
|
public const string ConfirmationTokenSecret = "CONFIRMATION_TOKEN_SECRET";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fallback configuration section path for the access-token signing key, checked when
|
||||||
|
/// <see cref="AccessTokenSecret" /> is not set.
|
||||||
|
/// </summary>
|
||||||
|
public const string JwtSecretKeyFallback = "Jwt:SecretKey";
|
||||||
|
|
||||||
|
public const string WebsiteBaseUrl = "WEBSITE_BASE_URL";
|
||||||
|
|
||||||
|
public const string SmtpHost = "SMTP_HOST";
|
||||||
|
public const string SmtpPort = "SMTP_PORT";
|
||||||
|
public const string SmtpUsername = "SMTP_USERNAME";
|
||||||
|
public const string SmtpPassword = "SMTP_PASSWORD";
|
||||||
|
public const string SmtpUseSsl = "SMTP_USE_SSL";
|
||||||
|
public const string SmtpFromEmail = "SMTP_FROM_EMAIL";
|
||||||
|
public const string SmtpFromName = "SMTP_FROM_NAME";
|
||||||
|
|
||||||
|
public const string DbServer = "DB_SERVER";
|
||||||
|
public const string DbUser = "DB_USER";
|
||||||
|
public const string DbPassword = "DB_PASSWORD";
|
||||||
|
public const string DbName = "DB_NAME";
|
||||||
|
public const string DbConnectionString = "DB_CONNECTION_STRING";
|
||||||
|
public const string DbTrustServerCertificate = "DB_TRUST_SERVER_CERTIFICATE";
|
||||||
|
|
||||||
|
public const string ClearDatabase = "CLEAR_DATABASE";
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>Infrastructure.Configuration</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference
|
||||||
|
Include="Microsoft.Extensions.Configuration.Abstractions"
|
||||||
|
Version="10.0.1"
|
||||||
|
/>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -13,4 +13,8 @@
|
|||||||
Version="10.0.1"
|
Version="10.0.1"
|
||||||
/>
|
/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using MailKit.Net.Smtp;
|
using Infrastructure.Configuration;
|
||||||
|
using MailKit.Net.Smtp;
|
||||||
using MailKit.Security;
|
using MailKit.Security;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using MimeKit;
|
using MimeKit;
|
||||||
@@ -25,31 +26,27 @@ public class SmtpEmailProvider : IEmailProvider
|
|||||||
/// <c>SMTP_FROM_EMAIL</c>, <c>SMTP_FROM_NAME</c>) from <paramref name="configuration" />.
|
/// <c>SMTP_FROM_EMAIL</c>, <c>SMTP_FROM_NAME</c>) from <paramref name="configuration" />.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <exception cref="InvalidOperationException">
|
/// <exception cref="InvalidOperationException">
|
||||||
/// Thrown when <c>SMTP_HOST</c> or <c>SMTP_FROM_EMAIL</c> is not set, or when <c>SMTP_PORT</c> is not a valid integer.
|
/// Thrown when <c>SMTP_HOST</c>, <c>SMTP_PORT</c>, or <c>SMTP_FROM_EMAIL</c> is not set,
|
||||||
|
/// or when <c>SMTP_PORT</c> is not a valid integer.
|
||||||
/// </exception>
|
/// </exception>
|
||||||
public SmtpEmailProvider(IConfiguration configuration)
|
public SmtpEmailProvider(IConfiguration configuration)
|
||||||
{
|
{
|
||||||
_host =
|
_host = ConfigurationHelpers.GetKeyOrThrow(configuration, ConfigurationKeys.SmtpHost);
|
||||||
configuration["SMTP_HOST"]
|
|
||||||
?? throw new InvalidOperationException("SMTP_HOST environment variable is not set");
|
|
||||||
|
|
||||||
string portString = configuration["SMTP_PORT"] ?? "587";
|
_port = ConfigurationHelpers.GetIntKeyOrThrow(configuration, ConfigurationKeys.SmtpPort);
|
||||||
if (!int.TryParse(portString, out _port))
|
|
||||||
throw new InvalidOperationException($"SMTP_PORT '{portString}' is not a valid integer");
|
|
||||||
|
|
||||||
_username = configuration["SMTP_USERNAME"];
|
_username = configuration[ConfigurationKeys.SmtpUsername];
|
||||||
_password = configuration["SMTP_PASSWORD"];
|
_password = configuration[ConfigurationKeys.SmtpPassword];
|
||||||
|
|
||||||
string useSslString = configuration["SMTP_USE_SSL"] ?? "true";
|
string useSslString = configuration[ConfigurationKeys.SmtpUseSsl] ?? "true";
|
||||||
_useSsl = bool.Parse(useSslString);
|
_useSsl = bool.Parse(useSslString);
|
||||||
|
|
||||||
_fromEmail =
|
_fromEmail = ConfigurationHelpers.GetKeyOrThrow(
|
||||||
configuration["SMTP_FROM_EMAIL"]
|
configuration,
|
||||||
?? throw new InvalidOperationException(
|
ConfigurationKeys.SmtpFromEmail
|
||||||
"SMTP_FROM_EMAIL environment variable is not set"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
_fromName = configuration["SMTP_FROM_NAME"] ?? "The Biergarten";
|
_fromName = configuration[ConfigurationKeys.SmtpFromName] ?? "The Biergarten";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
using System.Data.Common;
|
|
||||||
using Microsoft.Data.SqlClient;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
|
|
||||||
namespace Infrastructure.Sql;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Default <see cref="ISqlConnectionFactory" /> implementation that creates SQL Server connections,
|
|
||||||
/// resolving the connection string from environment variables or application configuration.
|
|
||||||
/// </summary>
|
|
||||||
public class DefaultSqlConnectionFactory(IConfiguration configuration) : ISqlConnectionFactory
|
|
||||||
{
|
|
||||||
private readonly string _connectionString = GetConnectionString(configuration);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public DbConnection CreateConnection()
|
|
||||||
{
|
|
||||||
return new SqlConnection(_connectionString);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Resolves the SQL Server connection string, preferring (in order): the <c>DB_CONNECTION_STRING</c>
|
|
||||||
/// configuration value, a connection string built from individual <c>DB_*</c> configuration values,
|
|
||||||
/// and finally the <c>"Default"</c> connection string from configuration.
|
|
||||||
/// </summary>
|
|
||||||
/// <exception cref="InvalidOperationException">Thrown when no connection string can be resolved from any source.</exception>
|
|
||||||
private static string GetConnectionString(IConfiguration configuration)
|
|
||||||
{
|
|
||||||
string? fullConnectionString = configuration["DB_CONNECTION_STRING"];
|
|
||||||
if (!string.IsNullOrEmpty(fullConnectionString))
|
|
||||||
return fullConnectionString;
|
|
||||||
|
|
||||||
// Try to build from individual configuration values (preferred method for Docker)
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return SqlConnectionStringHelper.BuildConnectionString(configuration);
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException)
|
|
||||||
{
|
|
||||||
// Fall back to configuration-based connection string if env vars are not set
|
|
||||||
string? 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."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using System.Data.Common;
|
|
||||||
|
|
||||||
namespace Infrastructure.Sql;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Factory for creating database connections used by repositories.
|
|
||||||
/// </summary>
|
|
||||||
public interface ISqlConnectionFactory
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new, unopened database connection.
|
|
||||||
/// </summary>
|
|
||||||
DbConnection CreateConnection();
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
using Microsoft.Data.SqlClient;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
|
|
||||||
namespace Infrastructure.Sql;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Helper for building SQL Server connection strings from configuration
|
|
||||||
/// (backed by the <c>DB_*</c> environment variables).
|
|
||||||
/// </summary>
|
|
||||||
public static class SqlConnectionStringHelper
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Builds a SQL Server connection string from configuration.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>Throws <see cref="InvalidOperationException" /> if DB_SERVER, DB_NAME, DB_USER, or DB_PASSWORD is unset.</remarks>
|
|
||||||
/// <param name="configuration">The configuration to read the <c>DB_*</c> values from.</param>
|
|
||||||
/// <param name="databaseName">If null, falls back to the DB_NAME configuration value.</param>
|
|
||||||
public static string BuildConnectionString(
|
|
||||||
IConfiguration configuration,
|
|
||||||
string? databaseName = null
|
|
||||||
)
|
|
||||||
{
|
|
||||||
string server = Required(configuration, "DB_SERVER");
|
|
||||||
string dbName = databaseName ?? Required(configuration, "DB_NAME");
|
|
||||||
string user = Required(configuration, "DB_USER");
|
|
||||||
string password = Required(configuration, "DB_PASSWORD");
|
|
||||||
|
|
||||||
SqlConnectionStringBuilder builder = new()
|
|
||||||
{
|
|
||||||
DataSource = server,
|
|
||||||
InitialCatalog = dbName,
|
|
||||||
UserID = user,
|
|
||||||
Password = password,
|
|
||||||
TrustServerCertificate = ResolveTrustServerCertificate(configuration),
|
|
||||||
Encrypt = true,
|
|
||||||
};
|
|
||||||
|
|
||||||
return builder.ConnectionString;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string BuildMasterConnectionString(IConfiguration configuration)
|
|
||||||
{
|
|
||||||
return BuildConnectionString(configuration, "master");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Resolves the <c>DB_TRUST_SERVER_CERTIFICATE</c> configuration value, defaulting to
|
|
||||||
/// <c>true</c> when it is unset or not a valid boolean.
|
|
||||||
/// </summary>
|
|
||||||
private static bool ResolveTrustServerCertificate(IConfiguration configuration)
|
|
||||||
{
|
|
||||||
return !bool.TryParse(
|
|
||||||
configuration["DB_TRUST_SERVER_CERTIFICATE"],
|
|
||||||
out bool trustServerCertificate
|
|
||||||
) || trustServerCertificate;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Required(IConfiguration configuration, string key) =>
|
|
||||||
configuration[key]
|
|
||||||
?? throw new InvalidOperationException($"The {key} environment variable is not set.");
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
namespace Infrastructure.Sql;
|
|
||||||
Reference in New Issue
Block a user