mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-04-05 10:09:03 +00:00
Update documentation (#156)
This commit is contained in:
418
docs/architecture.md
Normal file
418
docs/architecture.md
Normal file
@@ -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<UserCredential> GetUserCredentialAsync(string username);
|
||||||
|
Task<int> 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).
|
||||||
75
docs/diagrams/architecture.puml
Normal file
75
docs/diagrams/architecture.puml
Normal file
@@ -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
|
||||||
298
docs/diagrams/authentication-flow.puml
Normal file
298
docs/diagrams/authentication-flow.puml
Normal file
@@ -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
|
||||||
523
docs/diagrams/class-diagram.puml
Normal file
523
docs/diagrams/class-diagram.puml
Normal file
@@ -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" <<Rectangle>> #E3F2FD {
|
||||||
|
|
||||||
|
class AuthController {
|
||||||
|
- IRegisterService _registerService
|
||||||
|
- ILoginService _loginService
|
||||||
|
+ <<async>> Task<ActionResult> Register(RegisterRequest)
|
||||||
|
+ <<async>> Task<ActionResult> Login(LoginRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserController {
|
||||||
|
- IUserService _userService
|
||||||
|
+ <<async>> Task<ActionResult<IEnumerable<UserAccount>>> GetAll(int?, int?)
|
||||||
|
+ <<async>> Task<ActionResult<UserAccount>> GetById(Guid)
|
||||||
|
}
|
||||||
|
|
||||||
|
class GlobalExceptionFilter {
|
||||||
|
- ILogger<GlobalExceptionFilter> _logger
|
||||||
|
+ void OnException(ExceptionContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Contracts" {
|
||||||
|
class RegisterRequest <<record>> {
|
||||||
|
+ string Username
|
||||||
|
+ string FirstName
|
||||||
|
+ string LastName
|
||||||
|
+ string Email
|
||||||
|
+ DateTime DateOfBirth
|
||||||
|
+ string Password
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoginRequest <<record>> {
|
||||||
|
+ string Username
|
||||||
|
+ string Password
|
||||||
|
}
|
||||||
|
|
||||||
|
class RegisterRequestValidator {
|
||||||
|
+ RegisterRequestValidator()
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoginRequestValidator {
|
||||||
|
+ LoginRequestValidator()
|
||||||
|
}
|
||||||
|
|
||||||
|
class "ResponseBody<T>" <<record>> {
|
||||||
|
+ string Message
|
||||||
|
+ T Payload
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoginPayload <<record>> {
|
||||||
|
+ Guid UserAccountId
|
||||||
|
+ string Username
|
||||||
|
+ string RefreshToken
|
||||||
|
+ string AccessToken
|
||||||
|
}
|
||||||
|
|
||||||
|
class RegistrationPayload <<record>> {
|
||||||
|
+ Guid UserAccountId
|
||||||
|
+ string Username
|
||||||
|
+ string RefreshToken
|
||||||
|
+ string AccessToken
|
||||||
|
+ bool ConfirmationEmailSent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
' Service Layer
|
||||||
|
package "Service Layer" <<Rectangle>> #C8E6C9 {
|
||||||
|
|
||||||
|
package "Service.Auth" {
|
||||||
|
interface IRegisterService {
|
||||||
|
+ <<async>> Task<RegisterServiceReturn> RegisterAsync(UserAccount, string)
|
||||||
|
}
|
||||||
|
|
||||||
|
class RegisterService {
|
||||||
|
- IAuthRepository _authRepo
|
||||||
|
- IPasswordInfrastructure _passwordInfra
|
||||||
|
- ITokenService _tokenService
|
||||||
|
- IEmailService _emailService
|
||||||
|
+ <<async>> Task<RegisterServiceReturn> RegisterAsync(UserAccount, string)
|
||||||
|
- <<async>> Task ValidateUserDoesNotExist(UserAccount)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ILoginService {
|
||||||
|
+ <<async>> Task<LoginServiceReturn> LoginAsync(string, string)
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoginService {
|
||||||
|
- IAuthRepository _authRepo
|
||||||
|
- IPasswordInfrastructure _passwordInfra
|
||||||
|
- ITokenService _tokenService
|
||||||
|
+ <<async>> Task<LoginServiceReturn> LoginAsync(string, string)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITokenService {
|
||||||
|
+ string GenerateAccessToken(UserAccount)
|
||||||
|
+ string GenerateRefreshToken(UserAccount)
|
||||||
|
}
|
||||||
|
|
||||||
|
class TokenService {
|
||||||
|
- ITokenInfrastructure _tokenInfrastructure
|
||||||
|
+ string GenerateAccessToken(UserAccount)
|
||||||
|
+ string GenerateRefreshToken(UserAccount)
|
||||||
|
}
|
||||||
|
|
||||||
|
class RegisterServiceReturn <<record>> {
|
||||||
|
+ bool IsAuthenticated
|
||||||
|
+ bool EmailSent
|
||||||
|
+ UserAccount UserAccount
|
||||||
|
+ string AccessToken
|
||||||
|
+ string RefreshToken
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoginServiceReturn <<record>> {
|
||||||
|
+ UserAccount UserAccount
|
||||||
|
+ string RefreshToken
|
||||||
|
+ string AccessToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Service.UserManagement" {
|
||||||
|
interface IUserService {
|
||||||
|
+ <<async>> Task<IEnumerable<UserAccount>> GetAllAsync(int?, int?)
|
||||||
|
+ <<async>> Task<UserAccount> GetByIdAsync(Guid)
|
||||||
|
+ <<async>> Task UpdateAsync(UserAccount)
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserService {
|
||||||
|
- IUserAccountRepository _repository
|
||||||
|
+ <<async>> Task<IEnumerable<UserAccount>> GetAllAsync(int?, int?)
|
||||||
|
+ <<async>> Task<UserAccount> GetByIdAsync(Guid)
|
||||||
|
+ <<async>> Task UpdateAsync(UserAccount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Service.Emails" {
|
||||||
|
interface IEmailService {
|
||||||
|
+ <<async>> Task SendRegistrationEmailAsync(UserAccount, string)
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmailService {
|
||||||
|
- IEmailProvider _emailProvider
|
||||||
|
- IEmailTemplateProvider _templateProvider
|
||||||
|
+ <<async>> Task SendRegistrationEmailAsync(UserAccount, string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
' Domain Layer
|
||||||
|
package "Domain" <<Rectangle>> #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" <<Rectangle>> #E1BEE7 {
|
||||||
|
|
||||||
|
package "Infrastructure.Repository" {
|
||||||
|
interface IAuthRepository {
|
||||||
|
+ <<async>> Task<UserAccount> RegisterUserAsync(string, string, string, string, DateTime, string)
|
||||||
|
+ <<async>> Task<UserAccount?> GetUserByEmailAsync(string)
|
||||||
|
+ <<async>> Task<UserAccount?> GetUserByUsernameAsync(string)
|
||||||
|
+ <<async>> Task<UserCredential?> GetActiveCredentialByUserAccountIdAsync(Guid)
|
||||||
|
+ <<async>> Task RotateCredentialAsync(Guid, string)
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthRepository {
|
||||||
|
- ISqlConnectionFactory _connectionFactory
|
||||||
|
+ <<async>> Task<UserAccount> RegisterUserAsync(...)
|
||||||
|
+ <<async>> Task<UserAccount?> GetUserByEmailAsync(string)
|
||||||
|
+ <<async>> Task<UserAccount?> GetUserByUsernameAsync(string)
|
||||||
|
+ <<async>> Task<UserCredential?> GetActiveCredentialByUserAccountIdAsync(Guid)
|
||||||
|
+ <<async>> Task RotateCredentialAsync(Guid, string)
|
||||||
|
# UserAccount MapToEntity(DbDataReader)
|
||||||
|
- UserCredential MapToCredentialEntity(DbDataReader)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IUserAccountRepository {
|
||||||
|
+ <<async>> Task<UserAccount?> GetByIdAsync(Guid)
|
||||||
|
+ <<async>> Task<IEnumerable<UserAccount>> GetAllAsync(int?, int?)
|
||||||
|
+ <<async>> Task UpdateAsync(UserAccount)
|
||||||
|
+ <<async>> Task DeleteAsync(Guid)
|
||||||
|
+ <<async>> Task<UserAccount?> GetByUsernameAsync(string)
|
||||||
|
+ <<async>> Task<UserAccount?> GetByEmailAsync(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserAccountRepository {
|
||||||
|
- ISqlConnectionFactory _connectionFactory
|
||||||
|
+ <<async>> Task<UserAccount?> GetByIdAsync(Guid)
|
||||||
|
+ <<async>> Task<IEnumerable<UserAccount>> GetAllAsync(int?, int?)
|
||||||
|
+ <<async>> Task UpdateAsync(UserAccount)
|
||||||
|
+ <<async>> Task DeleteAsync(Guid)
|
||||||
|
+ <<async>> Task<UserAccount?> GetByUsernameAsync(string)
|
||||||
|
+ <<async>> Task<UserAccount?> GetByEmailAsync(string)
|
||||||
|
# UserAccount MapToEntity(DbDataReader)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class "Repository<T>" {
|
||||||
|
# ISqlConnectionFactory _connectionFactory
|
||||||
|
# <<async>> Task<DbConnection> CreateConnection()
|
||||||
|
# {abstract} T MapToEntity(DbDataReader)
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Sql" {
|
||||||
|
interface ISqlConnectionFactory {
|
||||||
|
+ DbConnection CreateConnection()
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultSqlConnectionFactory {
|
||||||
|
- string _connectionString
|
||||||
|
+ DbConnection CreateConnection()
|
||||||
|
}
|
||||||
|
|
||||||
|
class SqlConnectionStringHelper <<static>> {
|
||||||
|
+ {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 {
|
||||||
|
+ <<async>> Task SendAsync(string, string, string, bool)
|
||||||
|
+ <<async>> Task SendAsync(IEnumerable<string>, string, string, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
class SmtpEmailProvider {
|
||||||
|
- string _host
|
||||||
|
- int _port
|
||||||
|
- string? _username
|
||||||
|
- string? _password
|
||||||
|
- bool _useSsl
|
||||||
|
- string _fromEmail
|
||||||
|
- string _fromName
|
||||||
|
+ <<async>> Task SendAsync(string, string, string, bool)
|
||||||
|
+ <<async>> Task SendAsync(IEnumerable<string>, string, string, bool)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Infrastructure.Email.Templates" {
|
||||||
|
interface IEmailTemplateProvider {
|
||||||
|
+ <<async>> Task<string> RenderUserRegisteredEmailAsync(string, string)
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmailTemplateProvider {
|
||||||
|
- IServiceProvider _serviceProvider
|
||||||
|
- ILoggerFactory _loggerFactory
|
||||||
|
+ <<async>> Task<string> RenderUserRegisteredEmailAsync(string, string)
|
||||||
|
- <<async>> Task<string> RenderComponentAsync<TComponent>(Dictionary<string, object?>)
|
||||||
|
}
|
||||||
|
|
||||||
|
class "UserRegistration <<Razor>>" {
|
||||||
|
+ string Username
|
||||||
|
+ string ConfirmationLink
|
||||||
|
}
|
||||||
|
|
||||||
|
class "Header <<Razor>>" {
|
||||||
|
}
|
||||||
|
|
||||||
|
class "Footer <<Razor>>" {
|
||||||
|
+ string? FooterText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
' Database Layer
|
||||||
|
package "Database" <<Rectangle>> #FFCCBC {
|
||||||
|
|
||||||
|
class "SQL Server" <<Database>> {
|
||||||
|
.. 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<T>" : 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<T>" <|-- AuthRepository : extends
|
||||||
|
"Repository<T>" <|-- 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<T>" ..> ISqlConnectionFactory : uses
|
||||||
|
|
||||||
|
ISqlConnectionFactory <|.. DefaultSqlConnectionFactory : implements
|
||||||
|
DefaultSqlConnectionFactory ..> SqlConnectionStringHelper : uses
|
||||||
|
|
||||||
|
' Relationships - Infrastructure
|
||||||
|
IPasswordInfrastructure <|.. Argon2Infrastructure : implements
|
||||||
|
ITokenInfrastructure <|.. JwtInfrastructure : implements
|
||||||
|
IEmailProvider <|.. SmtpEmailProvider : implements
|
||||||
|
IEmailTemplateProvider <|.. EmailTemplateProvider : implements
|
||||||
|
|
||||||
|
EmailTemplateProvider ..> "UserRegistration <<Razor>>" : renders
|
||||||
|
"UserRegistration <<Razor>>" ..> "Header <<Razor>>" : includes
|
||||||
|
"UserRegistration <<Razor>>" ..> "Footer <<Razor>>" : 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
|
||||||
104
docs/diagrams/database-schema.puml
Normal file
104
docs/diagrams/database-schema.puml
Normal file
@@ -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 <<PK>>
|
||||||
|
--
|
||||||
|
* Username: NVARCHAR(30) <<UNIQUE>>
|
||||||
|
* Email: NVARCHAR(255) <<UNIQUE>>
|
||||||
|
* FirstName: NVARCHAR(50)
|
||||||
|
* LastName: NVARCHAR(50)
|
||||||
|
Bio: NVARCHAR(500)
|
||||||
|
CreatedAt: DATETIME2
|
||||||
|
UpdatedAt: DATETIME2
|
||||||
|
LastLoginAt: DATETIME2
|
||||||
|
}
|
||||||
|
|
||||||
|
entity "UserCredential" as Cred {
|
||||||
|
* UserCredentialId: INT <<PK>>
|
||||||
|
--
|
||||||
|
* UserAccountId: INT <<FK>>
|
||||||
|
* PasswordHash: VARBINARY(32)
|
||||||
|
* PasswordSalt: VARBINARY(16)
|
||||||
|
CredentialRotatedAt: DATETIME2
|
||||||
|
CredentialExpiresAt: DATETIME2
|
||||||
|
CredentialRevokedAt: DATETIME2
|
||||||
|
* IsActive: BIT
|
||||||
|
CreatedAt: DATETIME2
|
||||||
|
}
|
||||||
|
|
||||||
|
entity "UserVerification" as Verify {
|
||||||
|
* UserVerificationId: INT <<PK>>
|
||||||
|
--
|
||||||
|
* UserAccountId: INT <<FK>>
|
||||||
|
* IsVerified: BIT
|
||||||
|
VerifiedAt: DATETIME2
|
||||||
|
VerificationToken: NVARCHAR(255)
|
||||||
|
TokenExpiresAt: DATETIME2
|
||||||
|
}
|
||||||
|
|
||||||
|
entity "UserAvatar" as Avatar {
|
||||||
|
* UserAvatarId: INT <<PK>>
|
||||||
|
--
|
||||||
|
* UserAccountId: INT <<FK>>
|
||||||
|
PhotoId: INT <<FK>>
|
||||||
|
* IsActive: BIT
|
||||||
|
CreatedAt: DATETIME2
|
||||||
|
}
|
||||||
|
|
||||||
|
entity "UserFollow" as Follow {
|
||||||
|
* UserFollowId: INT <<PK>>
|
||||||
|
--
|
||||||
|
* FollowerUserId: INT <<FK>>
|
||||||
|
* FollowedUserId: INT <<FK>>
|
||||||
|
CreatedAt: DATETIME2
|
||||||
|
}
|
||||||
|
|
||||||
|
entity "Photo" as Photo {
|
||||||
|
* PhotoId: INT <<PK>>
|
||||||
|
--
|
||||||
|
* 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
|
||||||
227
docs/diagrams/deployment.puml
Normal file
227
docs/diagrams/deployment.puml
Normal file
@@ -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
|
||||||
|
<b>Dev Network (bridge: biergarten-dev)</b>
|
||||||
|
Internal DNS:
|
||||||
|
- sql-server (resolves to SQL container)
|
||||||
|
- api (resolves to API container)
|
||||||
|
end note
|
||||||
|
|
||||||
|
note bottom of TestDB
|
||||||
|
<b>Test Network (bridge: biergarten-test)</b>
|
||||||
|
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
|
||||||
|
<b>Production Deployment (not shown):</b>
|
||||||
|
|
||||||
|
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
|
||||||
327
docs/docker.md
Normal file
327
docs/docker.md
Normal file
@@ -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)
|
||||||
384
docs/environment-variables.md
Normal file
384
docs/environment-variables.md
Normal file
@@ -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=<generated-secret> # Email confirmation tokens
|
||||||
|
RESET_PASSWORD_TOKEN_SECRET=<generated-secret> # Password reset tokens
|
||||||
|
SESSION_SECRET=<generated-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=<generated-with-openssl>
|
||||||
|
RESET_PASSWORD_TOKEN_SECRET=<generated-with-openssl>
|
||||||
|
SESSION_SECRET=<generated-with-openssl>
|
||||||
|
|
||||||
|
# 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!
|
||||||
|
```
|
||||||
261
docs/getting-started.md
Normal file
261
docs/getting-started.md
Normal file
@@ -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 <repository-url>
|
||||||
|
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 <service-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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).
|
||||||
293
docs/testing.md
Normal file
293
docs/testing.md
Normal file
@@ -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<IAuthRepository>();
|
||||||
|
var mockJwt = new Mock<IJwtService>();
|
||||||
|
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 <service-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
Reference in New Issue
Block a user