mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-04-05 18:09:04 +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