diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8be2885 --- /dev/null +++ b/.env.example @@ -0,0 +1,38 @@ +# ============================================== +# Biergarten App - Environment Variables Template +# ============================================== +# +# This file contains backend/Docker environment variables. +# Copy this to create environment-specific files: +# - .env.dev (development) +# - .env.test (testing) +# - .env.prod (production) +# +# For frontend variables, create a separate .env.local file +# in the Website/ directory. See README.md for complete docs. +# +# ============================================== + +# ====================== +# Database Configuration +# ====================== + +# SQL Server Connection Components (Recommended for Docker) +# These are used to build connection strings dynamically +DB_SERVER=sqlserver,1433 +DB_NAME=Biergarten +DB_USER=sa +DB_PASSWORD=YourStrong!Passw0rd + +# Alternative: Full Connection String (Local Development) +# If set, this overrides the component-based configuration above +# DB_CONNECTION_STRING=Server=localhost,1433;Database=Biergarten;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True; + +# ====================== +# JWT Configuration +# ====================== + +# JWT Secret for signing tokens +# IMPORTANT: Generate a secure secret (minimum 32 characters) +# Command: openssl rand -base64 32 +JWT_SECRET=128490218jfklsdajfdsa90f8sd0fid0safasr31jl2k1j4AFSDR diff --git a/.gitignore b/.gitignore index b06b40a..ecede2b 100644 --- a/.gitignore +++ b/.gitignore @@ -486,4 +486,7 @@ FodyWeavers.xsd database -.env.* \ No newline at end of file +.env +.env.dev +.env.test +.env.prod diff --git a/README.md b/README.md index 6faa879..917e8de 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,15 @@ A social platform for craft beer enthusiasts to discover breweries, share review - [Prerequisites](#prerequisites) - [Quick Start (Development Environment)](#quick-start-development-environment) - [Manual Setup (Without Docker)](#manual-setup-without-docker) +- [Environment Variables](#environment-variables) + - [Overview](#overview) + - [Backend Variables (.NET API)](#backend-variables-net-api) + - [Frontend Variables (Next.js)](#frontend-variables-nextjs) + - [Docker Variables](#docker-variables) + - [External Services](#external-services) + - [Generating Secrets](#generating-secrets) + - [Environment File Structure](#environment-file-structure) + - [Variable Reference Table](#variable-reference-table) - [Testing](#testing) - [Database Schema](#database-schema) - [Authentication & Security](#authentication--security) @@ -154,17 +163,25 @@ Website/ # Next.js frontend application 2. **Configure environment variables** - Create a `.env` file in the project root: + Copy the example file and customize: ```bash - # Database - SA_PASSWORD=YourStrong!Passw0rd - DB_CONNECTION_STRING=Server=localhost,1433;Database=Biergarten;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True; - MASTER_DB_CONNECTION_STRING=Server=localhost,1433;Database=master;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True; + cp .env.example .env.dev + ``` + + Required variables in `.env.dev`: + ```bash + # Database (component-based for Docker) + DB_SERVER=sqlserver,1433 + DB_NAME=Biergarten + DB_USER=sa + DB_PASSWORD=YourStrong!Passw0rd # JWT Authentication - JWT_SECRET=your-secret-key-here-min-32-chars + JWT_SECRET=your-secret-key-minimum-32-characters-required ``` + For a complete list of all backend and frontend environment variables, see the [Environment Variables](#environment-variables) section. + 3. **Start the development environment** ```bash docker compose -f docker-compose.dev.yaml up -d @@ -181,27 +198,39 @@ Website/ # Next.js frontend application Navigate to http://localhost:8080/swagger to explore and test API endpoints. 5. **Run the frontend** (optional) + + The frontend requires additional environment variables. See [Frontend Variables](#frontend-variables-nextjs) section. + ```bash cd Website + + # Create .env.local with frontend variables + # (see Environment Variables section) + npm install npm run dev ``` - For Website environment variables, see `Website/README.old.md`. + For complete environment variable documentation, see the [Environment Variables](#environment-variables) section below. ### Manual Setup (Without Docker) +#### Backend Setup + 1. **Start SQL Server locally** or use a hosted instance -2. **Set environment variable** +2. **Set environment variables** + + See [Backend Variables](#backend-variables-net-api) for details. + ```bash # macOS/Linux export DB_CONNECTION_STRING="Server=localhost,1433;Database=Biergarten;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True;" - export JWT_SECRET="your-secret-key-here-min-32-chars" + export JWT_SECRET="your-secret-key-minimum-32-characters-required" # Windows PowerShell $env:DB_CONNECTION_STRING="Server=localhost,1433;Database=Biergarten;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True;" - $env:JWT_SECRET="your-secret-key-here-min-32-chars" + $env:JWT_SECRET="your-secret-key-minimum-32-characters-required" ``` 3. **Run migrations** @@ -220,6 +249,324 @@ Website/ # Next.js frontend application dotnet run --project API/API.Core/API.Core.csproj ``` +#### Frontend Setup + +1. **Navigate to Website directory** + ```bash + cd Website + ``` + +2. **Create environment file** + + Create `.env.local` with required frontend variables. See [Frontend Variables](#frontend-variables-nextjs) for the complete list. + + ```bash + # Example minimal setup + BASE_URL=http://localhost:3000 + NODE_ENV=development + + # Generate secrets + CONFIRMATION_TOKEN_SECRET=$(openssl rand -base64 127) + RESET_PASSWORD_TOKEN_SECRET=$(openssl rand -base64 127) + SESSION_SECRET=$(openssl rand -base64 127) + + # Add external service credentials + NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=your-cloud-name + CLOUDINARY_KEY=your-api-key + CLOUDINARY_SECRET=your-api-secret + # ... (see Environment Variables section for complete list) + ``` + +3. **Install dependencies** + ```bash + npm install + ``` + +4. **Run Prisma migrations** (current frontend database) + ```bash + npx prisma generate + npx prisma migrate dev + ``` + +5. **Start the development server** + ```bash + npm run dev + ``` + + The frontend will be available at http://localhost:3000 + +--- + +## Environment Variables + +### Overview + +The Biergarten App uses environment variables for configuration across both backend (.NET API) and frontend (Next.js) services. This section provides complete documentation for all required and optional variables. + +**Configuration Patterns:** +- **Backend**: Direct environment variable access via `Environment.GetEnvironmentVariable()` +- **Frontend**: Centralized configuration module at [src/Website/src/config/env/index.ts](src/Website/src/config/env/index.ts) with Zod validation +- **Docker**: Environment-specific `.env` files (`.env.dev`, `.env.test`, `.env.prod`) + +### Backend Variables (.NET API) + +The .NET API requires environment variables for database connectivity and JWT authentication. These can be set directly in your shell or via `.env` files when using Docker. + +#### Database Connection + +**Option 1: Component-Based (Recommended for Docker)** + +Use individual components to build the connection string: + +```bash +DB_SERVER=sqlserver,1433 # SQL Server address and port +DB_NAME=Biergarten # Database name +DB_USER=sa # SQL Server username +DB_PASSWORD=YourStrong!Passw0rd # SQL Server password +DB_TRUST_SERVER_CERTIFICATE=True # Optional, defaults to True +``` + +**Option 2: Full Connection String (Local Development)** + +Provide a complete SQL Server connection string: + +```bash +DB_CONNECTION_STRING="Server=localhost,1433;Database=Biergarten;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True;" +``` + +The connection factory checks for `DB_CONNECTION_STRING` first, then falls back to building from components. See [DefaultSqlConnectionFactory.cs](src/Core/Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs). + +#### JWT Authentication + +```bash +JWT_SECRET=your-secret-key-minimum-32-characters-required +``` + +- **Required**: Yes +- **Minimum Length**: 32 characters +- **Used For**: Signing JWT tokens for user authentication +- **Location**: [JwtService.cs](src/Core/Service/Service.Core/Services/JwtService.cs) + +**Additional JWT Configuration** (in `appsettings.json`): +- `Jwt:ExpirationMinutes` - Token lifetime (default: 60) +- `Jwt:Issuer` - Token issuer (default: "biergarten-api") +- `Jwt:Audience` - Token audience (default: "biergarten-users") + +#### Migration Control + +```bash +CLEAR_DATABASE=true # Development/Testing only +``` + +- **Required**: No +- **Effect**: If set to "true", drops and recreates the database during migrations +- **Usage**: Development and testing environments only +- **Warning**: Never use in production + +### Frontend Variables (Next.js) + +The Next.js frontend requires environment variables for external services, authentication, and database connectivity. Create a `.env` or `.env.local` file in the `Website/` directory. + +All variables are validated at runtime using Zod schemas. See [src/Website/src/config/env/index.ts](src/Website/src/config/env/index.ts). + +#### Base Configuration + +```bash +BASE_URL=http://localhost:3000 # Application base URL +NODE_ENV=development # Environment: development, production, test +``` + +#### Authentication & Sessions + +```bash +# Token signing secrets (generate with: openssl rand -base64 127) +CONFIRMATION_TOKEN_SECRET= # Email confirmation tokens +RESET_PASSWORD_TOKEN_SECRET= # Password reset tokens +SESSION_SECRET= # Session cookie signing + +# Session configuration +SESSION_TOKEN_NAME=biergarten # Cookie name +SESSION_MAX_AGE=604800 # Cookie max age in seconds (604800 = 1 week) +``` + +#### Database (Prisma/Postgres) + +**Current State**: The frontend currently uses Neon Postgres with Prisma. This will migrate to the SQL Server backend once feature parity is achieved. + +```bash +POSTGRES_PRISMA_URL=postgresql://user:pass@host/db?pgbouncer=true # Pooled connection +POSTGRES_URL_NON_POOLING=postgresql://user:pass@host/db # Direct connection (migrations) +SHADOW_DATABASE_URL=postgresql://user:pass@host/shadow_db # Prisma shadow DB +``` + +#### Admin Account + +```bash +ADMIN_PASSWORD=SecureAdminPassword123! # Initial admin account password for seeding +``` + +### Docker Variables + +When running services in Docker, additional environment variables control container behavior: + +#### ASP.NET Core + +```bash +ASPNETCORE_ENVIRONMENT=Development # Development, Production +ASPNETCORE_URLS=http://0.0.0.0:8080 # Binding address +DOTNET_RUNNING_IN_CONTAINER=true # Container execution flag +``` + +#### SQL Server (Docker Container) + +```bash +SA_PASSWORD=YourStrong!Passw0rd # SQL Server SA password (maps to DB_PASSWORD) +ACCEPT_EULA=Y # Accept SQL Server EULA +MSSQL_PID=Express # SQL Server edition (Express, Developer, etc.) +``` + +**Note**: `SA_PASSWORD` in the SQL Server container maps to `DB_PASSWORD` for the API application. + +### External Services + +The frontend integrates with several third-party services. Sign up for accounts and retrieve API credentials: + +#### Cloudinary (Image Hosting) + +```bash +NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=your-cloud-name # Public, client-accessible +CLOUDINARY_KEY=your-api-key # Server-side API key +CLOUDINARY_SECRET=your-api-secret # Server-side secret +``` + +**Setup**: +1. Sign up at [cloudinary.com](https://cloudinary.com) +2. Navigate to Dashboard +3. Copy Cloud Name, API Key, and API Secret + +**Note**: The `NEXT_PUBLIC_` prefix makes the cloud name accessible in client-side code. + +#### Mapbox (Maps & Geocoding) + +```bash +MAPBOX_ACCESS_TOKEN=pk.your-public-token +``` + +**Setup**: +1. Create account at [mapbox.com](https://mapbox.com) +2. Navigate to Account → Tokens +3. Create a new token with public scopes +4. Copy the access token + +#### SparkPost (Email Service) + +```bash +SPARKPOST_API_KEY=your-api-key +SPARKPOST_SENDER_ADDRESS=noreply@yourdomain.com +``` + +**Setup**: +1. Sign up at [sparkpost.com](https://sparkpost.com) +2. Verify your sending domain or use sandbox +3. Create an API key with "Send via SMTP" permission +4. Configure sender address (must match verified domain) + +### Generating Secrets + +For authentication secrets (`JWT_SECRET`, `CONFIRMATION_TOKEN_SECRET`, etc.), generate cryptographically secure random values: + +**macOS/Linux:** +```bash +openssl rand -base64 127 +``` + +**Windows PowerShell:** +```powershell +[Convert]::ToBase64String((1..127 | ForEach-Object { Get-Random -Maximum 256 })) +``` + +**Requirements**: +- `JWT_SECRET`: Minimum 32 characters +- Session/token secrets: Recommend 127+ characters for maximum security + +### Environment File Structure + +The project uses multiple environment files depending on the context: + +#### Backend/Docker (Root Directory) + +- **`.env.example`** - Template file (tracked in Git) +- **`.env.dev`** - Development environment (gitignored) +- **`.env.test`** - Testing environment (gitignored) +- **`.env.prod`** - Production environment (gitignored) + +**Setup**: +```bash +# Copy template and customize +cp .env.example .env.dev +# Edit .env.dev with your values +``` + +Docker Compose files reference these: +- `docker-compose.dev.yaml` → `.env.dev` +- `docker-compose.test.yaml` → `.env.test` +- `docker-compose.prod.yaml` → `.env.prod` + +#### Frontend (Website Directory) + +- **`.env`** or **`.env.local`** - Local development (gitignored) + +**Setup**: +```bash +cd Website +# Create .env file with frontend variables +touch .env.local +``` + +### Variable Reference Table + +| Variable | Backend | Frontend | Docker | Required | Notes | +|----------|---------|----------|--------|----------|-------| +| **Database** | +| `DB_SERVER` | ✓ | | ✓ | Yes* | SQL Server address | +| `DB_NAME` | ✓ | | ✓ | Yes* | Database name | +| `DB_USER` | ✓ | | ✓ | Yes* | SQL username | +| `DB_PASSWORD` | ✓ | | ✓ | Yes* | SQL password | +| `DB_CONNECTION_STRING` | ✓ | | | Yes* | Alternative to components | +| `DB_TRUST_SERVER_CERTIFICATE` | ✓ | | ✓ | No | Defaults to True | +| `SA_PASSWORD` | | | ✓ | Yes | SQL Server container only | +| **Authentication (Backend)** | +| `JWT_SECRET` | ✓ | | ✓ | Yes | Min 32 chars | +| **Authentication (Frontend)** | +| `CONFIRMATION_TOKEN_SECRET` | | ✓ | | Yes | Email confirmation | +| `RESET_PASSWORD_TOKEN_SECRET` | | ✓ | | Yes | Password reset | +| `SESSION_SECRET` | | ✓ | | Yes | Session signing | +| `SESSION_TOKEN_NAME` | | ✓ | | No | Default: "biergarten" | +| `SESSION_MAX_AGE` | | ✓ | | No | Default: 604800 | +| **Base Configuration** | +| `BASE_URL` | | ✓ | | Yes | App base URL | +| `NODE_ENV` | | ✓ | | Yes | development/production | +| `ASPNETCORE_ENVIRONMENT` | ✓ | | ✓ | Yes | Development/Production | +| `ASPNETCORE_URLS` | ✓ | | ✓ | Yes | Binding address | +| **Database (Frontend - Current)** | +| `POSTGRES_PRISMA_URL` | | ✓ | | Yes | Pooled connection | +| `POSTGRES_URL_NON_POOLING` | | ✓ | | Yes | Direct connection | +| `SHADOW_DATABASE_URL` | | ✓ | | No | Prisma shadow DB | +| **External Services** | +| `NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME` | | ✓ | | Yes | Public, client-side | +| `CLOUDINARY_KEY` | | ✓ | | Yes | Server-side | +| `CLOUDINARY_SECRET` | | ✓ | | Yes | Server-side | +| `MAPBOX_ACCESS_TOKEN` | | ✓ | | Yes | Maps/geocoding | +| `SPARKPOST_API_KEY` | | ✓ | | Yes | Email service | +| `SPARKPOST_SENDER_ADDRESS` | | ✓ | | Yes | From address | +| **Other** | +| `ADMIN_PASSWORD` | | ✓ | | No | Seeding only | +| `CLEAR_DATABASE` | ✓ | | ✓ | No | Dev/test only | +| `ACCEPT_EULA` | | | ✓ | Yes | SQL Server EULA | +| `MSSQL_PID` | | | ✓ | No | SQL Server edition | + +\* Either `DB_CONNECTION_STRING` OR the four component variables (`DB_SERVER`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`) are required. + --- ## Testing diff --git a/docker-compose.db.yaml b/docker-compose.db.yaml index 5db74df..4c359b3 100644 --- a/docker-compose.db.yaml +++ b/docker-compose.db.yaml @@ -1,25 +1,27 @@ services: - sqlserver: + sqlserver: + env_file: ".env.dev" image: mcr.microsoft.com/mssql/server:2022-latest platform: linux/amd64 container_name: dev-env-sqlserver environment: ACCEPT_EULA: "Y" - SA_PASSWORD: "${SA_PASSWORD}" + SA_PASSWORD: "${DB_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"] + test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1"] interval: 10s timeout: 5s retries: 12 start_period: 30s networks: - devnet - database.migrations: + database.migrations: + env_file: ".env.dev" image: database.migrations container_name: dev-env-database-migrations depends_on: @@ -33,14 +35,17 @@ services: APP_UID: 1000 environment: DOTNET_RUNNING_IN_CONTAINER: "true" - DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}" - MASTER_DB_CONNECTION_STRING: "${MASTER_DB_CONNECTION_STRING}" + DB_SERVER: "${DB_SERVER}" + DB_NAME: "${DB_NAME}" + DB_USER: "${DB_USER}" + DB_PASSWORD: "${DB_PASSWORD}" CLEAR_DATABASE: "true" restart: "no" networks: - devnet - database.seed: +database.seed: + env_file: ".env.dev" image: database.seed container_name: dev-env-database-seed depends_on: @@ -54,11 +59,13 @@ services: APP_UID: 1000 environment: DOTNET_RUNNING_IN_CONTAINER: "true" - DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}" + DB_SERVER: "${DB_SERVER}" + DB_NAME: "${DB_NAME}" + DB_USER: "${DB_USER}" + DB_PASSWORD: "${DB_PASSWORD}" restart: "no" networks: - devnet - volumes: sqlserverdata-dev: driver: local diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index 26eedfc..f764ee0 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -1,18 +1,19 @@ services: sqlserver: + env_file: ".env.dev" image: mcr.microsoft.com/mssql/server:2022-latest platform: linux/amd64 container_name: dev-env-sqlserver environment: ACCEPT_EULA: "Y" - SA_PASSWORD: "${SA_PASSWORD}" + SA_PASSWORD: "${DB_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"] + test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1"] interval: 10s timeout: 5s retries: 12 @@ -20,6 +21,7 @@ services: networks: - devnet database.migrations: + env_file: ".env.dev" image: database.migrations container_name: dev-env-database-migrations depends_on: @@ -33,14 +35,17 @@ services: APP_UID: 1000 environment: DOTNET_RUNNING_IN_CONTAINER: "true" - DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}" - MASTER_DB_CONNECTION_STRING: "${MASTER_DB_CONNECTION_STRING}" + DB_SERVER: "${DB_SERVER}" + DB_NAME: "${DB_NAME}" + DB_USER: "${DB_USER}" + DB_PASSWORD: "${DB_PASSWORD}" CLEAR_DATABASE: "true" restart: "no" networks: - devnet database.seed: + env_file: ".env.dev" image: database.seed container_name: dev-env-database-seed depends_on: @@ -54,12 +59,16 @@ services: APP_UID: 1000 environment: DOTNET_RUNNING_IN_CONTAINER: "true" - DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}" + DB_SERVER: "${DB_SERVER}" + DB_NAME: "${DB_NAME}" + DB_USER: "${DB_USER}" + DB_PASSWORD: "${DB_PASSWORD}" restart: "no" networks: - devnet api.core: + env_file: ".env.dev" image: api.core container_name: dev-env-api-core depends_on: @@ -78,7 +87,10 @@ services: ASPNETCORE_ENVIRONMENT: "Development" ASPNETCORE_URLS: "http://0.0.0.0:8080" DOTNET_RUNNING_IN_CONTAINER: "true" - DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}" + DB_SERVER: "${DB_SERVER}" + DB_NAME: "${DB_NAME}" + DB_USER: "${DB_USER}" + DB_PASSWORD: "${DB_PASSWORD}" JWT_SECRET: "${JWT_SECRET}" restart: unless-stopped networks: diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml index eccbf98..d41d268 100644 --- a/docker-compose.prod.yaml +++ b/docker-compose.prod.yaml @@ -1,16 +1,17 @@ services: sqlserver: + env_file: ".env.prod" image: mcr.microsoft.com/mssql/server:2022-latest platform: linux/amd64 container_name: prod-env-sqlserver environment: ACCEPT_EULA: "Y" - SA_PASSWORD: "${SA_PASSWORD}" + SA_PASSWORD: "${DB_PASSWORD}" MSSQL_PID: "Express" volumes: - sqlserverdata-prod:/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"] + test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1"] interval: 10s timeout: 5s retries: 12 @@ -19,6 +20,7 @@ services: - prodnet database.migrations: + env_file: ".env.prod" image: database.migrations container_name: prod-env-database-migrations depends_on: @@ -32,13 +34,16 @@ services: APP_UID: 1000 environment: DOTNET_RUNNING_IN_CONTAINER: "true" - DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}" - MASTER_DB_CONNECTION_STRING: "${MASTER_DB_CONNECTION_STRING}" + DB_SERVER: "${DB_SERVER}" + DB_NAME: "${DB_NAME}" + DB_USER: "${DB_USER}" + DB_PASSWORD: "${DB_PASSWORD}" restart: "no" networks: - prodnet api.core: + env_file: ".env.prod" image: api.core container_name: prod-env-api-core depends_on: @@ -57,8 +62,10 @@ services: ASPNETCORE_ENVIRONMENT: "Production" ASPNETCORE_URLS: "http://0.0.0.0:8080" DOTNET_RUNNING_IN_CONTAINER: "true" - MASTER_DB_CONNECTION_STRING: "${MASTER_DB_CONNECTION_STRING}" - DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}" + DB_SERVER: "${DB_SERVER}" + DB_NAME: "${DB_NAME}" + DB_USER: "${DB_USER}" + DB_PASSWORD: "${DB_PASSWORD}" JWT_SECRET: "${JWT_SECRET}" restart: unless-stopped networks: diff --git a/docker-compose.test.yaml b/docker-compose.test.yaml index 08e93b7..17b2cf2 100644 --- a/docker-compose.test.yaml +++ b/docker-compose.test.yaml @@ -1,17 +1,18 @@ services: sqlserver: + env_file: ".env.test" image: mcr.microsoft.com/mssql/server:2022-latest platform: linux/amd64 container_name: test-env-sqlserver environment: ACCEPT_EULA: "Y" - SA_PASSWORD: "${SA_PASSWORD}" + SA_PASSWORD: "${DB_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"] + test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1"] interval: 10s timeout: 5s retries: 12 @@ -20,6 +21,7 @@ services: - testnet database.migrations: + env_file: ".env.test" image: database.migrations container_name: test-env-database-migrations depends_on: @@ -33,14 +35,17 @@ services: 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}" + DB_SERVER: "${DB_SERVER}" + DB_NAME: "${DB_NAME}" + DB_USER: "${DB_USER}" + DB_PASSWORD: "${DB_PASSWORD}" CLEAR_DATABASE: "true" restart: "no" networks: - testnet database.seed: + env_file: ".env.test" image: database.seed container_name: test-env-database-seed depends_on: @@ -54,12 +59,16 @@ services: APP_UID: 1000 environment: DOTNET_RUNNING_IN_CONTAINER: "true" - DB_CONNECTION_STRING: "${TEST_DB_CONNECTION_STRING}" + DB_SERVER: "${DB_SERVER}" + DB_NAME: "${DB_NAME}" + DB_USER: "${DB_USER}" + DB_PASSWORD: "${DB_PASSWORD}" restart: "no" networks: - testnet api.specs: + env_file: ".env.test" image: api.specs container_name: test-env-api-specs depends_on: @@ -72,7 +81,10 @@ services: BUILD_CONFIGURATION: Release environment: DOTNET_RUNNING_IN_CONTAINER: "true" - DB_CONNECTION_STRING: "${TEST_DB_CONNECTION_STRING}" + DB_SERVER: "${DB_SERVER}" + DB_NAME: "${DB_NAME}" + DB_USER: "${DB_USER}" + DB_PASSWORD: "${DB_PASSWORD}" JWT_SECRET: "${JWT_SECRET}" volumes: - ./test-results:/app/test-results @@ -81,6 +93,7 @@ services: - testnet repository.tests: + env_file: ".env.test" image: repository.tests container_name: test-env-repository-tests depends_on: @@ -93,8 +106,6 @@ services: 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" diff --git a/src/Core/API/API.Specs/Dockerfile b/src/Core/API/API.Specs/Dockerfile index 7929db8..97139c9 100644 --- a/src/Core/API/API.Specs/Dockerfile +++ b/src/Core/API/API.Specs/Dockerfile @@ -16,4 +16,4 @@ 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"] +ENTRYPOINT ["dotnet", "test", "API.Specs.csproj", "-c", "Release", "--logger", "trx;LogFileName=/app/test-results/test-results.trx"] diff --git a/src/Core/API/API.Specs/TestApiFactory.cs b/src/Core/API/API.Specs/TestApiFactory.cs index 36e7820..15da2c8 100644 --- a/src/Core/API/API.Specs/TestApiFactory.cs +++ b/src/Core/API/API.Specs/TestApiFactory.cs @@ -10,11 +10,6 @@ namespace API.Specs protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.UseEnvironment("Testing"); - - builder.ConfigureAppConfiguration((context, configBuilder) => - { - var connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING"); - }); } } } diff --git a/src/Core/Database/Database.Migrations/Program.cs b/src/Core/Database/Database.Migrations/Program.cs index 86281cb..af80640 100644 --- a/src/Core/Database/Database.Migrations/Program.cs +++ b/src/Core/Database/Database.Migrations/Program.cs @@ -7,8 +7,39 @@ namespace Database.Migrations; public static class Program { - private static readonly string? connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING"); - private static readonly string? masterConnectionString = Environment.GetEnvironmentVariable("MASTER_DB_CONNECTION_STRING"); + private static string BuildConnectionString(string? databaseName = null) + { + var server = Environment.GetEnvironmentVariable("DB_SERVER") + ?? throw new InvalidOperationException("DB_SERVER environment variable is not set"); + + var dbName = databaseName + ?? Environment.GetEnvironmentVariable("DB_NAME") + ?? throw new InvalidOperationException("DB_NAME environment variable is not set"); + + var user = Environment.GetEnvironmentVariable("DB_USER") + ?? throw new InvalidOperationException("DB_USER environment variable is not set"); + + var password = Environment.GetEnvironmentVariable("DB_PASSWORD") + ?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set"); + + var trustServerCertificate = Environment.GetEnvironmentVariable("DB_TRUST_SERVER_CERTIFICATE") + ?? "True"; + + var builder = new SqlConnectionStringBuilder + { + DataSource = server, + InitialCatalog = dbName, + UserID = user, + Password = password, + TrustServerCertificate = bool.Parse(trustServerCertificate), + Encrypt = true + }; + + return builder.ConnectionString; + } + + private static readonly string connectionString = BuildConnectionString(); + private static readonly string masterConnectionString = BuildConnectionString("master"); private static bool DeployMigrations() { diff --git a/src/Core/Database/Database.Seed/Program.cs b/src/Core/Database/Database.Seed/Program.cs index 0fb1138..e7fff96 100644 --- a/src/Core/Database/Database.Seed/Program.cs +++ b/src/Core/Database/Database.Seed/Program.cs @@ -3,9 +3,39 @@ using Microsoft.Data.SqlClient; using DbUp; using System.Reflection; +string BuildConnectionString() +{ + var server = Environment.GetEnvironmentVariable("DB_SERVER") + ?? throw new InvalidOperationException("DB_SERVER environment variable is not set"); + + var dbName = Environment.GetEnvironmentVariable("DB_NAME") + ?? throw new InvalidOperationException("DB_NAME environment variable is not set"); + + var user = Environment.GetEnvironmentVariable("DB_USER") + ?? throw new InvalidOperationException("DB_USER environment variable is not set"); + + var password = Environment.GetEnvironmentVariable("DB_PASSWORD") + ?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set"); + + var trustServerCertificate = Environment.GetEnvironmentVariable("DB_TRUST_SERVER_CERTIFICATE") + ?? "True"; + + var builder = new SqlConnectionStringBuilder + { + DataSource = server, + InitialCatalog = dbName, + UserID = user, + Password = password, + TrustServerCertificate = bool.Parse(trustServerCertificate), + Encrypt = true + }; + + return builder.ConnectionString; +} + try { - var connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING"); + var connectionString = BuildConnectionString(); Console.WriteLine("Attempting to connect to database..."); diff --git a/src/Core/Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs b/src/Core/Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs index eb8abad..b38f25d 100644 --- a/src/Core/Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs +++ b/src/Core/Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs @@ -7,11 +7,36 @@ namespace DataAccessLayer.Sql { public class DefaultSqlConnectionFactory(IConfiguration configuration) : ISqlConnectionFactory { - private readonly string _connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING") - ?? configuration.GetConnectionString("Default") - ?? throw new InvalidOperationException( - "Database connection string not configured. Set DB_CONNECTION_STRING env var or ConnectionStrings:Default." - ); + private readonly string _connectionString = GetConnectionString(configuration); + + private static string GetConnectionString(IConfiguration configuration) + { + // Check for full connection string first + var fullConnectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING"); + if (!string.IsNullOrEmpty(fullConnectionString)) + { + return fullConnectionString; + } + + // Try to build from individual environment variables (preferred method for Docker) + try + { + return SqlConnectionStringHelper.BuildConnectionString(); + } + catch (InvalidOperationException) + { + // Fall back to configuration-based connection string if env vars are not set + var connString = configuration.GetConnectionString("Default"); + if (!string.IsNullOrEmpty(connString)) + { + return connString; + } + + throw new InvalidOperationException( + "Database connection string not configured. Set DB_CONNECTION_STRING or DB_SERVER, DB_NAME, DB_USER, DB_PASSWORD env vars or ConnectionStrings:Default." + ); + } + } public DbConnection CreateConnection() { diff --git a/src/Core/Repository/Repository.Core/Sql/SqlConnectionStringHelper.cs b/src/Core/Repository/Repository.Core/Sql/SqlConnectionStringHelper.cs new file mode 100644 index 0000000..907c82f --- /dev/null +++ b/src/Core/Repository/Repository.Core/Sql/SqlConnectionStringHelper.cs @@ -0,0 +1,50 @@ +using Microsoft.Data.SqlClient; + +namespace DataAccessLayer.Sql +{ + public static class SqlConnectionStringHelper + { + /// + /// Builds a SQL Server connection string from environment variables. + /// Expects DB_SERVER, DB_NAME, DB_USER, DB_PASSWORD, and DB_TRUST_SERVER_CERTIFICATE. + /// + /// Optional override for the database name. If null, uses DB_NAME env var. + /// A properly formatted SQL Server connection string. + public static string BuildConnectionString(string? databaseName = null) + { + var server = Environment.GetEnvironmentVariable("DB_SERVER") + ?? throw new InvalidOperationException("DB_SERVER environment variable is not set"); + + var dbName = databaseName + ?? Environment.GetEnvironmentVariable("DB_NAME") + ?? throw new InvalidOperationException("DB_NAME environment variable is not set"); + + var user = Environment.GetEnvironmentVariable("DB_USER") + ?? throw new InvalidOperationException("DB_USER environment variable is not set"); + + var password = Environment.GetEnvironmentVariable("DB_PASSWORD") + ?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set"); + + var builder = new SqlConnectionStringBuilder + { + DataSource = server, + InitialCatalog = dbName, + UserID = user, + Password = password, + TrustServerCertificate = true, + Encrypt = true + }; + + return builder.ConnectionString; + } + + /// + /// Builds a connection string to the master database using environment variables. + /// + /// A connection string for the master database. + public static string BuildMasterConnectionString() + { + return BuildConnectionString("master"); + } + } +} diff --git a/src/Core/Repository/Repository.Tests/Dockerfile b/src/Core/Repository/Repository.Tests/Dockerfile index 6d5db48..4b83399 100644 --- a/src/Core/Repository/Repository.Tests/Dockerfile +++ b/src/Core/Repository/Repository.Tests/Dockerfile @@ -11,4 +11,4 @@ RUN dotnet build "./Repository.Tests.csproj" -c $BUILD_CONFIGURATION -o /app/bui 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"] +ENTRYPOINT ["dotnet", "test", "./Repository.Tests.csproj", "-c", "Release", "--logger", "trx;LogFileName=/app/test-results/repository-tests.trx"]