diff --git a/README.md b/README.md index 5304939..658189a 100644 --- a/README.md +++ b/README.md @@ -1,918 +1,278 @@ # The Biergarten App -A social platform for craft beer enthusiasts to discover breweries, share reviews, and connect with fellow beer lovers. +A social platform for craft beer enthusiasts to discover breweries, share reviews, and +connect with fellow beer lovers. -## Project Status +**Documentation** -This project is in active development, transitioning from a full-stack Next.js application to a **multi-project monorepo** with: -- **Backend**: .NET 10 Web API with SQL Server -- **Frontend**: Next.js with TypeScript -- **Architecture**: SQL-first approach using stored procedures +- [Getting Started](docs/getting-started.md) - Setup and installation +- [Architecture](docs/architecture.md) - System design and patterns +- [Database](docs/database.md) - Schema and stored procedures +- [Docker Guide](docs/docker.md) - Container deployment +- [Testing](docs/testing.md) - Test strategy and commands +- [Environment Variables](docs/environment-variables.md) - Configuration reference -**Current State** (February 2026): -- Core authentication and user management APIs functional -- Database schema and migrations established -- Domain, Infrastructure, Repository, and Service layers implemented -- Frontend integration with .NET API in progress -- Migrating remaining features from Next.js serverless functions +**Diagrams** -**The Next.js app currently runs standalone with its original Prisma/Neon Postgres backend. It will be fully integrated with the .NET API once feature parity is achieved.** +- [Architecture](docs/diagrams/pdf/architecture.pdf) - Layered architecture +- [Deployment](docs/diagrams/pdf/deployment.pdf) - Docker topology +- [Authentication Flow](docs/diagrams/pdf/authentication-flow.pdf) - Auth sequence +- [Database Schema](docs/diagrams/pdf/database-schema.pdf) - Entity relationships + +## Project Status + +**Active Development** - Transitioning from full-stack Next.js to multi-project monorepo + +- Core authentication and user management APIs +- Database schema with migrations and seeding +- Layered architecture (Domain, Service, Infrastructure, Repository, API) +- Comprehensive test suite (unit + integration) +- Frontend integration with .NET API (in progress) +- Migration from Next.js serverless functions --- -## Repository Structure +## Tech Stack -``` -src/Core/ -├── API/ -│ ├── API.Core/ # ASP.NET Core Web API with Swagger/OpenAPI -│ └── API.Specs/ # Integration tests using Reqnroll (BDD) -├── Database/ -│ ├── Database.Migrations/ # DbUp migrations (embedded SQL scripts) -│ └── Database.Seed/ # Database seeding for development/testing -├── Domain/ -│ └── Domain.csproj # Domain entities and models -│ └── Entities/ # Core domain entities (UserAccount, UserCredential, etc.) -├── Infrastructure/ -│ ├── Infrastructure.Jwt/ # JWT token generation and validation -│ ├── Infrastructure.PasswordHashing/ # Argon2id password hashing -│ └── Infrastructure.Repository/ -│ ├── Infrastructure.Repository/ # Data access layer (stored procedure-based) -│ └── Infrastructure.Repository.Tests/ # Unit tests for repositories -└── Service/ - └── Service.Core/ # Business logic layer - -Website/ # Next.js frontend application -misc/ -└── raw-data/ # Sample data files (breweries, beers) -``` - -### Key Components - -**API Layer** (`API.Core`) -- RESTful endpoints for authentication, users, and breweries -- Controllers: `AuthController`, `UserController` -- Configured with Swagger UI for API exploration -- Health checks and structured logging -- Middleware for error handling and request processing - -**Database Layer** -- SQL Server with stored procedures for all data operations -- DbUp for version-controlled migrations -- Comprehensive schema including users, breweries, beers, locations, and social features -- Seeders for development data (users, locations across US/Canada/Mexico) - -**Domain Layer** (`Domain`) -- Core business entities and models -- Entities: `UserAccount`, `UserCredential`, `UserVerification` -- Shared domain logic and value objects -- No external dependencies - pure domain model - -**Infrastructure Layer** -- **Infrastructure.Jwt**: JWT token generation, validation, and configuration -- **Infrastructure.PasswordHashing**: Argon2id password hashing with configurable parameters -- **Infrastructure.Password**: Password utilities and validation -- **Infrastructure.Repository**: Repository pattern infrastructure and base classes - -**Repository Layer** (`Infrastructure.Repository`) -- Abstraction over SQL Server using ADO.NET -- `ISqlConnectionFactory` for connection management -- Repositories: `AuthRepository`, `UserAccountRepository` -- All data access via stored procedures (no inline SQL) - -**Service Layer** (`Service.Core`) -- Business logic and orchestration -- Services: `AuthService`, `UserService` -- Integration with infrastructure components -- Transaction management and business rule enforcement - -**Frontend** (`Website`) -- Next.js 14+ with TypeScript -- TailwindCSS, Headless UI, DaisyUI for UI components -- Integrations: Mapbox (maps), Cloudinary (image hosting) -- Progressive migration from serverless API routes to .NET API +**Backend**: .NET 10, ASP.NET Core, SQL Server 2022, DbUp **Frontend**: Next.js 14+, +TypeScript, TailwindCSS **Testing**: xUnit, Reqnroll (BDD), FluentAssertions, Moq +**Infrastructure**: Docker, Docker Compose **Security**: Argon2id password hashing, JWT +(HS256) --- -## Technology Stack - -### Backend -- **.NET 10** - Latest C# and runtime features -- **ASP.NET Core** - Web API framework -- **SQL Server 2022** - Primary database -- **DbUp** - Database migration tool -- **Argon2id** - Password hashing -- **JWT** - Authentication tokens - -### Frontend -- **Next.js 14+** - React framework -- **TypeScript** - Type safety -- **TailwindCSS** - Utility-first CSS -- **Mapbox GL** - Interactive maps -- **Cloudinary** - Image management - -### Testing -- **xUnit** - Unit testing framework -- **Reqnroll** - BDD/Gherkin integration testing -- **FluentAssertions** - Assertion library -- **DbMocker** - Database mocking - -### DevOps & Infrastructure -- **Docker** - Containerization for all services -- **Docker Compose** - Multi-container orchestration -- **Multi-stage builds** - Optimized image sizes -- **Health checks** - Container readiness and liveness probes -- **Separate environments** - Development, testing, and production configurations - ---- - -## Getting Started +## Quick Start ### Prerequisites -- **.NET SDK 10+** ([Download](https://dotnet.microsoft.com/download)) -- **Node.js 18+** ([Download](https://nodejs.org/)) -- **Docker Desktop** ([Download](https://www.docker.com/products/docker-desktop)) +- [.NET SDK 10+](https://dotnet.microsoft.com/download) +- [Docker Desktop](https://www.docker.com/products/docker-desktop) +- [Node.js 18+](https://nodejs.org/) (for frontend) -### Quick Start (Development Environment) - -1. **Clone the repository** - ```bash - git clone - cd biergarten-app - ``` - -2. **Configure environment variables** - - Copy the example file and customize: - ```bash - 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-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 - ``` - - This will: - - Start SQL Server - - Run database migrations - - Seed initial data - - Start the API on http://localhost:8080 - -4. **Access Swagger UI** - - 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 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 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-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-minimum-32-characters-required" - ``` - -3. **Run migrations** - ```bash - cd src/Core - dotnet run --project Database/Database.Migrations/Database.Migrations.csproj - ``` - -4. **Seed the database** - ```bash - dotnet run --project Database/Database.Seed/Database.Seed.csproj - ``` - -5. **Start the API** - ```bash - 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: +### Start Development Environment ```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 -``` +# Clone repository +git clone https://github.com/aaronpo97/the-biergarten-app +cd the-biergarten-app -**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/Infrastructure/Infrastructure.Repository/Infrastructure.Repository/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 +# Configure environment 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 - -### Run All Tests (Docker) -```bash -docker compose -f docker-compose.test.yaml up --abort-on-container-exit -``` - -This runs: -- **API.Specs** - BDD integration tests -- **Infrastructure.Repository.Tests** - Unit tests for data access - -Test results are output to `./test-results/`. - -### Run Tests Locally - -**Integration Tests (API.Specs)** -```bash -cd src/Core -dotnet test API/API.Specs/API.Specs.csproj -``` - -**Unit Tests (Infrastructure.Repository.Tests)** -```bash -cd src/Core -dotnet test Infrastructure/Infrastructure.Repository/Infrastructure.Repository.Tests/Repository.Tests.csproj -``` - -### Test Features - -Current test coverage includes: -- User authentication (login, registration) -- JWT token generation -- Password validation -- 404 error handling -- User repository operations - ---- - -## Database Schema - -The database uses a SQL-first approach with comprehensive normalization and referential integrity. - -### Key Tables - -**User Management** -- `UserAccount` - User profiles -- `UserCredential` - Password hashes (Argon2id) -- `UserVerification` - Account verification status -- `UserAvatar` - Profile pictures -- `UserFollow` - Social following relationships - -**Location Data** -- `Country` - ISO 3166-1 country codes -- `StateProvince` - ISO 3166-2 subdivisions -- `City` - City/municipality data - -**Content** -- `BreweryPost` - Brewery information -- `BreweryPostLocation` - Geographic data with `GEOGRAPHY` type -- `BeerStyle` - Beer style taxonomy -- `BeerPost` - Individual beers with ABV/IBU -- `BeerPostComment` - User reviews and ratings -- `Photo` - Image metadata - -**Stored Procedures** (examples) -- `USP_RegisterUser` - Create user account with credential -- `USP_GetUserAccountByUsername` - Retrieve user by username -- `USP_RotateUserCredential` - Update password -- `USP_CreateCountry/StateProvince/City` - Location management - ---- - -## Authentication & Security - -- **Password Hashing**: Argon2id with configurable parameters - - Salt: 128-bit (16 bytes) - - Hash: 256-bit (32 bytes) - - Memory: 64MB - - Iterations: 4 - - Parallelism: Based on CPU cores - -- **JWT Tokens**: HS256 signing - - Claims: User ID (sub), Username (unique_name), JTI - - Configurable expiration (60-120 minutes) - - Secret key from environment variable - -- **Credential Management**: - - Credential rotation/invalidation supported - - Expiry tracking (90-day default) - - Revocation timestamps - ---- - -## Architecture Patterns - -### Layered Architecture -``` -API (Controllers) - | -Service Layer (Business Logic) - | -Repository Layer (Data Access) - | -Database (SQL Server) -``` - -### Design Patterns -- **Repository Pattern**: Abstraction over data access -- **Dependency Injection**: Constructor injection throughout -- **Factory Pattern**: `ISqlConnectionFactory` for database connections -- **Service Pattern**: Encapsulated business logic - -### SQL-First Approach -- All CRUD operations via stored procedures -- No ORM (Entity Framework, Dapper, etc.) -- Direct ADO.NET for maximum control -- Version-controlled schema via DbUp - ---- - -## Docker & Containerization - - -### Container Architecture - -### Docker Compose Environments - -Three separate compose files manage different environments: - -#### 1. **Development** (`docker-compose.dev.yaml`) -- **Purpose**: Local development with live data -- **Features**: - - SQL Server with persistent volume - - Database migrations with `CLEAR_DATABASE=true` (drops/recreates schema) - - Seed data for testing - - API accessible on `localhost:8080` - - Hot reload support via volume mounts - -**Services**: -```yaml -sqlserver # SQL Server 2022 (port 1433) -database.migrations # Runs DbUp migrations -database.seed # Seeds initial data -api.core # Web API (ports 8080, 8081) -``` - -**Usage**: -```bash -docker compose -f docker-compose.dev.yaml up -d -docker compose -f docker-compose.dev.yaml logs -f # View logs -docker compose -f docker-compose.dev.yaml down # Stop all services -``` - -#### 2. **Testing** (`docker-compose.test.yaml`) -- **Purpose**: Automated CI/CD testing -- **Features**: - - Isolated test database - - Runs integration and unit tests - - Test results exported to `./test-results/` - - Containers exit after tests complete - -**Services**: -```yaml -sqlserver # Test database instance -database.migrations # Fresh schema each run -database.seed # Test data -api.specs # Integration tests (Reqnroll) -repository.tests # Unit tests (xUnit) -``` - -**Usage**: -```bash -docker compose -f docker-compose.test.yaml up --abort-on-container-exit; -docker compose -f docker-compose.test.yaml down -v; -# View results in ./test-results/ -``` - -#### 3. **Production** (`docker-compose.prod.yaml`) -- **Purpose**: Production-like deployment -- **Features**: - - Production logging levels - - No database clearing - - Optimized builds - - Health checks enabled - - Restart policies configured - -**Services**: -```yaml -sqlserver # Production SQL Server -database.migrations # Schema updates only (no drops) -api.core # Production API -``` - -### Service Dependencies - -Docker Compose manages service startup order using **health checks** and **depends_on** conditions: - -```yaml -database.migrations: - depends_on: - sqlserver: - condition: service_healthy # Waits for SQL Server to be ready - -database.seed: - depends_on: - database.migrations: - condition: service_completed_successfully # Waits for migrations -``` - -**Flow**: -1. `sqlserver` starts and runs health check (SQL query) -2. `database.migrations` starts when SQL Server is healthy -3. `database.seed` starts when migrations complete successfully -4. `api.core` starts when seeding completes - -### Health Checks - -SQL Server container includes a health check to ensure it's ready: - -```yaml -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 -``` - -This prevents downstream services from attempting connections before SQL Server is ready. - -### Volumes - -**Persistent Storage**: -- `sqlserverdata-dev` - Development database data -- `sqlserverdata-test` - Test database data -- `sqlserverdata-prod` - Production database data -- `nuget-cache-dev/prod` - NuGet package cache (speeds up builds) - -**Mounted Volumes**: -```yaml -volumes: - - ./test-results:/app/test-results # Export test results to host - - nuget-cache-dev:/root/.nuget/packages # Cache dependencies -``` - -### Networks - -Each environment uses isolated bridge networks: -- `devnet` - Development network -- `testnet` - Testing network (fully isolated) -- `prodnet` - Production network - -This prevents cross-environment communication and enhances security. - -### Environment Variables - -All containers receive configuration via environment variables: - -```yaml -environment: - ASPNETCORE_ENVIRONMENT: "Development" - DOTNET_RUNNING_IN_CONTAINER: "true" - DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}" - JWT_SECRET: "${JWT_SECRET}" -``` - -Values are populated from the `.env` file in the project root. - -### Container Lifecycle - -**Development Workflow**: -```bash -# Start environment +# Start all services docker compose -f docker-compose.dev.yaml up -d # View logs -docker compose -f docker-compose.dev.yaml logs -f api.core - -# Restart a service -docker compose -f docker-compose.dev.yaml restart api.core - -# Rebuild after code changes -docker compose -f docker-compose.dev.yaml up -d --build api.core - -# Clean shutdown -docker compose -f docker-compose.dev.yaml down - -# Remove volumes (fresh start) -docker compose -f docker-compose.dev.yaml down -v +docker compose -f docker-compose.dev.yaml logs -f ``` -**Testing Workflow**: +**Access**: + +- API: http://localhost:8080/swagger +- Health: http://localhost:8080/health + +### Run Tests + ```bash -# Run tests (containers auto-exit) docker compose -f docker-compose.test.yaml up --abort-on-container-exit - -# Check test results -cat test-results/test-results.trx -cat test-results/repository-tests.trx - -# Clean up -docker compose -f docker-compose.test.yaml down -v ``` -## Docker Tips & Troubleshooting +Results are in `./test-results/` -### Common Commands +--- -**View running containers**: -```bash -docker ps +## Repository Structure + +``` +src/Core/ # Backend (.NET) +├── API/ +│ ├── API.Core/ # ASP.NET Core Web API +│ └── API.Specs/ # Integration tests (Reqnroll) +├── Database/ +│ ├── Database.Migrations/ # DbUp migrations +│ └── Database.Seed/ # Data seeding +├── Domain.Entities/ # Domain models +├── Infrastructure/ # Cross-cutting concerns +│ ├── Infrastructure.Jwt/ +│ ├── Infrastructure.PasswordHashing/ +│ ├── Infrastructure.Email/ +│ ├── Infrastructure.Repository/ +│ └── Infrastructure.Repository.Tests/ +└── Service/ # Business logic + ├── Service.Auth/ + ├── Service.Auth.Tests/ + └── Service.UserManagement/ + +Website/ # Frontend (Next.js) +docs/ # Documentation +docs/diagrams/ # PlantUML diagrams ``` -**View all containers (including stopped)**: -```bash -docker ps -a +--- + +## Key Features + +### Implemented + +- User registration and authentication +- JWT token-based auth +- Argon2id password hashing +- SQL Server with stored procedures +- Database migrations (DbUp) +- Docker containerization +- Comprehensive test suite +- Swagger/OpenAPI documentation +- Health checks + +### Planned + +- [ ] Brewery discovery and management +- [ ] Beer reviews and ratings +- [ ] Social following/followers +- [ ] Geospatial brewery search +- [ ] Image upload (Cloudinary) +- [ ] Email notifications +- [ ] OAuth integration + +--- + +## Architecture Highlights + +### Layered Architecture + +``` +API Layer (Controllers) + │ +Service Layer (Business Logic) + │ +Infrastructure Layer (Repositories, JWT, Email) + │ +Domain Layer (Entities) + │ +Database (SQL Server + Stored Procedures) ``` -**View logs for a specific service**: +### SQL-First Approach + +- All queries via stored procedures +- No ORM (no Entity Framework) +- Version-controlled schema + +### Security + +- **Password Hashing**: Argon2id (64MB memory, 4 iterations) +- **JWT Tokens**: HS256 with configurable expiration +- **Credential Rotation**: Built-in password change support + +See [Architecture Guide](docs/architecture.md) for details. + +--- + +## Testing + +The project includes three test suites: + +| Suite | Type | Framework | Purpose | +| ---------------------- | ----------- | -------------- | ---------------------- | +| **API.Specs** | Integration | Reqnroll (BDD) | End-to-end API testing | +| **Repository.Tests** | Unit | xUnit | Data access layer | +| **Service.Auth.Tests** | Unit | xUnit + Moq | Business logic | + +**Run All Tests**: + ```bash +docker compose -f docker-compose.test.yaml up --abort-on-container-exit +``` + +**Run Individual Test Suite**: + +```bash +cd src/Core +dotnet test API/API.Specs/API.Specs.csproj +dotnet test Infrastructure/Infrastructure.Repository.Tests/Infrastructure.Repository.Tests.csproj +dotnet test Service/Service.Auth.Tests/Service.Auth.Tests.csproj +``` + +See [Testing Guide](docs/testing.md) for more information. + +--- + +## Docker Environments + +The project uses three Docker Compose configurations: + +| File | Purpose | Features | +| ---------------------------- | ------------- | ------------------------------------------------- | +| **docker-compose.dev.yaml** | Development | Persistent data, hot reload, Swagger UI | +| **docker-compose.test.yaml** | CI/CD Testing | Isolated DB, auto-exit, test results export | +| **docker-compose.prod.yaml** | Production | Optimized builds, health checks, restart policies | + +**Common Commands**: + +```bash +# Development +docker compose -f docker-compose.dev.yaml up -d docker compose -f docker-compose.dev.yaml logs -f api.core +docker compose -f docker-compose.dev.yaml down -v + +# Testing +docker compose -f docker-compose.test.yaml up --abort-on-container-exit +docker compose -f docker-compose.test.yaml down -v + +# Build +docker compose -f docker-compose.dev.yaml build +docker compose -f docker-compose.dev.yaml build --no-cache ``` -**Execute commands in a running container**: +See [Docker Guide](docs/docker.md) for troubleshooting and advanced usage. + +--- + +## Configuration + +### Required Environment Variables + +**Backend** (`.env.dev`): + ```bash -docker exec -it dev-env-api-core bash +DB_SERVER=sqlserver,1433 +DB_NAME=Biergarten +DB_USER=sa +DB_PASSWORD=YourStrong!Passw0rd +JWT_SECRET= ``` -**Connect to SQL Server from host**: +**Frontend** (`.env.local`): + ```bash -# Using sqlcmd (if installed) -sqlcmd -S localhost,1433 -U sa -P 'YourStrong!Passw0rd' -C - -# Config -Server: localhost,1433 -Authentication: SQL Login -Username: sa -Password: (from .env) +BASE_URL=http://localhost:3000 +NODE_ENV=development +CONFIRMATION_TOKEN_SECRET= +RESET_PASSWORD_TOKEN_SECRET= +SESSION_SECRET= +# + External services (Cloudinary, Mapbox, SparkPost) ``` ---- - -## Roadmap - -### Near-term -- [ ] Complete API endpoints for breweries and beers -- [ ] Integrate frontend with .NET API -- [ ] Implement image upload service -- [ ] Add comprehensive API documentation - -### Medium-term -- [ ] Geospatial queries for nearby breweries -- [ ] Advanced authentication (OAuth, 2FA) ---- - -## License - -See [LICENSE.md](LICENSE.md) for details. +See [Environment Variables Guide](docs/environment-variables.md) for complete reference. --- -## Contact & Support +## Contributing -For questions about this project, please open an issue in the repository. +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request +### Development Workflow +1. Start development environment: `docker compose -f docker-compose.dev.yaml up -d` +2. Make changes to code +3. Run tests: `docker compose -f docker-compose.test.yaml up --abort-on-container-exit` +4. Rebuild if needed: `docker compose -f docker-compose.dev.yaml up -d --build api.core` + +## Support + +- **Documentation**: [docs/](docs/) +- **Architecture**: See [Architecture Guide](docs/architecture.md) diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..b603191 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,418 @@ +# Architecture + +This document describes the architecture patterns and design decisions for The Biergarten +App. + +## High-Level Overview + +The Biergarten App follows a **multi-project monorepo** architecture with clear separation +between backend and frontend: + +- **Backend**: .NET 10 Web API with SQL Server +- **Frontend**: Next.js with TypeScript +- **Architecture Style**: Layered architecture with SQL-first approach + +## Diagrams + +For visual representations, see: + +- [architecture.pdf](diagrams/pdf/architecture.pdf) - Layered architecture diagram +- [deployment.pdf](diagrams/pdf/deployment.pdf) - Docker deployment diagram +- [authentication-flow.pdf](diagrams/pdf/authentication-flow.pdf) - Authentication + workflow +- [database-schema.pdf](diagrams/pdf/database-schema.pdf) - Database relationships + +Generate diagrams with: `make diagrams` + +## Backend Architecture + +### Layered Architecture Pattern + +The backend follows a strict layered architecture: + +``` +┌─────────────────────────────────────┐ +│ API Layer (Controllers) │ +│ - HTTP Endpoints │ +│ - Request/Response mapping │ +│ - Swagger/OpenAPI │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ Service Layer (Business Logic) │ +│ - Authentication logic │ +│ - User management │ +│ - Validation & orchestration │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ Infrastructure Layer (Tools) │ +│ - JWT token generation │ +│ - Password hashing (Argon2id) │ +│ - Email services │ +│ - Repository implementations │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ Domain Layer (Entities) │ +│ - UserAccount, UserCredential │ +│ - Pure POCO classes │ +│ - No external dependencies │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ Database (SQL Server) │ +│ - Stored procedures │ +│ - Tables & constraints │ +└─────────────────────────────────────┘ +``` + +### Layer Responsibilities + +#### API Layer (`API.Core`) + +**Purpose**: HTTP interface and request handling + +**Components**: + +- Controllers (`AuthController`, `UserController`) +- Middleware for error handling +- Swagger/OpenAPI documentation +- Health check endpoints + +**Dependencies**: + +- Service layer +- ASP.NET Core framework + +**Rules**: + +- No business logic +- Only request/response transformation +- Delegates all work to Service layer + +#### Service Layer (`Service.Auth`, `Service.UserManagement`) + +**Purpose**: Business logic and orchestration + +**Components**: + +- Authentication services (login, registration) +- User management services +- Business rule validation +- Transaction coordination + +**Dependencies**: + +- Infrastructure layer (repositories, JWT, password hashing) +- Domain entities + +**Rules**: + +- Contains all business logic +- Coordinates multiple infrastructure components +- No direct database access (uses repositories) +- Returns domain models, not DTOs + +#### Infrastructure Layer + +**Purpose**: Technical capabilities and external integrations + +**Components**: + +- **Infrastructure.Repository**: Data access via stored procedures +- **Infrastructure.Jwt**: JWT token generation and validation +- **Infrastructure.PasswordHashing**: Argon2id password hashing +- **Infrastructure.Email**: Email sending capabilities +- **Infrastructure.Email.Templates**: Email template rendering + +**Dependencies**: + +- Domain entities +- External libraries (ADO.NET, JWT, Argon2, etc.) + +**Rules**: + +- Implements technical concerns +- No business logic +- Reusable across services + +#### Domain Layer (`Domain.Entities`) + +**Purpose**: Core business entities and models + +**Components**: + +- `UserAccount` - User profile data +- `UserCredential` - Authentication credentials +- `UserVerification` - Account verification state + +**Dependencies**: + +- None (pure domain) + +**Rules**: + +- Plain Old CLR Objects (POCOs) +- No framework dependencies +- No infrastructure references +- Represents business concepts + +### Design Patterns + +#### Repository Pattern + +**Purpose**: Abstract database access behind interfaces + +**Implementation**: + +- `IAuthRepository` - Authentication queries +- `IUserAccountRepository` - User account queries +- `DefaultSqlConnectionFactory` - Connection management + +**Benefits**: + +- Testable (easy to mock) +- SQL-first approach (stored procedures) +- Centralized data access logic + +**Example**: + +```csharp +public interface IAuthRepository +{ + Task GetUserCredentialAsync(string username); + Task CreateUserAccountAsync(UserAccount user, UserCredential credential); +} +``` + +#### Dependency Injection + +**Purpose**: Loose coupling and testability + +**Configuration**: `Program.cs` registers all services + +**Lifetimes**: + +- Scoped: Repositories, Services (per request) +- Singleton: Connection factories, JWT configuration +- Transient: Utilities, helpers + +#### SQL-First Approach + +**Purpose**: Leverage database capabilities + +**Strategy**: + +- All queries via stored procedures +- No ORM (Entity Framework not used) +- Database handles complex logic +- Application focuses on orchestration + +**Stored Procedure Examples**: + +- `USP_RegisterUser` - User registration +- `USP_GetUserAccountByUsername` - User lookup +- `USP_RotateUserCredential` - Password update + +## Frontend Architecture + +### Next.js Application Structure + +``` +Website/src/ +├── components/ # React components +├── pages/ # Next.js routes +├── contexts/ # React context providers +├── hooks/ # Custom React hooks +├── controllers/ # Business logic layer +├── services/ # API communication +├── requests/ # API request builders +├── validation/ # Form validation schemas +├── config/ # Configuration & env vars +└── prisma/ # Database schema (current) +``` + +### Migration Strategy + +The frontend is **transitioning** from a standalone architecture to integrate with the +.NET API: + +**Current State**: + +- Uses Prisma ORM with Postgres (Neon) +- Has its own server-side API routes +- Direct database access from Next.js + +**Target State**: + +- Pure client-side Next.js app +- All data via .NET API +- No server-side database access +- JWT-based authentication + +## Security Architecture + +### Authentication Flow + +1. **Registration**: + - User submits credentials + - Password hashed with Argon2id + - User account created + - JWT token issued + +2. **Login**: + - User submits credentials + - Password verified against hash + - JWT token issued + - Token stored client-side + +3. **API Requests**: + - Client sends JWT in Authorization header + - Middleware validates token + - Request proceeds if valid + +### Password Security + +**Algorithm**: Argon2id + +- Memory: 64MB +- Iterations: 4 +- Parallelism: CPU core count +- Salt: 128-bit (16 bytes) +- Hash: 256-bit (32 bytes) + +### JWT Tokens + +**Algorithm**: HS256 (HMAC-SHA256) + +**Claims**: + +- `sub` - User ID +- `unique_name` - Username +- `jti` - Unique token ID +- `iat` - Issued at timestamp +- `exp` - Expiration timestamp + +**Configuration** (appsettings.json): + +```json +{ + "Jwt": { + "ExpirationMinutes": 60, + "Issuer": "biergarten-api", + "Audience": "biergarten-users" + } +} +``` + +## Database Architecture + +### SQL-First Philosophy + +**Principles**: + +1. Database is source of truth +2. Complex queries in stored procedures +3. Database handles referential integrity +4. Application orchestrates, database executes + +**Benefits**: + +- Performance optimization via execution plans +- Centralized query logic +- Version-controlled schema (migrations) +- Easier query profiling and tuning + +### Migration Strategy + +**Tool**: DbUp + +**Process**: + +1. Write SQL migration script +2. Embed in `Database.Migrations` project +3. Run migrations on startup +4. Idempotent and versioned + +**Migration Files**: + +``` +scripts/ +├── 001-CreateUserTables.sql +├── 002-CreateLocationTables.sql +├── 003-CreateBreweryTables.sql +└── ... +``` + +### Data Seeding + +**Purpose**: Populate development/test databases + +**Implementation**: `Database.Seed` project + +**Seed Data**: + +- Countries, states/provinces, cities +- Test user accounts +- Sample breweries (future) + +## Deployment Architecture + +### Docker Containerization + +**Container Structure**: + +- `sqlserver` - SQL Server 2022 +- `database.migrations` - Schema migration runner +- `database.seed` - Data seeder +- `api.core` - ASP.NET Core Web API + +**Environments**: + +- Development (`docker-compose.dev.yaml`) +- Testing (`docker-compose.test.yaml`) +- Production (`docker-compose.prod.yaml`) + +For details, see [Docker Guide](docker.md). + +### Health Checks + +**SQL Server**: Validates database connectivity **API**: Checks service health and +dependencies + +**Configuration**: + +```yaml +healthcheck: + test: ['CMD-SHELL', 'sqlcmd health check'] + interval: 10s + retries: 12 + start_period: 30s +``` + +## Testing Architecture + +### Test Pyramid + +``` + ┌──────────────┐ + │ Integration │ ← API.Specs (Reqnroll) + │ Tests │ + ├──────────────┤ + │ Unit Tests │ ← Service.Auth.Tests + │ (Service) │ Repository.Tests + ├──────────────┤ + │ Unit Tests │ + │ (Repository) │ + └──────────────┘ +``` + +**Strategy**: + +- Many unit tests (fast, isolated) +- Fewer integration tests (slower, e2e) +- Mock external dependencies +- Test database for integration tests + +For details, see [Testing Guide](testing.md). diff --git a/docs/diagrams/architecture.puml b/docs/diagrams/architecture.puml new file mode 100644 index 0000000..ebb79db --- /dev/null +++ b/docs/diagrams/architecture.puml @@ -0,0 +1,75 @@ +@startuml architecture +!theme plain +skinparam backgroundColor #FFFFFF +skinparam defaultFontName Arial +skinparam packageStyle rectangle + +title The Biergarten App - Layered Architecture + +package "API Layer" #E3F2FD { + [API.Core\nASP.NET Core Web API] as API + note right of API + - Controllers (Auth, User) + - Swagger/OpenAPI + - Middleware + - Health Checks + end note +} + +package "Service Layer" #F3E5F5 { + [Service.Auth] as AuthSvc + [Service.UserManagement] as UserSvc + note right of AuthSvc + - Business Logic + - Validation + - Orchestration + end note +} + +package "Infrastructure Layer" #FFF3E0 { + [Infrastructure.Repository] as Repo + [Infrastructure.Jwt] as JWT + [Infrastructure.PasswordHashing] as PwdHash + [Infrastructure.Email] as Email +} + +package "Domain Layer" #E8F5E9 { + [Domain.Entities] as Domain + note right of Domain + - UserAccount + - UserCredential + - UserVerification + end note +} + +database "SQL Server" { + [Stored Procedures] as SP + [Tables] as Tables +} + +' Relationships +API --> AuthSvc +API --> UserSvc + +AuthSvc --> Repo +AuthSvc --> JWT +AuthSvc --> PwdHash +AuthSvc --> Email + +UserSvc --> Repo + +Repo --> SP +Repo --> Domain +SP --> Tables + +AuthSvc --> Domain +UserSvc --> Domain + +' Notes +note left of Repo + SQL-first approach + All queries via + stored procedures +end note + +@enduml diff --git a/docs/diagrams/authentication-flow.puml b/docs/diagrams/authentication-flow.puml new file mode 100644 index 0000000..25e6d4a --- /dev/null +++ b/docs/diagrams/authentication-flow.puml @@ -0,0 +1,298 @@ +@startuml authentication-flow +!theme plain +skinparam backgroundColor #FFFFFF +skinparam defaultFontName Arial +skinparam sequenceMessageAlign center +skinparam maxMessageSize 200 + +title User Authentication Flow - Expanded + +actor User +participant "API\nController" as API +box "Service Layer" #LightBlue +participant "RegisterService" as RegSvc +participant "LoginService" as LoginSvc +participant "TokenService" as TokenSvc +participant "EmailService" as EmailSvc +end box +box "Infrastructure Layer" #LightGreen +participant "Argon2\nInfrastructure" as Argon2 +participant "JWT\nInfrastructure" as JWT +participant "Email\nProvider" as SMTP +participant "Template\nProvider" as Template +end box +box "Repository Layer" #LightYellow +participant "AuthRepository" as AuthRepo +participant "UserAccount\nRepository" as UserRepo +end box +database "SQL Server\nStored Procedures" as DB + +== Registration Flow == + +User -> API: POST /api/auth/register\n{username, firstName, lastName,\nemail, dateOfBirth, password} +activate API + +note right of API + FluentValidation runs: + - Username: 3-64 chars, alphanumeric + [._-] + - Email: valid format, max 128 chars + - Password: min 8 chars, uppercase,\n lowercase, number, special char + - DateOfBirth: must be 19+ years old +end note + +API -> API: Validate request\n(FluentValidation) + +alt Validation fails + API -> User: 400 Bad Request\n{errors: {...}} +else Validation succeeds + API -> RegSvc: RegisterAsync(userAccount, password) + activate RegSvc + + RegSvc -> AuthRepo: GetUserByUsernameAsync(username) + activate AuthRepo + AuthRepo -> DB: EXEC usp_GetUserAccountByUsername + activate DB + DB --> AuthRepo: null (user doesn't exist) + deactivate DB + deactivate AuthRepo + + RegSvc -> AuthRepo: GetUserByEmailAsync(email) + activate AuthRepo + AuthRepo -> DB: EXEC usp_GetUserAccountByEmail + activate DB + DB --> AuthRepo: null (email doesn't exist) + deactivate DB + deactivate AuthRepo + + alt User/Email already exists + RegSvc -> API: throw ConflictException + API -> User: 409 Conflict\n"Username or email already exists" + else User doesn't exist + + RegSvc -> Argon2: Hash(password) + activate Argon2 + note right of Argon2 + Argon2id parameters: + - Salt: 16 bytes (128-bit) + - Memory: 64MB + - Iterations: 4 + - Parallelism: CPU count + - Hash output: 32 bytes + end note + Argon2 -> Argon2: Generate random salt\n(16 bytes) + Argon2 -> Argon2: Hash password with\nArgon2id algorithm + Argon2 --> RegSvc: "base64(salt):base64(hash)" + deactivate Argon2 + + RegSvc -> AuthRepo: RegisterUserAsync(\n username, firstName, lastName,\n email, dateOfBirth, hash) + activate AuthRepo + + AuthRepo -> DB: EXEC USP_RegisterUser + activate DB + note right of DB + Transaction begins: + 1. INSERT UserAccount + 2. INSERT UserCredential + (with hashed password) + Transaction commits + end note + DB -> DB: BEGIN TRANSACTION + DB -> DB: INSERT INTO UserAccount\n(Username, FirstName, LastName,\nEmail, DateOfBirth) + DB -> DB: OUTPUT INSERTED.UserAccountID + DB -> DB: INSERT INTO UserCredential\n(UserAccountId, Hash) + DB -> DB: COMMIT TRANSACTION + DB --> AuthRepo: UserAccountId (GUID) + deactivate DB + + AuthRepo --> RegSvc: UserAccount entity + deactivate AuthRepo + + RegSvc -> TokenSvc: GenerateAccessToken(userAccount) + activate TokenSvc + TokenSvc -> JWT: GenerateJwt(userId, username, expiry) + activate JWT + note right of JWT + JWT Configuration: + - Algorithm: HS256 + - Expires: 1 hour + - Claims: + * sub: userId + * unique_name: username + * jti: unique token ID + end note + JWT -> JWT: Create JWT with claims + JWT -> JWT: Sign with secret key + JWT --> TokenSvc: Access Token + deactivate JWT + TokenSvc --> RegSvc: Access Token + deactivate TokenSvc + + RegSvc -> TokenSvc: GenerateRefreshToken(userAccount) + activate TokenSvc + TokenSvc -> JWT: GenerateJwt(userId, username, expiry) + activate JWT + note right of JWT + Refresh Token: + - Expires: 21 days + - Same structure as access token + end note + JWT --> TokenSvc: Refresh Token + deactivate JWT + TokenSvc --> RegSvc: Refresh Token + deactivate TokenSvc + + RegSvc -> EmailSvc: SendRegistrationEmailAsync(\n createdUser, confirmationToken) + activate EmailSvc + + EmailSvc -> Template: RenderUserRegisteredEmailAsync(\n firstName, confirmationLink) + activate Template + note right of Template + Razor Component: + - Header with branding + - Welcome message + - Confirmation button + - Footer + end note + Template -> Template: Render Razor component\nto HTML + Template --> EmailSvc: HTML email content + deactivate Template + + EmailSvc -> SMTP: SendAsync(email, subject, body) + activate SMTP + note right of SMTP + SMTP Configuration: + - Host: from env (SMTP_HOST) + - Port: from env (SMTP_PORT) + - TLS: StartTLS + - Auth: username/password + end note + SMTP -> SMTP: Create MIME message + SMTP -> SMTP: Connect to SMTP server + SMTP -> SMTP: Authenticate + SMTP -> SMTP: Send email + SMTP -> SMTP: Disconnect + SMTP --> EmailSvc: Success / Failure + deactivate SMTP + + alt Email sent successfully + EmailSvc --> RegSvc: emailSent = true + else Email failed + EmailSvc --> RegSvc: emailSent = false\n(error suppressed) + end + deactivate EmailSvc + + RegSvc --> API: RegisterServiceReturn(\n userAccount, accessToken,\n refreshToken, emailSent) + deactivate RegSvc + + API -> API: Create response body + API -> User: 201 Created\n{\n message: "User registered successfully",\n payload: {\n userAccountId, username,\n accessToken, refreshToken,\n confirmationEmailSent\n }\n} + end +end +deactivate API + +== Login Flow == + +User -> API: POST /api/auth/login\n{username, password} +activate API + +API -> API: Validate request\n(FluentValidation) + +alt Validation fails + API -> User: 400 Bad Request\n{errors: {...}} +else Validation succeeds + + API -> LoginSvc: LoginAsync(username, password) + activate LoginSvc + + LoginSvc -> AuthRepo: GetUserByUsernameAsync(username) + activate AuthRepo + AuthRepo -> DB: EXEC usp_GetUserAccountByUsername + activate DB + DB -> DB: SELECT FROM UserAccount\nWHERE Username = @Username + DB --> AuthRepo: UserAccount entity + deactivate DB + deactivate AuthRepo + + alt User not found + LoginSvc -> API: throw UnauthorizedException\n"Invalid username or password" + API -> User: 401 Unauthorized + else User found + + LoginSvc -> AuthRepo: GetActiveCredentialByUserAccountIdAsync(userId) + activate AuthRepo + AuthRepo -> DB: EXEC USP_GetActiveUserCredentialByUserAccountId + activate DB + note right of DB + SELECT FROM UserCredential + WHERE UserAccountId = @UserAccountId + AND IsRevoked = 0 + end note + DB --> AuthRepo: UserCredential entity + deactivate DB + deactivate AuthRepo + + alt No active credential + LoginSvc -> API: throw UnauthorizedException + API -> User: 401 Unauthorized + else Active credential found + + LoginSvc -> Argon2: Verify(password, storedHash) + activate Argon2 + note right of Argon2 + 1. Split stored hash: "salt:hash" + 2. Extract salt + 3. Hash provided password\n with same salt + 4. Constant-time comparison + end note + Argon2 -> Argon2: Parse salt from stored hash + Argon2 -> Argon2: Hash provided password\nwith extracted salt + Argon2 -> Argon2: FixedTimeEquals(\n computed, stored) + Argon2 --> LoginSvc: true/false + deactivate Argon2 + + alt Password invalid + LoginSvc -> API: throw UnauthorizedException + API -> User: 401 Unauthorized + else Password valid + + LoginSvc -> TokenSvc: GenerateAccessToken(user) + activate TokenSvc + TokenSvc -> JWT: GenerateJwt(...) + activate JWT + JWT --> TokenSvc: Access Token + deactivate JWT + TokenSvc --> LoginSvc: Access Token + deactivate TokenSvc + + LoginSvc -> TokenSvc: GenerateRefreshToken(user) + activate TokenSvc + TokenSvc -> JWT: GenerateJwt(...) + activate JWT + JWT --> TokenSvc: Refresh Token + deactivate JWT + TokenSvc --> LoginSvc: Refresh Token + deactivate TokenSvc + + LoginSvc --> API: LoginServiceReturn(\n userAccount, accessToken,\n refreshToken) + deactivate LoginSvc + + API -> User: 200 OK\n{\n message: "Logged in successfully",\n payload: {\n userAccountId, username,\n accessToken, refreshToken\n }\n} + end + end + end +end +deactivate API + +== Error Handling (Global Exception Filter) == + +note over API + GlobalExceptionFilter catches: + - ValidationException → 400 Bad Request + - ConflictException → 409 Conflict + - NotFoundException → 404 Not Found + - UnauthorizedException → 401 Unauthorized + - ForbiddenException → 403 Forbidden + - All others → 500 Internal Server Error +end note + +@enduml diff --git a/docs/diagrams/class-diagram.puml b/docs/diagrams/class-diagram.puml new file mode 100644 index 0000000..6817eaf --- /dev/null +++ b/docs/diagrams/class-diagram.puml @@ -0,0 +1,523 @@ +@startuml class-diagram +!theme plain +skinparam backgroundColor #FFFFFF +skinparam defaultFontName Arial +skinparam classAttributeIconSize 0 +skinparam linetype ortho + +title Biergarten Application - Class Diagram + +' API Layer +package "API.Core" <> #E3F2FD { + + class AuthController { + - IRegisterService _registerService + - ILoginService _loginService + + <> Task Register(RegisterRequest) + + <> Task Login(LoginRequest) + } + + class UserController { + - IUserService _userService + + <> Task>> GetAll(int?, int?) + + <> Task> GetById(Guid) + } + + class GlobalExceptionFilter { + - ILogger _logger + + void OnException(ExceptionContext) + } + + package "Contracts" { + class RegisterRequest <> { + + string Username + + string FirstName + + string LastName + + string Email + + DateTime DateOfBirth + + string Password + } + + class LoginRequest <> { + + string Username + + string Password + } + + class RegisterRequestValidator { + + RegisterRequestValidator() + } + + class LoginRequestValidator { + + LoginRequestValidator() + } + + class "ResponseBody" <> { + + string Message + + T Payload + } + + class LoginPayload <> { + + Guid UserAccountId + + string Username + + string RefreshToken + + string AccessToken + } + + class RegistrationPayload <> { + + Guid UserAccountId + + string Username + + string RefreshToken + + string AccessToken + + bool ConfirmationEmailSent + } + } +} + +' Service Layer +package "Service Layer" <> #C8E6C9 { + + package "Service.Auth" { + interface IRegisterService { + + <> Task RegisterAsync(UserAccount, string) + } + + class RegisterService { + - IAuthRepository _authRepo + - IPasswordInfrastructure _passwordInfra + - ITokenService _tokenService + - IEmailService _emailService + + <> Task RegisterAsync(UserAccount, string) + - <> Task ValidateUserDoesNotExist(UserAccount) + } + + interface ILoginService { + + <> Task LoginAsync(string, string) + } + + class LoginService { + - IAuthRepository _authRepo + - IPasswordInfrastructure _passwordInfra + - ITokenService _tokenService + + <> Task LoginAsync(string, string) + } + + interface ITokenService { + + string GenerateAccessToken(UserAccount) + + string GenerateRefreshToken(UserAccount) + } + + class TokenService { + - ITokenInfrastructure _tokenInfrastructure + + string GenerateAccessToken(UserAccount) + + string GenerateRefreshToken(UserAccount) + } + + class RegisterServiceReturn <> { + + bool IsAuthenticated + + bool EmailSent + + UserAccount UserAccount + + string AccessToken + + string RefreshToken + } + + class LoginServiceReturn <> { + + UserAccount UserAccount + + string RefreshToken + + string AccessToken + } + } + + package "Service.UserManagement" { + interface IUserService { + + <> Task> GetAllAsync(int?, int?) + + <> Task GetByIdAsync(Guid) + + <> Task UpdateAsync(UserAccount) + } + + class UserService { + - IUserAccountRepository _repository + + <> Task> GetAllAsync(int?, int?) + + <> Task GetByIdAsync(Guid) + + <> Task UpdateAsync(UserAccount) + } + } + + package "Service.Emails" { + interface IEmailService { + + <> Task SendRegistrationEmailAsync(UserAccount, string) + } + + class EmailService { + - IEmailProvider _emailProvider + - IEmailTemplateProvider _templateProvider + + <> Task SendRegistrationEmailAsync(UserAccount, string) + } + } +} + +' Domain Layer +package "Domain" <> #FFF9C4 { + + package "Domain.Entities" { + class UserAccount { + + Guid UserAccountId + + string Username + + string FirstName + + string LastName + + string Email + + DateTime CreatedAt + + DateTime? UpdatedAt + + DateTime DateOfBirth + + byte[]? Timer + } + + class UserCredential { + + Guid UserCredentialId + + Guid UserAccountId + + DateTime CreatedAt + + DateTime Expiry + + string Hash + + byte[]? Timer + } + + class UserVerification { + + Guid UserVerificationId + + Guid UserAccountId + + DateTime VerificationDateTime + + byte[]? Timer + } + } + + package "Domain.Exceptions" { + class ConflictException { + + ConflictException(string) + } + + class NotFoundException { + + NotFoundException(string) + } + + class UnauthorizedException { + + UnauthorizedException(string) + } + + class ForbiddenException { + + ForbiddenException(string) + } + + class ValidationException { + + ValidationException(string) + } + } +} + +' Infrastructure Layer +package "Infrastructure" <> #E1BEE7 { + + package "Infrastructure.Repository" { + interface IAuthRepository { + + <> Task RegisterUserAsync(string, string, string, string, DateTime, string) + + <> Task GetUserByEmailAsync(string) + + <> Task GetUserByUsernameAsync(string) + + <> Task GetActiveCredentialByUserAccountIdAsync(Guid) + + <> Task RotateCredentialAsync(Guid, string) + } + + class AuthRepository { + - ISqlConnectionFactory _connectionFactory + + <> Task RegisterUserAsync(...) + + <> Task GetUserByEmailAsync(string) + + <> Task GetUserByUsernameAsync(string) + + <> Task GetActiveCredentialByUserAccountIdAsync(Guid) + + <> Task RotateCredentialAsync(Guid, string) + # UserAccount MapToEntity(DbDataReader) + - UserCredential MapToCredentialEntity(DbDataReader) + } + + interface IUserAccountRepository { + + <> Task GetByIdAsync(Guid) + + <> Task> GetAllAsync(int?, int?) + + <> Task UpdateAsync(UserAccount) + + <> Task DeleteAsync(Guid) + + <> Task GetByUsernameAsync(string) + + <> Task GetByEmailAsync(string) + } + + class UserAccountRepository { + - ISqlConnectionFactory _connectionFactory + + <> Task GetByIdAsync(Guid) + + <> Task> GetAllAsync(int?, int?) + + <> Task UpdateAsync(UserAccount) + + <> Task DeleteAsync(Guid) + + <> Task GetByUsernameAsync(string) + + <> Task GetByEmailAsync(string) + # UserAccount MapToEntity(DbDataReader) + } + + abstract class "Repository" { + # ISqlConnectionFactory _connectionFactory + # <> Task CreateConnection() + # {abstract} T MapToEntity(DbDataReader) + } + + package "Sql" { + interface ISqlConnectionFactory { + + DbConnection CreateConnection() + } + + class DefaultSqlConnectionFactory { + - string _connectionString + + DbConnection CreateConnection() + } + + class SqlConnectionStringHelper <> { + + {static} string BuildConnectionString(string?) + + {static} string BuildMasterConnectionString() + } + } + } + + package "Infrastructure.PasswordHashing" { + interface IPasswordInfrastructure { + + string Hash(string) + + bool Verify(string, string) + } + + class Argon2Infrastructure { + - {static} int SaltSize = 16 + - {static} int HashSize = 32 + - {static} int ArgonIterations = 4 + - {static} int ArgonMemoryKb = 65536 + + string Hash(string) + + bool Verify(string, string) + } + } + + package "Infrastructure.Jwt" { + interface ITokenInfrastructure { + + string GenerateJwt(Guid, string, DateTime) + } + + class JwtInfrastructure { + - string? _secret + + string GenerateJwt(Guid, string, DateTime) + } + } + + package "Infrastructure.Email" { + interface IEmailProvider { + + <> Task SendAsync(string, string, string, bool) + + <> Task SendAsync(IEnumerable, string, string, bool) + } + + class SmtpEmailProvider { + - string _host + - int _port + - string? _username + - string? _password + - bool _useSsl + - string _fromEmail + - string _fromName + + <> Task SendAsync(string, string, string, bool) + + <> Task SendAsync(IEnumerable, string, string, bool) + } + } + + package "Infrastructure.Email.Templates" { + interface IEmailTemplateProvider { + + <> Task RenderUserRegisteredEmailAsync(string, string) + } + + class EmailTemplateProvider { + - IServiceProvider _serviceProvider + - ILoggerFactory _loggerFactory + + <> Task RenderUserRegisteredEmailAsync(string, string) + - <> Task RenderComponentAsync(Dictionary) + } + + class "UserRegistration <>" { + + string Username + + string ConfirmationLink + } + + class "Header <>" { + } + + class "Footer <>" { + + string? FooterText + } + } +} + +' Database Layer +package "Database" <> #FFCCBC { + + class "SQL Server" <> { + .. Tables .. + UserAccount + UserCredential + UserVerification + UserAvatar + Photo + UserFollow + Country + StateProvince + City + BreweryPost + BreweryPostLocation + BreweryPostPhoto + BeerStyle + BeerPost + BeerPostPhoto + BeerPostComment + .. Stored Procedures .. + USP_RegisterUser + usp_GetUserAccountByUsername + usp_GetUserAccountByEmail + usp_GetUserAccountById + USP_GetActiveUserCredentialByUserAccountId + USP_RotateUserCredential + USP_CreateUserVerification + } +} + +' Relationships - API to Service +AuthController ..> IRegisterService : uses +AuthController ..> ILoginService : uses +UserController ..> IUserService : uses + +AuthController ..> RegisterRequest : receives +AuthController ..> LoginRequest : receives +AuthController ..> "ResponseBody" : returns +AuthController ..> RegistrationPayload : returns +AuthController ..> LoginPayload : returns + +RegisterRequest ..> RegisterRequestValidator : validated by +LoginRequest ..> LoginRequestValidator : validated by + +' Relationships - Service Layer +IRegisterService <|.. RegisterService : implements +ILoginService <|.. LoginService : implements +ITokenService <|.. TokenService : implements +IUserService <|.. UserService : implements +IEmailService <|.. EmailService : implements + +RegisterService ..> IAuthRepository : uses +RegisterService ..> IPasswordInfrastructure : uses +RegisterService ..> ITokenService : uses +RegisterService ..> IEmailService : uses +RegisterService ..> RegisterServiceReturn : returns +RegisterService ..> UserAccount : uses + +LoginService ..> IAuthRepository : uses +LoginService ..> IPasswordInfrastructure : uses +LoginService ..> ITokenService : uses +LoginService ..> LoginServiceReturn : returns +LoginService ..> UserAccount : uses +LoginService ..> UserCredential : uses + +TokenService ..> ITokenInfrastructure : uses +TokenService ..> UserAccount : uses + +UserService ..> IUserAccountRepository : uses +UserService ..> UserAccount : uses + +EmailService ..> IEmailProvider : uses +EmailService ..> IEmailTemplateProvider : uses +EmailService ..> UserAccount : uses + +' Relationships - Repository Layer +IAuthRepository <|.. AuthRepository : implements +IUserAccountRepository <|.. UserAccountRepository : implements +"Repository" <|-- AuthRepository : extends +"Repository" <|-- UserAccountRepository : extends + +AuthRepository ..> ISqlConnectionFactory : uses +AuthRepository ..> UserAccount : returns +AuthRepository ..> UserCredential : returns +AuthRepository ..> "SQL Server" : queries + +UserAccountRepository ..> ISqlConnectionFactory : uses +UserAccountRepository ..> UserAccount : returns +UserAccountRepository ..> "SQL Server" : queries + +"Repository" ..> ISqlConnectionFactory : uses + +ISqlConnectionFactory <|.. DefaultSqlConnectionFactory : implements +DefaultSqlConnectionFactory ..> SqlConnectionStringHelper : uses + +' Relationships - Infrastructure +IPasswordInfrastructure <|.. Argon2Infrastructure : implements +ITokenInfrastructure <|.. JwtInfrastructure : implements +IEmailProvider <|.. SmtpEmailProvider : implements +IEmailTemplateProvider <|.. EmailTemplateProvider : implements + +EmailTemplateProvider ..> "UserRegistration <>" : renders +"UserRegistration <>" ..> "Header <>" : includes +"UserRegistration <>" ..> "Footer <>" : includes + +' Relationships - Domain +UserAccount -- UserCredential : "1" -- "*" +UserAccount -- UserVerification : "1" -- "0..1" + +' Exception handling +GlobalExceptionFilter ..> ConflictException : catches +GlobalExceptionFilter ..> NotFoundException : catches +GlobalExceptionFilter ..> UnauthorizedException : catches +GlobalExceptionFilter ..> ForbiddenException : catches +GlobalExceptionFilter ..> ValidationException : catches + +RegisterService ..> ConflictException : throws +LoginService ..> UnauthorizedException : throws +UserService ..> NotFoundException : throws + +' Notes +note right of Argon2Infrastructure + Security Parameters: + - Memory: 64MB + - Iterations: 4 + - Salt: 16 bytes + - Output: 32 bytes +end note + +note right of JwtInfrastructure + JWT Configuration: + - Algorithm: HS256 + - Access: 1 hour + - Refresh: 21 days +end note + +note right of "SQL Server" + Stored Procedures: + - USP_RegisterUser: Transaction + creates UserAccount + + UserCredential + - Credentials tracked with + IsRevoked flag +end note + +note right of AuthRepository + Uses ADO.NET with + parameterized queries + to prevent SQL injection +end note + +note bottom of RegisterService + Registration Flow: + 1. Validate user doesn't exist + 2. Hash password (Argon2) + 3. Create account + credential + 4. Generate tokens + 5. Send confirmation email +end note + +note bottom of LoginService + Login Flow: + 1. Find user by username + 2. Get active credential + 3. Verify password + 4. Generate tokens + 5. Return authenticated user +end note + +@enduml diff --git a/docs/diagrams/database-schema.puml b/docs/diagrams/database-schema.puml new file mode 100644 index 0000000..9c75257 --- /dev/null +++ b/docs/diagrams/database-schema.puml @@ -0,0 +1,104 @@ +@startuml database-schema +!theme plain +skinparam backgroundColor #FFFFFF +skinparam defaultFontName Arial +skinparam linetype ortho + +title Key Database Schema - User & Authentication + +entity "UserAccount" as User { + * UserAccountId: INT <> + -- + * Username: NVARCHAR(30) <> + * Email: NVARCHAR(255) <> + * FirstName: NVARCHAR(50) + * LastName: NVARCHAR(50) + Bio: NVARCHAR(500) + CreatedAt: DATETIME2 + UpdatedAt: DATETIME2 + LastLoginAt: DATETIME2 +} + +entity "UserCredential" as Cred { + * UserCredentialId: INT <> + -- + * UserAccountId: INT <> + * PasswordHash: VARBINARY(32) + * PasswordSalt: VARBINARY(16) + CredentialRotatedAt: DATETIME2 + CredentialExpiresAt: DATETIME2 + CredentialRevokedAt: DATETIME2 + * IsActive: BIT + CreatedAt: DATETIME2 +} + +entity "UserVerification" as Verify { + * UserVerificationId: INT <> + -- + * UserAccountId: INT <> + * IsVerified: BIT + VerifiedAt: DATETIME2 + VerificationToken: NVARCHAR(255) + TokenExpiresAt: DATETIME2 +} + +entity "UserAvatar" as Avatar { + * UserAvatarId: INT <> + -- + * UserAccountId: INT <> + PhotoId: INT <> + * IsActive: BIT + CreatedAt: DATETIME2 +} + +entity "UserFollow" as Follow { + * UserFollowId: INT <> + -- + * FollowerUserId: INT <> + * FollowedUserId: INT <> + CreatedAt: DATETIME2 +} + +entity "Photo" as Photo { + * PhotoId: INT <> + -- + * Url: NVARCHAR(500) + * CloudinaryPublicId: NVARCHAR(255) + Width: INT + Height: INT + Format: NVARCHAR(10) + CreatedAt: DATETIME2 +} + +' Relationships +User ||--o{ Cred : "has" +User ||--o| Verify : "has" +User ||--o{ Avatar : "has" +User ||--o{ Follow : "follows" +User ||--o{ Follow : "followed by" +Avatar }o--|| Photo : "refers to" + +note right of Cred + Password hashing: + - Algorithm: Argon2id + - Memory: 64MB + - Iterations: 4 + - Salt: 128-bit + - Hash: 256-bit +end note + +note right of Verify + Account verification + via email token + with expiry +end note + +note bottom of User + Core stored procedures: + - USP_RegisterUser + - USP_GetUserAccountByUsername + - USP_RotateUserCredential + - USP_UpdateUserAccount +end note + +@enduml diff --git a/docs/diagrams/deployment.puml b/docs/diagrams/deployment.puml new file mode 100644 index 0000000..2d6db2a --- /dev/null +++ b/docs/diagrams/deployment.puml @@ -0,0 +1,227 @@ +@startuml deployment +!theme plain +skinparam backgroundColor #FFFFFF +skinparam defaultFontName Arial +skinparam linetype ortho + +title Docker Deployment Architecture + +' External systems +actor Developer +cloud "Docker Host" as Host + +package "Development Environment\n(docker-compose.dev.yaml)" #E3F2FD { + + node "SQL Server\n(mcr.microsoft.com/mssql/server:2022-latest)" as DevDB { + database "Biergarten\nDatabase" as DevDBInner { + portin "1433" + } + note right + Environment: + - ACCEPT_EULA=Y + - SA_PASSWORD=*** + - MSSQL_PID=Developer + + Volumes: + - biergarten-dev-data + end note + } + + node "API Container\n(API.Core)" as DevAPI { + component "ASP.NET Core 10" as API1 + portin "8080:8080 (HTTP)" as DevPort1 + portin "8081:8081 (HTTPS)" as DevPort2 + + note right + Environment: + - ASPNETCORE_ENVIRONMENT=Development + - DB_SERVER=sql-server + - DB_NAME=Biergarten + - DB_USER/PASSWORD + - JWT_SECRET + - SMTP_* (10+ variables) + + Health Check: + /health endpoint + end note + } + + node "Migrations\n(run-once)" as DevMig { + component "Database.Migrations" as Mig1 + note bottom + Runs: DbUp migrations + Environment: + - CLEAR_DATABASE=false + Depends on: sql-server + end note + } + + node "Seed\n(run-once)" as DevSeed { + component "Database.Seed" as Seed1 + note bottom + Creates: + - 100 test users + - Location data (US/CA/MX) + - test.user account + Depends on: migrations + end note + } + +} + +package "Test Environment\n(docker-compose.test.yaml)" #FFF3E0 { + + node "SQL Server\n(isolated instance)" as TestDB { + database "Biergarten\nTest Database" as TestDBInner { + portin "1434" + } + note right + Fresh instance each run + CLEAR_DATABASE=true + + Volumes: + - biergarten-test-data + (ephemeral) + end note + } + + node "Migrations\n(test)" as TestMig { + component "Database.Migrations" + } + + node "Seed\n(test)" as TestSeed { + component "Database.Seed" + note bottom + Minimal seed: + - test.user only + - Essential data + end note + } + + node "API.Specs\n(Integration Tests)" as Specs { + component "Reqnroll + xUnit" as SpecsComp + note right + Tests: + - Registration flow + - Login flow + - Validation rules + - 404 handling + + Uses: TestApiFactory + Mocks: Email services + end note + } + + node "Infrastructure.Repository.Tests\n(Unit Tests)" as RepoTests { + component "xUnit + DbMocker" as RepoComp + note right + Tests: + - AuthRepository + - UserAccountRepository + - SQL command building + + Uses: Mock connections + No real database needed + end note + } + + node "Service.Auth.Tests\n(Unit Tests)" as SvcTests { + component "xUnit + Moq" as SvcComp + note right + Tests: + - RegisterService + - LoginService + - Token generation + + Uses: Mocked dependencies + No database or infrastructure + end note + } + +} + +folder "test-results/\n(mounted volume)" as Results { + file "api-specs/\n results.trx" as Result1 + file "repository-tests/\n results.trx" as Result2 + file "service-auth-tests/\n results.trx" as Result3 + + note bottom + TRX format + Readable by: + - Visual Studio + - Azure DevOps + - GitHub Actions + end note +} + +' External access +Developer --> Host : docker compose up +Host --> DevAPI : http://localhost:8080 + +' Development dependencies +DevMig --> DevDB : 1. Run migrations +DevSeed --> DevDB : 2. Seed data +DevAPI --> DevDB : 3. Connect & serve +DevMig .up.> DevDB : depends_on +DevSeed .up.> DevMig : depends_on +DevAPI .up.> DevSeed : depends_on + +' Test dependencies +TestMig --> TestDB : 1. Migrate +TestSeed --> TestDB : 2. Seed +Specs --> TestDB : 3. Integration test +RepoTests ..> TestDB : Mock (no connection) +SvcTests ..> TestDB : Mock (no connection) + +TestMig .up.> TestDB : depends_on +TestSeed .up.> TestMig : depends_on +Specs .up.> TestSeed : depends_on + +' Test results export +Specs --> Results : Export TRX +RepoTests --> Results : Export TRX +SvcTests --> Results : Export TRX + +' Network notes +note bottom of DevDB + Dev Network (bridge: biergarten-dev) + Internal DNS: + - sql-server (resolves to SQL container) + - api (resolves to API container) +end note + +note bottom of TestDB + Test Network (bridge: biergarten-test) + All test components isolated +end note + +' Startup sequence notes +note top of DevMig + Startup Order: + 1. SQL Server (health check) + 2. Migrations (run-once) + 3. Seed (run-once) + 4. API (long-running) +end note + +note top of Specs + Test Execution: + All tests run in parallel + Results aggregated +end note + +' Production note +note as ProductionNote + Production Deployment (not shown): + + Would include: + • Azure SQL Database / AWS RDS + • Azure Container Apps / ECS + • Azure Key Vault for secrets + • Application Insights / CloudWatch + • Load balancer + • HTTPS termination + • CDN for static assets +end note + +@enduml diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 0000000..21cb6af --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,327 @@ +# Docker Guide + +This document covers Docker deployment, configuration, and troubleshooting for The +Biergarten App. + +## Overview + +The project uses Docker Compose to orchestrate multiple services: + +- SQL Server 2022 database +- Database migrations runner (DbUp) +- Database seeder +- .NET API +- Test runners + +See the [deployment diagram](diagrams/pdf/deployment.pdf) for visual representation. + +## Docker Compose Environments + +### 1. Development (`docker-compose.dev.yaml`) + +**Purpose**: Local development with persistent data + +**Features**: + +- Persistent SQL Server volume +- Hot reload support +- Swagger UI enabled +- Seed data included +- `CLEAR_DATABASE=true` (drops and recreates schema) + +**Services**: + +```yaml +sqlserver # SQL Server 2022 (port 1433) +database.migrations # DbUp migrations +database.seed # Seed initial data +api.core # Web API (ports 8080, 8081) +``` + +**Start Development Environment**: + +```bash +docker compose -f docker-compose.dev.yaml up -d +``` + +**Access**: + +- API Swagger: http://localhost:8080/swagger +- Health Check: http://localhost:8080/health +- SQL Server: localhost:1433 (sa credentials from .env.dev) + +**Stop Environment**: + +```bash +# Stop services (keep volumes) +docker compose -f docker-compose.dev.yaml down + +# Stop and remove volumes (fresh start) +docker compose -f docker-compose.dev.yaml down -v +``` + +### 2. Testing (`docker-compose.test.yaml`) + +**Purpose**: Automated CI/CD testing in isolated environment + +**Features**: + +- Fresh database each run +- All test suites execute in parallel +- Test results exported to `./test-results/` +- Containers auto-exit after completion +- Fully isolated testnet network + +**Services**: + +```yaml +sqlserver # Test database +database.migrations # Fresh schema +database.seed # Test data +api.specs # Reqnroll BDD tests +repository.tests # Repository unit tests +service.auth.tests # Service unit tests +``` + +**Run Tests**: + +```bash +# Run all tests +docker compose -f docker-compose.test.yaml up --abort-on-container-exit + +# View results +ls -la test-results/ +cat test-results/api-specs/results.trx +cat test-results/repository-tests/results.trx +cat test-results/service-auth-tests/results.trx + +# Clean up +docker compose -f docker-compose.test.yaml down -v +``` + +### 3. Production (`docker-compose.prod.yaml`) + +**Purpose**: Production-ready deployment + +**Features**: + +- Production logging levels +- No database clearing +- Optimized build configurations +- Health checks enabled +- Restart policies (unless-stopped) +- Security hardening + +**Services**: + +```yaml +sqlserver # Production SQL Server +database.migrations # Schema updates only +api.core # Production API +``` + +**Deploy Production**: + +```bash +docker compose -f docker-compose.prod.yaml up -d +``` + +## Service Dependencies + +Docker Compose manages startup order using health checks: + +```mermaid +sqlserver (health check) + ↓ +database.migrations (completes successfully) + ↓ +database.seed (completes successfully) + ↓ +api.core / tests (start when ready) +``` + +**Health Check Example** (SQL Server): + +```yaml +healthcheck: + test: ['CMD-SHELL', "sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1'"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 30s +``` + +**Dependency Configuration**: + +```yaml +api.core: + depends_on: + database.seed: + condition: service_completed_successfully +``` + +## Volumes + +### Persistent Volumes + +**Development**: + +- `sqlserverdata-dev` - Database files persist between restarts +- `nuget-cache-dev` - NuGet package cache (speeds up builds) + +**Testing**: + +- `sqlserverdata-test` - Temporary, typically removed after tests + +**Production**: + +- `sqlserverdata-prod` - Production database files +- `nuget-cache-prod` - Production NuGet cache + +### Mounted Volumes + +**Test Results**: + +```yaml +volumes: + - ./test-results:/app/test-results +``` + +Test results are written to host filesystem for CI/CD integration. + +**Code Volumes** (development only): + +```yaml +volumes: + - ./src:/app/src # Hot reload for development +``` + +## Networks + +Each environment uses isolated bridge networks: + +- `devnet` - Development network +- `testnet` - Testing network (fully isolated) +- `prodnet` - Production network + +## Environment Variables + +All containers are configured via environment variables from `.env` files: + +```yaml +env_file: '.env.dev' # or .env.test, .env.prod + +environment: + ASPNETCORE_ENVIRONMENT: 'Development' + DOTNET_RUNNING_IN_CONTAINER: 'true' + DB_SERVER: '${DB_SERVER}' + DB_NAME: '${DB_NAME}' + DB_USER: '${DB_USER}' + DB_PASSWORD: '${DB_PASSWORD}' + JWT_SECRET: '${JWT_SECRET}' +``` + +For complete list, see [Environment Variables](environment-variables.md). + +## Common Commands + +### View Services + +```bash +# Running services +docker compose -f docker-compose.dev.yaml ps + +# All containers (including stopped) +docker ps -a +``` + +### View Logs + +```bash +# All services +docker compose -f docker-compose.dev.yaml logs -f + +# Specific service +docker compose -f docker-compose.dev.yaml logs -f api.core + +# Last 100 lines +docker compose -f docker-compose.dev.yaml logs --tail=100 api.core +``` + +### Execute Commands in Container + +```bash +# Interactive shell +docker exec -it dev-env-api-core bash + +# Run command +docker exec dev-env-sqlserver /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'password' -C +``` + +### Restart Services + +```bash +# Restart all services +docker compose -f docker-compose.dev.yaml restart + +# Restart specific service +docker compose -f docker-compose.dev.yaml restart api.core + +# Rebuild and restart +docker compose -f docker-compose.dev.yaml up -d --build api.core +``` + +### Build Images + +```bash +# Build all images +docker compose -f docker-compose.dev.yaml build + +# Build specific service +docker compose -f docker-compose.dev.yaml build api.core + +# Build without cache +docker compose -f docker-compose.dev.yaml build --no-cache +``` + +### Clean Up + +```bash +# Stop and remove containers +docker compose -f docker-compose.dev.yaml down + +# Remove containers and volumes +docker compose -f docker-compose.dev.yaml down -v + +# Remove containers, volumes, and images +docker compose -f docker-compose.dev.yaml down -v --rmi all + +# System-wide cleanup +docker system prune -af --volumes +``` + +## Dockerfile Structure + +### Multi-Stage Build + +```dockerfile +# Stage 1: Build +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY ["Project/Project.csproj", "Project/"] +RUN dotnet restore +COPY . . +RUN dotnet build -c Release + +# Stage 2: Runtime +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final +WORKDIR /app +COPY --from=build /app/build . +ENTRYPOINT ["dotnet", "Project.dll"] +``` + +## Additional Resources + +- [Docker Compose Documentation](https://docs.docker.com/compose/) +- [.NET Docker Images](https://hub.docker.com/_/microsoft-dotnet) +- [SQL Server Docker Images](https://hub.docker.com/_/microsoft-mssql-server) diff --git a/docs/environment-variables.md b/docs/environment-variables.md new file mode 100644 index 0000000..1578e4a --- /dev/null +++ b/docs/environment-variables.md @@ -0,0 +1,384 @@ +# Environment Variables + +Complete documentation for all environment variables used in The Biergarten App. + +## Overview + +The application uses environment variables for configuration across: + +- **.NET API Backend** - Database connections, JWT secrets +- **Next.js Frontend** - External services, authentication +- **Docker Containers** - Runtime configuration + +## Configuration Patterns + +### Backend (.NET API) + +Direct environment variable access via `Environment.GetEnvironmentVariable()`. + +### Frontend (Next.js) + +Centralized configuration module at `src/Website/src/config/env/index.ts` with Zod +validation. + +### Docker + +Environment-specific `.env` files loaded via `env_file:` in docker-compose.yaml: + +- `.env.dev` - Development +- `.env.test` - Testing +- `.env.prod` - Production + +## Backend Variables (.NET API) + +### Database Connection + +**Option 1: Component-Based (Recommended for Docker)** + +Build connection string from individual components: + +```bash +DB_SERVER=sqlserver,1433 # SQL Server host 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 complete connection string: + +```bash +DB_CONNECTION_STRING="Server=localhost,1433;Database=Biergarten;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True;" +``` + +**Priority**: `DB_CONNECTION_STRING` is checked first. If not found, connection string is +built from components. + +**Implementation**: See `DefaultSqlConnectionFactory.cs` + +### JWT Authentication + +```bash +JWT_SECRET=your-secret-key-minimum-32-characters-required +``` + +- **Required**: Yes +- **Minimum Length**: 32 characters (enforced) +- **Purpose**: Signs JWT tokens for user authentication +- **Algorithm**: HS256 (HMAC-SHA256) + +**Generate Secret**: + +```bash +# macOS/Linux +openssl rand -base64 127 + +# Windows PowerShell +[Convert]::ToBase64String((1..127 | %{Get-Random -Max 256})) +``` + +**Additional JWT Settings** (appsettings.json): + +```json +{ + "Jwt": { + "ExpirationMinutes": 60, + "Issuer": "biergarten-api", + "Audience": "biergarten-users" + } +} +``` + +### Migration Control + +```bash +CLEAR_DATABASE=true +``` + +- **Required**: No +- **Default**: false +- **Effect**: If "true", drops and recreates database during migrations +- **Usage**: Development and testing environments ONLY +- **Warning**: NEVER use in production + +### ASP.NET Core Configuration + +```bash +ASPNETCORE_ENVIRONMENT=Development # Development, Production, Staging +ASPNETCORE_URLS=http://0.0.0.0:8080 # Binding address and port +DOTNET_RUNNING_IN_CONTAINER=true # Flag for container execution +``` + +## Frontend Variables (Next.js) + +Create `.env.local` in the `Website/` directory. + +### Base Configuration + +```bash +BASE_URL=http://localhost:3000 # Application base URL +NODE_ENV=development # Environment: development, production, test +``` + +### Authentication & Sessions + +```bash +# Token signing secrets (use 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 (optional) +SESSION_MAX_AGE=604800 # Cookie max age in seconds (optional, default: 1 week) +``` + +**Security Requirements**: + +- All secrets should be 127+ characters +- Generate using cryptographically secure random functions +- Never reuse secrets across environments +- Rotate secrets periodically in production + +### Database (Current - Prisma/Postgres) + +**Note**: Frontend currently uses Neon Postgres. Will migrate to .NET API. + +```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 (optional) +``` + +### External Services + +#### 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 Steps**: + +1. Sign up at [cloudinary.com](https://cloudinary.com) +2. Navigate to Dashboard +3. Copy Cloud Name, API Key, and API Secret + +**Note**: `NEXT_PUBLIC_` prefix makes variable accessible in client-side code. + +#### Mapbox (Maps & Geocoding) + +```bash +MAPBOX_ACCESS_TOKEN=pk.your-public-token +``` + +**Setup Steps**: + +1. Create account at [mapbox.com](https://mapbox.com) +2. Navigate to Account → Tokens +3. Create new token with public scopes +4. Copy access token + +#### SparkPost (Email Service) + +```bash +SPARKPOST_API_KEY=your-api-key +SPARKPOST_SENDER_ADDRESS=noreply@yourdomain.com +``` + +**Setup Steps**: + +1. Sign up at [sparkpost.com](https://sparkpost.com) +2. Verify sending domain or use sandbox +3. Create API key with "Send via SMTP" permission +4. Configure sender address (must match verified domain) + +### Admin Account (Seeding) + +```bash +ADMIN_PASSWORD=SecureAdminPassword123! # Initial admin password for seeding +``` + +- **Required**: No (only needed for seeding) +- **Purpose**: Sets admin account password during database seeding +- **Security**: Use strong password, change immediately in production + +## Docker-Specific Variables + +### SQL Server Container + +```bash +SA_PASSWORD=YourStrong!Passw0rd # SQL Server SA password +ACCEPT_EULA=Y # Accept SQL Server EULA (required) +MSSQL_PID=Express # SQL Server edition (Express, Developer, Enterprise) +``` + +**Password Requirements**: + +- Minimum 8 characters +- Uppercase, lowercase, digits, and special characters +- Maps to `DB_PASSWORD` for application containers + +## Environment File Structure + +### Backend/Docker (Root Directory) + +``` +.env.example # Template (tracked in Git) +.env.dev # Development config (gitignored) +.env.test # Testing config (gitignored) +.env.prod # Production config (gitignored) +``` + +**Setup**: + +```bash +cp .env.example .env.dev +# Edit .env.dev with your values +``` + +**Docker Compose Mapping**: + +- `docker-compose.dev.yaml` → `.env.dev` +- `docker-compose.test.yaml` → `.env.test` +- `docker-compose.prod.yaml` → `.env.prod` + +### Frontend (Website Directory) + +``` +.env.local # Local development (gitignored) +.env.production # Production (gitignored) +``` + +**Setup**: + +```bash +cd Website +touch .env.local +# Add frontend variables +``` + +## 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 | +| **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 | Node environment | +| `ASPNETCORE_ENVIRONMENT` | ✓ | | ✓ | Yes | ASP.NET environment | +| `ASPNETCORE_URLS` | ✓ | | ✓ | Yes | API 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 | +| `DOTNET_RUNNING_IN_CONTAINER` | ✓ | | ✓ | No | Container flag | + +\* Either `DB_CONNECTION_STRING` OR the component variables (`DB_SERVER`, `DB_NAME`, +`DB_USER`, `DB_PASSWORD`) must be provided. + +## Validation + +### Backend Validation + +Variables are validated at startup: + +- Missing required variables cause application to fail +- JWT_SECRET length is enforced (min 32 chars) +- Connection string format is validated + +### Frontend Validation + +Zod schemas validate variables at runtime: + +- Type checking (string, number, URL, etc.) +- Format validation (email, URL patterns) +- Required vs optional enforcement + +**Location**: `src/Website/src/config/env/index.ts` + +## Example Configuration Files + +### `.env.dev` (Backend/Docker) + +```bash +# Database +DB_SERVER=sqlserver,1433 +DB_NAME=Biergarten +DB_USER=sa +DB_PASSWORD=Dev_Password_123! + +# JWT +JWT_SECRET=development-secret-key-at-least-32-characters-long-recommended-longer + +# Migration +CLEAR_DATABASE=true + +# ASP.NET Core +ASPNETCORE_ENVIRONMENT=Development +ASPNETCORE_URLS=http://0.0.0.0:8080 + +# SQL Server Container +SA_PASSWORD=Dev_Password_123! +ACCEPT_EULA=Y +MSSQL_PID=Express +``` + +### `.env.local` (Frontend) + +```bash +# Base +BASE_URL=http://localhost:3000 +NODE_ENV=development + +# Authentication +CONFIRMATION_TOKEN_SECRET= +RESET_PASSWORD_TOKEN_SECRET= +SESSION_SECRET= + +# Database (current Prisma setup) +POSTGRES_PRISMA_URL=postgresql://user:pass@db.neon.tech/biergarten?pgbouncer=true +POSTGRES_URL_NON_POOLING=postgresql://user:pass@db.neon.tech/biergarten + +# External Services +NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=my-cloud +CLOUDINARY_KEY=123456789012345 +CLOUDINARY_SECRET=abcdefghijklmnopqrstuvwxyz +MAPBOX_ACCESS_TOKEN=pk.eyJ... +SPARKPOST_API_KEY=abc123... +SPARKPOST_SENDER_ADDRESS=noreply@biergarten.app + +# Admin (for seeding) +ADMIN_PASSWORD=Admin_Dev_Password_123! +``` diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..2333844 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,261 @@ +# Getting Started + +This guide will help you set up and run The Biergarten App in your development +environment. + +## Prerequisites + +Before you begin, ensure you have the following installed: + +- **.NET SDK 10+** - [Download](https://dotnet.microsoft.com/download) +- **Node.js 18+** - [Download](https://nodejs.org/) +- **Docker Desktop** - [Download](https://www.docker.com/products/docker-desktop) + (recommended) +- **Java 8+** - Required for generating diagrams from PlantUML (optional) + +## Quick Start with Docker (Recommended) + +### 1. Clone the Repository + +```bash +git clone +cd the-biergarten-app +``` + +### 2. Configure Environment Variables + +Copy the example environment file: + +```bash +cp .env.example .env.dev +``` + +Edit `.env.dev` with your configuration: + +```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-minimum-32-characters-required +``` + +> For a complete list of environment variables, see +> [Environment Variables](environment-variables.md). + +### 3. Start the Development Environment + +```bash +docker compose -f docker-compose.dev.yaml up -d +``` + +This command will: + +- Start SQL Server container +- Run database migrations +- Seed initial data +- Start the API on http://localhost:8080 + +### 4. Access the API + +- **Swagger UI**: http://localhost:8080/swagger +- **Health Check**: http://localhost:8080/health + +### 5. View Logs + +```bash +# All services +docker compose -f docker-compose.dev.yaml logs -f + +# Specific service +docker compose -f docker-compose.dev.yaml logs -f api.core +``` + +### 6. Stop the Environment + +```bash +docker compose -f docker-compose.dev.yaml down + +# Remove volumes (fresh start) +docker compose -f docker-compose.dev.yaml down -v +``` + +## Manual Setup (Without Docker) + +If you prefer to run services locally without Docker: + +### Backend Setup + +#### 1. Start SQL Server + +You can use a local SQL Server instance or a cloud-hosted one. Ensure it's accessible and +you have the connection details. + +#### 2. Set Environment Variables + +```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-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-minimum-32-characters-required" +``` + +#### 3. Run Database Migrations + +```bash +cd src/Core +dotnet run --project Database/Database.Migrations/Database.Migrations.csproj +``` + +#### 4. Seed the Database + +```bash +dotnet run --project Database/Database.Seed/Database.Seed.csproj +``` + +#### 5. Start the API + +```bash +dotnet run --project API/API.Core/API.Core.csproj +``` + +The API will be available at http://localhost:5000 (or the port specified in +launchSettings.json). + +### Frontend Setup + +> **Note**: The frontend is currently transitioning from its standalone Prisma/Postgres +> backend to the .NET API. Some features may still use the old backend. + +#### 1. Navigate to Website Directory + +```bash +cd Website +``` + +#### 2. Create Environment File + +Create `.env.local` with frontend variables. See +[Environment Variables - Frontend](environment-variables.md#frontend-variables) for the +complete list. + +```bash +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) + +# External services (you'll need to register for these) +NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=your-cloud-name +CLOUDINARY_KEY=your-api-key +CLOUDINARY_SECRET=your-api-secret +NEXT_PUBLIC_MAPBOX_KEY=your-mapbox-token + +# Database URL (current Prisma setup) +DATABASE_URL=your-postgres-connection-string +``` + +#### 3. Install Dependencies + +```bash +npm install +``` + +#### 4. Run Prisma Migrations + +```bash +npx prisma generate +npx prisma migrate dev +``` + +#### 5. Start Development Server + +```bash +npm run dev +``` + +The frontend will be available at http://localhost:3000. + +## Generate Diagrams (Optional) + +The project includes PlantUML diagrams that can be converted to PDF or PNG: + +### Install Java + +Make sure Java 8+ is installed: + +```bash +# Check Java version +java -version +``` + +### Generate Diagrams + +```bash +# Generate all PDFs +make + +# Generate PNGs +make pngs + +# Generate both +make diagrams + +# View help +make help +``` + +Generated diagrams will be in `docs/diagrams/pdf/`. + +## Next Steps + +- **Test the API**: Visit http://localhost:8080/swagger and try the endpoints +- **Run Tests**: See [Testing Guide](testing.md) +- **Learn the Architecture**: Read [Architecture Overview](architecture.md) +- **Understand Docker Setup**: See [Docker Guide](docker.md) +- **Database Details**: Check [Database Schema](database.md) + +## Troubleshooting + +### Port Already in Use + +If port 8080 or 1433 is already in use, you can either: + +- Stop the service using that port +- Change the port mapping in `docker-compose.dev.yaml` + +### Database Connection Issues + +Check that: + +- SQL Server container is running: `docker ps` +- Connection string is correct in `.env.dev` +- Health check is passing: `docker compose -f docker-compose.dev.yaml ps` + +### Container Won't Start + +View container logs: + +```bash +docker compose -f docker-compose.dev.yaml logs +``` + +### Fresh Start + +Remove all containers and volumes: + +```bash +docker compose -f docker-compose.dev.yaml down -v +docker system prune -f +``` + +For more troubleshooting, see the [Docker Guide](docker.md#troubleshooting). diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..81d95c5 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,293 @@ +# Testing + +This document describes the testing strategy and how to run tests for The Biergarten App. + +## Overview + +The project uses a multi-layered testing approach: + +- **API.Specs** - BDD integration tests using Reqnroll (Gherkin) +- **Infrastructure.Repository.Tests** - Unit tests for data access layer +- **Service.Auth.Tests** - Unit tests for authentication business logic + +## Running Tests with Docker (Recommended) + +The easiest way to run all tests is using Docker Compose, which sets up an isolated test +environment: + +```bash +docker compose -f docker-compose.test.yaml up --abort-on-container-exit +``` + +This command: + +1. Starts a fresh SQL Server instance +2. Runs database migrations +3. Seeds test data +4. Executes all test suites in parallel +5. Exports results to `./test-results/` +6. Exits when tests complete + +### View Test Results + +```bash +# List test result files +ls -la test-results/ + +# View specific test results +cat test-results/api-specs/results.trx +cat test-results/repository-tests/results.trx +cat test-results/service-auth-tests/results.trx +``` + +### Clean Up + +```bash +# Remove test containers and volumes +docker compose -f docker-compose.test.yaml down -v +``` + +## Running Tests Locally + +You can run individual test projects locally without Docker: + +### Integration Tests (API.Specs) + +```bash +cd src/Core +dotnet test API/API.Specs/API.Specs.csproj +``` + +**Requirements**: + +- SQL Server instance running +- Database migrated and seeded +- Environment variables set (DB connection, JWT secret) + +### Repository Tests + +```bash +cd src/Core +dotnet test Infrastructure/Infrastructure.Repository.Tests/Infrastructure.Repository.Tests.csproj +``` + +**Requirements**: + +- SQL Server instance running (uses mock data) + +### Service Tests + +```bash +cd src/Core +dotnet test Service/Service.Auth.Tests/Service.Auth.Tests.csproj +``` + +**Requirements**: + +- No database required (uses Moq for mocking) + +## Test Coverage + +### Current Coverage + +**Authentication & User Management**: + +- User registration with validation +- User login with JWT token generation +- Password hashing and verification (Argon2id) +- JWT token generation and claims +- Invalid credentials handling +- 404 error responses + +**Repository Layer**: + +- User account creation +- User credential management +- GetUserByUsername queries +- Stored procedure execution + +**Service Layer**: + +- Login service with password verification +- Register service with validation +- Business logic for authentication flow + +### Planned Coverage + +- [ ] Email verification workflow +- [ ] Password reset functionality +- [ ] Token refresh mechanism +- [ ] Brewery data management +- [ ] Beer post operations +- [ ] User follow/unfollow +- [ ] Image upload service + +## Testing Frameworks & Tools + +### xUnit + +- Primary unit testing framework +- Used for Repository and Service layer tests +- Supports parallel test execution + +### Reqnroll (Gherkin/BDD) + +- Behavior-driven development framework +- Used for API integration tests +- Human-readable test scenarios in `.feature` files + +### FluentAssertions + +- Expressive assertion library +- Makes test assertions more readable +- Used across all test projects + +### Moq + +- Mocking framework for .NET +- Used in Service layer tests +- Enables isolated unit testing + +### DbMocker + +- Database mocking for repository tests +- Simulates SQL Server responses +- No real database required for unit tests + +## Test Structure + +### API.Specs (Integration Tests) + +``` +API.Specs/ +├── Features/ +│ ├── Authentication.feature # Login/register scenarios +│ └── UserManagement.feature # User CRUD scenarios +├── Steps/ +│ ├── AuthenticationSteps.cs # Step definitions +│ └── UserManagementSteps.cs +└── Mocks/ + └── TestApiFactory.cs # Test server setup +``` + +**Example Feature**: + +```gherkin +Feature: User Authentication + As a user + I want to register and login + So that I can access the platform + +Scenario: Successful user registration + Given I have valid registration details + When I register a new account + Then I should receive a JWT token + And my account should be created +``` + +### Infrastructure.Repository.Tests + +``` +Infrastructure.Repository.Tests/ +├── AuthRepositoryTests.cs # Auth repository tests +├── UserAccountRepositoryTests.cs # User account tests +└── TestFixtures/ + └── DatabaseFixture.cs # Shared test setup +``` + +### Service.Auth.Tests + +``` +Service.Auth.Tests/ +├── LoginService.test.cs # Login business logic tests +└── RegisterService.test.cs # Registration business logic tests +``` + +## Writing Tests + +### Unit Test Example (xUnit) + +```csharp +public class LoginServiceTests +{ + [Fact] + public async Task LoginAsync_ValidCredentials_ReturnsToken() + { + // Arrange + var mockRepo = new Mock(); + var mockJwt = new Mock(); + var service = new AuthService(mockRepo.Object, mockJwt.Object); + + // Act + var result = await service.LoginAsync("testuser", "password123"); + + // Assert + result.Should().NotBeNull(); + result.Token.Should().NotBeNullOrEmpty(); + } +} +``` + +### Integration Test Example (Reqnroll) + +```gherkin +Scenario: User login with valid credentials + Given a registered user with username "testuser" + When I POST to "/api/auth/login" with valid credentials + Then the response status should be 200 + And the response should contain a JWT token +``` + +## Continuous Integration + +Tests run automatically in CI/CD pipelines using the test Docker Compose configuration: + +```bash +# CI/CD command +docker compose -f docker-compose.test.yaml build +docker compose -f docker-compose.test.yaml up --abort-on-container-exit +docker compose -f docker-compose.test.yaml down -v +``` + +Exit codes: + +- `0` - All tests passed +- Non-zero - Test failures occurred + +## Troubleshooting + +### Tests Failing Due to Database Connection + +Ensure SQL Server is running and environment variables are set: + +```bash +docker compose -f docker-compose.test.yaml ps +``` + +### Port Conflicts + +If port 1433 is in use, stop other SQL Server instances or modify the port in +`docker-compose.test.yaml`. + +### Stale Test Data + +Clean up test database: + +```bash +docker compose -f docker-compose.test.yaml down -v +``` + +### View Container Logs + +```bash +docker compose -f docker-compose.test.yaml logs +``` + +## Best Practices + +1. **Isolation**: Each test should be independent and not rely on other tests +2. **Cleanup**: Use fixtures and dispose patterns for resource cleanup +3. **Mocking**: Mock external dependencies in unit tests +4. **Descriptive Names**: Use clear, descriptive test method names +5. **Arrange-Act-Assert**: Follow AAA pattern in unit tests +6. **Given-When-Then**: Follow GWT pattern in BDD scenarios