Merge branch 'main-2.0' of https://github.com/aaronpo97/the-biergarten-app into main-2.0

This commit is contained in:
Aaron Po
2026-02-08 00:07:43 -05:00
13 changed files with 368 additions and 73 deletions

70
docker-compose.db.yaml Normal file
View File

@@ -0,0 +1,70 @@
services:
sqlserver:
image: mcr.microsoft.com/mssql/server:2022-latest
platform: linux/amd64
container_name: dev-env-sqlserver
environment:
ACCEPT_EULA: "Y"
SA_PASSWORD: "${SA_PASSWORD}"
MSSQL_PID: "Express"
ports:
- "1433:1433"
volumes:
- sqlserverdata-dev:/var/opt/mssql
healthcheck:
test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${SA_PASSWORD}' -C -Q 'SELECT 1' || exit 1"]
interval: 10s
timeout: 5s
retries: 12
start_period: 30s
networks:
- devnet
database.migrations:
image: database.migrations
container_name: dev-env-database-migrations
depends_on:
sqlserver:
condition: service_healthy
build:
context: ./src/Core/Database
dockerfile: Database.Migrations/Dockerfile
args:
BUILD_CONFIGURATION: Release
APP_UID: 1000
environment:
DOTNET_RUNNING_IN_CONTAINER: "true"
DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}"
MASTER_DB_CONNECTION_STRING: "${MASTER_DB_CONNECTION_STRING}"
CLEAR_DATABASE: "true"
restart: "no"
networks:
- devnet
database.seed:
image: database.seed
container_name: dev-env-database-seed
depends_on:
database.migrations:
condition: service_completed_successfully
build:
context: ./src/Core
dockerfile: Database/Database.Seed/Dockerfile
args:
BUILD_CONFIGURATION: Release
APP_UID: 1000
environment:
DOTNET_RUNNING_IN_CONTAINER: "true"
DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}"
restart: "no"
networks:
- devnet
volumes:
sqlserverdata-dev:
driver: local
nuget-cache-dev:
driver: local
networks:
devnet:
driver: bridge

View File

@@ -35,15 +35,36 @@ services:
DOTNET_RUNNING_IN_CONTAINER: "true"
DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}"
MASTER_DB_CONNECTION_STRING: "${MASTER_DB_CONNECTION_STRING}"
CLEAR_DATABASE: "true"
restart: "no"
networks:
- devnet
database.seed:
image: database.seed
container_name: dev-env-database-seed
depends_on:
database.migrations:
condition: service_completed_successfully
build:
context: ./src/Core
dockerfile: Database/Database.Seed/Dockerfile
args:
BUILD_CONFIGURATION: Release
APP_UID: 1000
environment:
DOTNET_RUNNING_IN_CONTAINER: "true"
DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}"
restart: "no"
networks:
- devnet
api.core:
image: api.core
container_name: dev-env-api-core
depends_on:
sqlserver:
condition: service_healthy
database.seed:
condition: service_completed_successfully
build:
context: ./src/Core
dockerfile: API/API.Core/Dockerfile
@@ -58,6 +79,7 @@ services:
ASPNETCORE_URLS: "http://0.0.0.0:8080"
DOTNET_RUNNING_IN_CONTAINER: "true"
DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}"
JWT_SECRET: "${JWT_SECRET}"
restart: unless-stopped
networks:
- devnet

View File

@@ -59,6 +59,7 @@ services:
DOTNET_RUNNING_IN_CONTAINER: "true"
MASTER_DB_CONNECTION_STRING: "${MASTER_DB_CONNECTION_STRING}"
DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}"
JWT_SECRET: "${JWT_SECRET}"
restart: unless-stopped
networks:
- prodnet

110
docker-compose.test.yaml Normal file
View File

@@ -0,0 +1,110 @@
services:
sqlserver:
image: mcr.microsoft.com/mssql/server:2022-latest
platform: linux/amd64
container_name: test-env-sqlserver
environment:
ACCEPT_EULA: "Y"
SA_PASSWORD: "${SA_PASSWORD}"
MSSQL_PID: "Express"
DOTNET_RUNNING_IN_CONTAINER: "true"
volumes:
- sqlserverdata-test:/var/opt/mssql
healthcheck:
test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${SA_PASSWORD}' -C -Q 'SELECT 1' || exit 1"]
interval: 10s
timeout: 5s
retries: 12
start_period: 30s
networks:
- testnet
database.migrations:
image: database.migrations
container_name: test-env-database-migrations
depends_on:
sqlserver:
condition: service_healthy
build:
context: ./src/Core/Database
dockerfile: Database.Migrations/Dockerfile
args:
BUILD_CONFIGURATION: Release
APP_UID: 1000
environment:
DOTNET_RUNNING_IN_CONTAINER: "true"
DB_CONNECTION_STRING: "${TEST_DB_CONNECTION_STRING}"
MASTER_DB_CONNECTION_STRING: "${TEST_MASTER_DB_CONNECTION_STRING}"
CLEAR_DATABASE: "true"
restart: "no"
networks:
- testnet
database.seed:
image: database.seed
container_name: test-env-database-seed
depends_on:
database.migrations:
condition: service_completed_successfully
build:
context: ./src/Core
dockerfile: Database/Database.Seed/Dockerfile
args:
BUILD_CONFIGURATION: Release
APP_UID: 1000
environment:
DOTNET_RUNNING_IN_CONTAINER: "true"
DB_CONNECTION_STRING: "${TEST_DB_CONNECTION_STRING}"
restart: "no"
networks:
- testnet
api.specs:
image: api.specs
container_name: test-env-api-specs
depends_on:
database.seed:
condition: service_completed_successfully
build:
context: ./src/Core
dockerfile: API/API.Specs/Dockerfile
args:
BUILD_CONFIGURATION: Release
environment:
DOTNET_RUNNING_IN_CONTAINER: "true"
DB_CONNECTION_STRING: "${TEST_DB_CONNECTION_STRING}"
JWT_SECRET: "${JWT_SECRET}"
volumes:
- ./test-results:/app/test-results
restart: "no"
networks:
- testnet
repository.tests:
image: repository.tests
container_name: test-env-repository-tests
depends_on:
database.seed:
condition: service_completed_successfully
build:
context: ./src/Core
dockerfile: Repository/Repository.Tests/Dockerfile
args:
BUILD_CONFIGURATION: Release
environment:
DOTNET_RUNNING_IN_CONTAINER: "true"
DB_CONNECTION_STRING: "${TEST_DB_CONNECTION_STRING}"
JWT_SECRET: "${JWT_SECRET}"
volumes:
- ./test-results:/app/test-results
restart: "no"
networks:
- testnet
volumes:
sqlserverdata-test:
driver: local
networks:
testnet:
driver: bridge

View File

@@ -34,6 +34,5 @@
<ItemGroup>
<ProjectReference Include="..\API.Core\API.Core.csproj" />
<ProjectReference Include="..\..\Database\Database.Core\Database.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,19 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["API/API.Core/API.Core.csproj", "API/API.Core/"]
COPY ["API/API.Specs/API.Specs.csproj", "API/API.Specs/"]
COPY ["Repository/Repository.Core/Repository.Core.csproj", "Repository/Repository.Core/"]
COPY ["Service/Service.Core/Service.Core.csproj", "Service/Service.Core/"]
RUN dotnet restore "API/API.Specs/API.Specs.csproj"
COPY . .
WORKDIR "/src/API/API.Specs"
RUN dotnet build "./API.Specs.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS final
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /app/test-results
WORKDIR /src/API/API.Specs
ENTRYPOINT ["dotnet", "test", "API.Specs.csproj", "-c", "Release", "--no-build", "--no-restore", "--logger", "trx;LogFileName=/app/test-results/test-results.trx"]

View File

@@ -4,7 +4,7 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>DataLayer</RootNamespace>
<RootNamespace>Database.Migrations</RootNamespace>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>

View File

@@ -3,7 +3,7 @@ using System.Reflection;
using DbUp;
using Microsoft.Data.SqlClient;
namespace DataLayer;
namespace Database.Migrations;
public static class Program
{
@@ -22,6 +22,57 @@ public static class Program
return result.Successful;
}
private static bool ClearDatabase()
{
var myConn = new SqlConnection(masterConnectionString);
try
{
myConn.Open();
// First, set the database to single user mode to close all connections
var setModeCommand = new SqlCommand(
"IF EXISTS (SELECT 1 FROM sys.databases WHERE name = 'Biergarten') " +
"ALTER DATABASE [Biergarten] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;",
myConn);
try
{
setModeCommand.ExecuteNonQuery();
Console.WriteLine("Database set to single user mode.");
}
catch (System.Exception ex)
{
Console.WriteLine($"Warning: Could not set single user mode: {ex.Message}");
}
// Then drop the database
var dropCommand = new SqlCommand("DROP DATABASE IF EXISTS [Biergarten];", myConn);
try
{
dropCommand.ExecuteNonQuery();
Console.WriteLine("Database cleared successfully.");
}
catch (System.Exception ex)
{
Console.WriteLine($"Error dropping database: {ex}");
return false;
}
}
catch (System.Exception ex)
{
Console.WriteLine($"Error clearing database: {ex}");
return false;
}
finally
{
if (myConn.State == ConnectionState.Open)
{
myConn.Close();
}
}
return true;
}
private static bool CreateDatabaseIfNotExists()
{
var myConn = new SqlConnection(masterConnectionString);
@@ -58,6 +109,13 @@ public static class Program
try
{
var clearDatabase = Environment.GetEnvironmentVariable("CLEAR_DATABASE");
if (clearDatabase == "true")
{
Console.WriteLine("CLEAR_DATABASE is enabled. Clearing existing database...");
ClearDatabase();
}
CreateDatabaseIfNotExists();
var success = DeployMigrations();

View File

@@ -4,7 +4,7 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>DBSeed</RootNamespace>
<RootNamespace>Database.Seed</RootNamespace>
</PropertyGroup>
<ItemGroup>
@@ -18,7 +18,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Database.Migrations\Database.Migrations.csproj" />
<ProjectReference Include="..\..\Repository\Repository.Core\Repository.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,16 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
# Copy everything from the context (src/Core)
COPY . .
RUN dotnet restore "./Database/Database.Seed/Database.Seed.csproj"
RUN dotnet build "./Database/Database.Seed/Database.Seed.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./Database/Database.Seed/Database.Seed.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM mcr.microsoft.com/dotnet/runtime:10.0 AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Database.Seed.dll"]

View File

@@ -5,73 +5,60 @@ using System.Reflection;
try
{
var connectionString = Environment.GetEnvironmentVariable(
"DB_CONNECTION_STRING"
);
if (string.IsNullOrWhiteSpace(connectionString))
throw new InvalidOperationException(
"Environment variable DB_CONNECTION_STRING is not set or is empty."
);
var connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING");
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
Console.WriteLine("Attempting to connect to database...");
// drop and recreate the database
var useMaster = connection.CreateCommand();
useMaster.CommandText = "USE master;";
await useMaster.ExecuteNonQueryAsync();
// Retry logic for database connection
SqlConnection? connection = null;
int maxRetries = 10;
int retryDelayMs = 2000;
var dbName = "Biergarten";
var dropDb = connection.CreateCommand();
dropDb.CommandText = $@"
IF DB_ID(N'{dbName}') IS NOT NULL
BEGIN
ALTER DATABASE [{dbName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
DROP DATABASE [{dbName}];
END";
await dropDb.ExecuteNonQueryAsync();
var createDb = connection.CreateCommand();
createDb.CommandText = $@"CREATE DATABASE [{dbName}];";
await createDb.ExecuteNonQueryAsync();
await connection.CloseAsync();
await connection.OpenAsync();
Console.WriteLine("Connected to database.");
Console.WriteLine("Starting migrations...");
// Run Database.Core migrations (embedded resources) via DbUp
var migrationAssembly = Assembly.Load("Database.Core");
var upgrader = DeployChanges
.To.SqlDatabase(connectionString)
.WithScriptsEmbeddedInAssembly(migrationAssembly)
.LogToConsole()
.Build();
var upgradeResult = upgrader.PerformUpgrade();
if (!upgradeResult.Successful)
throw upgradeResult.Error;
Console.WriteLine("Migrations completed.");
ISeeder[] seeders =
[
new LocationSeeder(),
new UserSeeder(),
];
foreach (var seeder in seeders)
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
Console.WriteLine($"Seeding {seeder.GetType().Name}...");
await seeder.SeedAsync(connection);
Console.WriteLine($"{seeder.GetType().Name} seeded.");
try
{
connection = new SqlConnection(connectionString);
await connection.OpenAsync();
Console.WriteLine($"Connected to database successfully on attempt {attempt}.");
break;
}
catch (SqlException ex) when (attempt < maxRetries)
{
Console.WriteLine($"Connection attempt {attempt}/{maxRetries} failed: {ex.Message}");
Console.WriteLine($"Retrying in {retryDelayMs}ms...");
await Task.Delay(retryDelayMs);
connection?.Dispose();
connection = null;
}
}
if (connection == null)
{
throw new Exception($"Failed to connect to database after {maxRetries} attempts.");
}
Console.WriteLine("Starting seeding...");
using (connection)
{
ISeeder[] seeders =
[
new LocationSeeder(),
new UserSeeder(),
];
foreach (var seeder in seeders)
{
Console.WriteLine($"Seeding {seeder.GetType().Name}...");
await seeder.SeedAsync(connection);
Console.WriteLine($"{seeder.GetType().Name} seeded.");
}
Console.WriteLine("Seed completed successfully.");
}
Console.WriteLine("Seed completed successfully.");
await connection.CloseAsync();
return 0;
}
catch (Exception ex)

View File

@@ -0,0 +1,14 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["Repository/Repository.Core/Repository.Core.csproj", "Repository/Repository.Core/"]
COPY ["Repository/Repository.Tests/Repository.Tests.csproj", "Repository/Repository.Tests/"]
RUN dotnet restore "Repository/Repository.Tests/Repository.Tests.csproj"
COPY . .
WORKDIR "/src/Repository/Repository.Tests"
RUN dotnet build "./Repository.Tests.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS final
RUN mkdir -p /app/test-results
WORKDIR /src/Repository/Repository.Tests
ENTRYPOINT ["dotnet", "test", "./Repository.Tests.csproj", "-c", "Release", "--no-build", "--logger", "trx;LogFileName=/app/test-results/repository-tests.trx"]

View File

@@ -1,3 +1,4 @@
using System;
using System.Security.Claims;
using System.Text;
using Microsoft.Extensions.Configuration;
@@ -6,10 +7,9 @@ using Microsoft.IdentityModel.Tokens;
using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames;
namespace ServiceCore.Services;
public class JwtService(IConfiguration config) : IJwtService
public class JwtService : IJwtService
{
// private readonly string? _secret = config["Jwt:Secret"];
private readonly string? _secret = "128490218jfklsdajfdsa90f8sd0fid0safasr31jl2k1j4AFSDR!@#$fdsafjdslajfl";
private readonly string? _secret = Environment.GetEnvironmentVariable("JWT_SECRET");
public string GenerateJwt(Guid userId, string username, DateTime expiry)
{
var handler = new JsonWebTokenHandler();