Update documentation to reflect new architecture and previous refactors

This commit is contained in:
Aaron Po
2026-06-20 01:33:05 -04:00
parent fd341e332c
commit 07aedcb866
10 changed files with 543 additions and 346 deletions

View File

@@ -41,7 +41,7 @@ Data generation pipeline (C++):
Active areas in the repository: Active areas in the repository:
- .NET 10 backend (layered architecture) + SQL Server - .NET 10 backend (vertical-slice architecture with MediatR) + SQL Server
- React 19 website (React Router 7 + Vite) - React 19 website (React Router 7 + Vite)
- Shared Biergarten theme system + Storybook coverage - Shared Biergarten theme system + Storybook coverage
- Auth flows and account/email integration (local Mailpit in dev compose) - Auth flows and account/email integration (local Mailpit in dev compose)
@@ -105,7 +105,8 @@ npm run test:storybook:playwright
```text ```text
web/ web/
backend/ .NET API + domain/service/infrastructure + DB projects backend/ .NET API: API.Core (host), Features.* (vertical slices),
Shared.*, Domain.*, Infrastructure.*, Database.* projects
frontend/ React Router website + Storybook + Playwright/Vitest frontend/ React Router website + Storybook + Playwright/Vitest
tooling/ tooling/

View File

@@ -7,61 +7,65 @@ This document describes the active architecture of The Biergarten App.
The Biergarten App is a monorepo with a clear split between the backend and the The Biergarten App is a monorepo with a clear split between the backend and the
active website: active website:
- **Backend**: .NET 10 Web API with SQL Server and a layered architecture - **Backend**: .NET 10 Web API with SQL Server, organized as vertical feature
- **Frontend**: React 19 + React Router 7 website in `src/Website` slices with MediatR
- **Architecture Style**: Layered backend plus server-rendered React frontend - **Frontend**: React 19 + React Router 7 website in `web/frontend`
- **Architecture Style**: Vertical-slice backend plus server-rendered React
frontend
The legacy Next.js frontend has been retained in `src/Website-v1` for reference The legacy Next.js frontend has been retained in `archive/next-js-web-app/`
only and is documented in for reference only.
[archive/legacy-website-v1.md](archive/legacy-website-v1.md).
## Diagrams ## Diagrams
For visual representations, see: For visual representations, see:
- [architecture.svg](diagrams-out/architecture.svg) - Layered architecture - [architecture.svg](website/diagrams-out/architecture.svg) - Vertical-slice
architecture diagram
- [deployment.svg](website/diagrams-out/deployment.svg) - Docker deployment
diagram diagram
- [deployment.svg](diagrams-out/deployment.svg) - Docker deployment diagram - [authentication-flow.svg](website/diagrams-out/authentication-flow.svg) -
- [authentication-flow.svg](diagrams-out/authentication-flow.svg) -
Authentication workflow Authentication workflow
- [database-schema.svg](diagrams-out/database-schema.svg) - Database - [database-schema.svg](website/diagrams-out/database-schema.svg) - Database
relationships relationships
## Backend Architecture ## Backend Architecture
### Layered Architecture Pattern ### Vertical Slice Architecture Pattern
The backend follows a strict layered architecture: The backend organizes business capabilities as feature slices instead of
technical layers. Each feature (`Features.Auth`, `Features.Breweries`,
`Features.UserManagement`, `Features.Emails`) is a single project that owns
its own controller, MediatR commands/queries/handlers, validators, and
repository, end to end:
``` ```
┌─────────────────────────────────────┐ ┌───────────────────────────────────────────────────────────────────────
API Layer (Controllers) API.Core (thin host)
│ - HTTP Endpoints │ - Program.cs wiring: MediatR + AddApplicationPart per slice
│ - Request/Response mapping │ - Swagger/OpenAPI, JWT auth middleware, global exception filter
│ - Swagger/OpenAPI │ └───────────────────────────────────────────────────────────────────────┘
└─────────────────────────────────────┘ ↓ discovers controllers via AddApplicationPart
┌───────────────┬───────────────┬───────────────────┬───────────────────┐
│ Features.Auth │Features. │ Features. │ Features.Emails │
│ │Breweries │ UserManagement │ (no controller, │
│ Controller │ Controller │ Controller │ internal only) │
│ Commands/ │ Commands/ │ Commands/ │ Commands/ │
│ Queries + │ Queries + │ Queries + │ Handlers │
│ Handlers │ Handlers │ Handlers │ │
│ Repository │ Repository │ Repository │ EmailDispatcher │
└───────────────┴───────────────┴───────────────────┴───────────────────┘
↓ each slice depends only on shared/domain/infra, never on another slice
┌─────────────────────────┬─────────────────────────┬────────────────────┐
│ Shared.Contracts │ Shared.Application │ Domain.Entities / │
│ (ResponseBody envelope) │ (ValidationBehavior, │ Domain.Exceptions │
│ │ cross-slice email cmds) │ │
└─────────────────────────┴─────────────────────────┴────────────────────┘
┌─────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────
Service Layer (Business Logic) Infrastructure.Sql, Infrastructure.Jwt, Infrastructure.PasswordHashing,
- Authentication logic Infrastructure.Email, Infrastructure.Email.Templates
│ - 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) │ │ Database (SQL Server) │
@@ -70,75 +74,123 @@ The backend follows a strict layered architecture:
└─────────────────────────────────────┘ └─────────────────────────────────────┘
``` ```
Slices never reference each other's project. The one cross-slice
interaction (`Features.Auth` triggering a confirmation email handled by
`Features.Emails`) goes through a MediatR command (`SendRegistrationEmailCommand`)
whose contract lives in `Shared.Application`, so neither slice takes a
project reference on the other.
### Layer Responsibilities ### Layer Responsibilities
#### API Layer (`API.Core`) #### API Layer (`API.Core`)
**Purpose**: HTTP interface and request handling **Purpose**: Thin ASP.NET Core host: no business logic, no controllers of
its own
**Components**: **Components**:
- Controllers (`AuthController`, `UserController`) - `Program.cs`: registers MediatR (scanning every `Features.*` assembly),
- Middleware for error handling FluentValidation, and uses `AddApplicationPart` so each slice's controllers
- Swagger/OpenAPI documentation are discovered by MVC
- Health check endpoints - `GlobalException.cs`: global exception filter (host-level cross-cutting
concern)
- `Authentication/JwtAuthenticationHandler.cs`: JWT auth scheme middleware
- Swagger/OpenAPI documentation, health check endpoints
**Dependencies**: **Dependencies**:
- Service layer - Every `Features.*` project (for controller/MediatR discovery)
- ASP.NET Core framework - `Shared.Contracts`, `Shared.Application`
- `Infrastructure.Jwt` (for the auth middleware)
**Rules**: **Rules**:
- No business logic - No controllers, no business logic, no feature-specific contracts
- Only request/response transformation - Exists purely to host and wire up the feature slices
- Delegates all work to Service layer
#### Service Layer (`Service.Auth`, `Service.UserManagement`) #### Feature Slices (`Features.Auth`, `Features.Breweries`, `Features.UserManagement`, `Features.Emails`)
**Purpose**: Business logic and orchestration **Purpose**: Each slice is the complete vertical for one business capability
**Components** (per slice):
- `Controllers/`: HTTP endpoints, binding directly to Command/Query types
as the request contract
- `Commands/<Operation>/` and `Queries/<Operation>/`: one folder per
operation, each containing the Command/Query record, its
`IRequestHandler`, and (for commands) a FluentValidation validator
- `Repository/`: the slice's own stored-procedure repository
implementation
- `Dtos/`: response shapes returned by query handlers (never the raw
domain entity)
- `DependencyInjection/`: an `AddFeaturesX()` extension method registering
the slice's repository/services
`Features.Emails` has no `Controllers/` folder. It's invoked only via
MediatR commands sent from other slices, never over HTTP.
**Dependencies**:
- `Domain.Entities`, `Domain.Exceptions`
- `Infrastructure.Sql` (generic ADO.NET plumbing) plus whichever
infrastructure project the slice needs (`Infrastructure.Jwt`/
`Infrastructure.PasswordHashing` for Auth, `Infrastructure.Email`/
`Infrastructure.Email.Templates` for Emails)
- `Shared.Contracts`, `Shared.Application`
- **Never** another `Features.*` project
**Rules**:
- All business logic for that feature lives in its command/query handlers
- No direct controller-to-repository calls; everything flows through
MediatR
- Read endpoints return a dedicated `Dto`, never the domain entity directly
#### Shared Projects (`Shared.Contracts`, `Shared.Application`)
**Purpose**: The minimum cross-slice surface area required because every
slice needs it, or because duplicating it four times would be worse than
sharing it
**Components**: **Components**:
- Authentication services (login, registration) - `Shared.Contracts`: `ResponseBody<T>`/`ResponseBody`, the API response
- User management services envelope every controller returns
- Business rule validation - `Shared.Application`: `ValidationBehavior<TRequest,TResponse>` (the
- Transaction coordination MediatR pipeline behavior that runs FluentValidation before a handler
executes) and the cross-slice email commands
**Dependencies**: (`SendRegistrationEmailCommand`, `SendResendConfirmationEmailCommand`)
- Infrastructure layer (repositories, JWT, password hashing)
- Domain entities
**Rules**: **Rules**:
- Contains all business logic - Kept deliberately small: this is the exception to "no slice depends on
- Coordinates multiple infrastructure components another slice," not a general-purpose dumping ground
- No direct database access (uses repositories)
- Returns domain models, not DTOs
#### Infrastructure Layer #### Infrastructure Layer
**Purpose**: Technical capabilities and external integrations **Purpose**: Technical capabilities and external integrations, shared by
whichever slices need them
**Components**: **Components**:
- **Infrastructure.Repository**: Data access via stored procedures - **Infrastructure.Sql**: generic ADO.NET connection/command plumbing
(`ISqlConnectionFactory`, the abstract `Repository<T>` base class), not
domain-specific; each slice's own `Repository/` folder builds on this
- **Infrastructure.Jwt**: JWT token generation and validation - **Infrastructure.Jwt**: JWT token generation and validation
- **Infrastructure.PasswordHashing**: Argon2id password hashing - **Infrastructure.PasswordHashing**: Argon2id password hashing
- **Infrastructure.Email**: Email sending capabilities - **Infrastructure.Email**: Email sending capabilities (SMTP/MailKit)
- **Infrastructure.Email.Templates**: Email template rendering - **Infrastructure.Email.Templates**: Email template rendering (Razor
components)
**Dependencies**: **Dependencies**:
- Domain entities - Domain entities
- External libraries (ADO.NET, JWT, Argon2, etc.) - External libraries (ADO.NET, JWT, Argon2, MailKit, etc.)
**Rules**: **Rules**:
- Implements technical concerns - Implements technical concerns only, no business logic
- No business logic - Reusable across slices
- Reusable across services
#### Domain Layer (`Domain.Entities`) #### Domain Layer (`Domain.Entities`)
@@ -163,21 +215,55 @@ The backend follows a strict layered architecture:
### Design Patterns ### Design Patterns
#### Vertical Slice + MediatR
**Purpose**: Organize code by feature instead of by technical layer, so
everything needed to understand or change one capability lives in one
project
**Implementation**:
- Each HTTP write operation is a `Command` (e.g. `CreateBreweryCommand` in
`Features.Breweries/Commands/CreateBrewery/`); each read operation is a
`Query` (e.g. `GetBreweryByIdQuery`)
- Controllers bind directly to the Command/Query as the request body;
there is no separate request DTO + mapping step for writes
- A single shared `ValidationBehavior<TRequest,TResponse>`
(`Shared.Application/Behaviors/`) runs FluentValidation validators in the
MediatR pipeline before any handler executes
- Query handlers map to a dedicated response `Dto`, so domain entities never
leak over the wire
**Example**:
```csharp
public record CreateBreweryCommand(Guid PostedById, string BreweryName, string Description, ...)
: IRequest<BreweryDto>;
public class CreateBreweryHandler(IBreweryRepository repository) : IRequestHandler<CreateBreweryCommand, BreweryDto>
{
public async Task<BreweryDto> Handle(CreateBreweryCommand request, CancellationToken cancellationToken) { ... }
}
```
#### Repository Pattern #### Repository Pattern
**Purpose**: Abstract database access behind interfaces **Purpose**: Abstract database access behind interfaces
**Implementation**: **Implementation**: each slice owns its own repository, scoped to that
feature only:
- `IAuthRepository` - Authentication queries - `Features.Auth/Repository/IAuthRepository.cs`
- `IUserAccountRepository` - User account queries - `Features.Breweries/Repository/IBreweryRepository.cs`
- `DefaultSqlConnectionFactory` - Connection management - `Features.UserManagement/Repository/IUserAccountRepository.cs`
- `Infrastructure.Sql/DefaultSqlConnectionFactory.cs`: the generic
connection factory every slice's repository builds on
**Benefits**: **Benefits**:
- Testable (easy to mock) - Testable (easy to mock)
- SQL-first approach (stored procedures) - SQL-first approach (stored procedures)
- Centralized data access logic - Each slice's data access logic is self-contained
**Example**: **Example**:
@@ -193,17 +279,19 @@ public interface IAuthRepository
**Purpose**: Loose coupling and testability **Purpose**: Loose coupling and testability
**Configuration**: `Program.cs` registers all services **Configuration**: `Program.cs` wires up MediatR/FluentValidation across
every `Features.*` assembly; each slice exposes its own `AddFeaturesX()`
extension method that registers its repository and slice-internal services
**Lifetimes**: **Lifetimes**:
- Scoped: Repositories, Services (per request) - Scoped: Repositories, slice-internal services (per request)
- Singleton: Connection factories, JWT configuration - Singleton: `ISqlConnectionFactory`
- Transient: Utilities, helpers - Transient: Utilities, helpers
#### SQL-First Approach #### SQL-First Approach
**Purpose**: Leverage database capabilities **Purpose**: Push complex logic into the database
**Strategy**: **Strategy**:
@@ -220,13 +308,13 @@ public interface IAuthRepository
## Frontend Architecture ## Frontend Architecture
### Active Website (`src/Website`) ### Active Website (`web/frontend`)
The current website is a React Router 7 application with server-side rendering The current website is a React Router 7 application with server-side rendering
enabled. enabled.
```text ```text
src/Website/ web/frontend/
├── app/ ├── app/
│ ├── components/ Shared UI such as Navbar, FormField, SubmitButton, ToastProvider │ ├── components/ Shared UI such as Navbar, FormField, SubmitButton, ToastProvider
│ ├── lib/ Auth helpers, schemas, and theme metadata │ ├── lib/ Auth helpers, schemas, and theme metadata
@@ -262,10 +350,9 @@ All component styling should prefer semantic tokens such as `primary`,
### Legacy Frontend ### Legacy Frontend
The previous Next.js frontend has been archived at `src/Website-v1`. Active The previous Next.js frontend has been archived at `archive/next-js-web-app/`
product and engineering documentation should point to `src/Website`, while for reference only. Active product and engineering documentation should point
legacy notes live in to `web/frontend`.
[archive/legacy-website-v1.md](archive/legacy-website-v1.md).
## Security Architecture ## Security Architecture
@@ -390,7 +477,7 @@ scripts/
- Testing (`docker-compose.test.yaml`) - Testing (`docker-compose.test.yaml`)
- Production (`docker-compose.prod.yaml`) - Production (`docker-compose.prod.yaml`)
For details, see [Docker Guide](docker.md). For details, see [Docker Guide](website/docker.md).
### Health Checks ### Health Checks
@@ -416,11 +503,10 @@ healthcheck:
│ Integration │ ← API.Specs (Reqnroll) │ Integration │ ← API.Specs (Reqnroll)
│ Tests │ │ Tests │
├──────────────┤ ├──────────────┤
│ Unit Tests │ ← Service.Auth.Tests │ Unit Tests │ ← Features.Auth.Tests, Features.Breweries.Tests,
(Service)Repository.Tests (per slice,Features.UserManagement.Tests, Features.Emails.Tests
├──────────────┤ │ handlers + │ (commands/queries/handlers + that slice's own
Unit Tests │ repository) │ repository, mocked with Moq/DbMocker)
│ (Repository) │
└──────────────┘ └──────────────┘
``` ```
@@ -431,4 +517,4 @@ healthcheck:
- Mock external dependencies - Mock external dependencies
- Test database for integration tests - Test database for integration tests
For details, see [Testing Guide](testing.md). For details, see [Testing Guide](website/testing.md).

File diff suppressed because one or more lines are too long

View File

@@ -4,41 +4,67 @@ skinparam backgroundColor #FFFFFF
skinparam defaultFontName Arial skinparam defaultFontName Arial
skinparam packageStyle rectangle skinparam packageStyle rectangle
title The Biergarten App - Layered Architecture title The Biergarten App - Vertical Slice Architecture
package "API Layer" #E3F2FD { package "API.Core (thin host)" #E3F2FD {
[API.Core\nASP.NET Core Web API] as API [API.Core\nASP.NET Core Web API host] as API
note right of API note right of API
- Controllers (Auth, User) - Program.cs wiring only
- Swagger/OpenAPI - MediatR + AddApplicationPart
- Middleware per Features.* assembly
- Health Checks - Swagger/OpenAPI, health checks
- JWT auth middleware
- Global exception filter
end note end note
} }
package "Service Layer" #F3E5F5 { package "Feature Slices" #F3E5F5 {
[Service.Auth] as AuthSvc [Features.Auth] as AuthSlice
[Service.UserManagement] as UserSvc [Features.Breweries] as BrewerySlice
note right of AuthSvc [Features.UserManagement] as UserSlice
- Business Logic [Features.Emails] as EmailsSlice
- Validation note right of AuthSlice
- Orchestration Each slice owns its own:
- Controller (HTTP endpoints)
- Commands/Queries + Handlers
- Validators
- Repository
end note
note right of EmailsSlice
No controller - invoked only
via MediatR commands sent
from other slices
end note
}
package "Shared" #FCE4EC {
[Shared.Contracts] as SharedContracts
[Shared.Application] as SharedApp
note right of SharedApp
- ValidationBehavior
(MediatR pipeline)
- Cross-slice email commands
(SendRegistrationEmailCommand,
SendResendConfirmationEmailCommand)
end note end note
} }
package "Infrastructure Layer" #FFF3E0 { package "Infrastructure Layer" #FFF3E0 {
[Infrastructure.Repository] as Repo [Infrastructure.Sql] as Sql
[Infrastructure.Jwt] as JWT [Infrastructure.Jwt] as JWT
[Infrastructure.PasswordHashing] as PwdHash [Infrastructure.PasswordHashing] as PwdHash
[Infrastructure.Email] as Email [Infrastructure.Email] as Email
[Infrastructure.Email.Templates] as EmailTemplates
} }
package "Domain Layer" #E8F5E9 { package "Domain Layer" #E8F5E9 {
[Domain.Entities] as Domain [Domain.Entities] as Domain
[Domain.Exceptions] as DomainExceptions
note right of Domain note right of Domain
- UserAccount - UserAccount
- UserCredential - UserCredential
- UserVerification - UserVerification
- BreweryPost
end note end note
} }
@@ -48,28 +74,53 @@ database "SQL Server" {
} }
' Relationships ' Relationships
API --> AuthSvc API ..> AuthSlice : AddApplicationPart\n+ MediatR scan
API --> UserSvc API ..> BrewerySlice : AddApplicationPart\n+ MediatR scan
API ..> UserSlice : AddApplicationPart\n+ MediatR scan
API ..> EmailsSlice : MediatR scan only\n(no controller)
AuthSvc --> Repo AuthSlice --> Sql
AuthSvc --> JWT AuthSlice --> JWT
AuthSvc --> PwdHash AuthSlice --> PwdHash
AuthSvc --> Email AuthSlice --> SharedContracts
AuthSlice --> SharedApp
UserSvc --> Repo BrewerySlice --> Sql
BrewerySlice --> SharedContracts
BrewerySlice --> SharedApp
Repo --> SP UserSlice --> Sql
Repo --> Domain UserSlice --> SharedContracts
UserSlice --> SharedApp
EmailsSlice --> Email
EmailsSlice --> EmailTemplates
EmailsSlice --> SharedApp
' The one cross-slice interaction: a MediatR command contract, not a project reference
AuthSlice ..> SharedApp : sends\nSendRegistrationEmailCommand
EmailsSlice ..> SharedApp : handles\nSendRegistrationEmailCommand
Sql --> SP
Sql --> Domain
SP --> Tables SP --> Tables
AuthSvc --> Domain AuthSlice --> Domain
UserSvc --> Domain BrewerySlice --> Domain
UserSlice --> Domain
AuthSlice --> DomainExceptions
' Notes ' Notes
note left of Repo note left of Sql
SQL-first approach SQL-first approach
All queries via All queries via
stored procedures stored procedures
end note end note
note bottom of AuthSlice
No Features.* project
ever references another
Features.* project directly
end note
@enduml @enduml

View File

@@ -112,38 +112,32 @@ package "Test Environment\n(docker-compose.test.yaml)" #FFF3E0 {
end note end note
} }
node "Infrastructure.Repository.Tests\n(Unit Tests)" as RepoTests { node "unit.tests\n(Features.*.Tests, one container)" as UnitTests {
component "xUnit + DbMocker" as RepoComp component "xUnit + Moq + DbMocker" as UnitComp
note right note right
Tests: Builds Dockerfile.tests, which
- AuthRepository restores/builds against Core.slnx
- UserAccountRepository then runs `dotnet test` for every
- SQL command building Features/*.Tests project in turn:
- Features.Auth.Tests
- Features.Breweries.Tests
- Features.UserManagement.Tests
- Features.Emails.Tests
Uses: Mock connections Uses: Moq for handlers,
DbMocker for repositories
No real database needed No real database needed
end note 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 { folder "test-results/\n(mounted volume)" as Results {
file "api-specs/\n results.trx" as Result1 file "api-specs/\n results.trx" as Result1
file "repository-tests/\n results.trx" as Result2 file "Features.Auth.Tests.trx" as Result2
file "service-auth-tests/\n results.trx" as Result3 file "Features.Breweries.Tests.trx" as Result3
file "Features.UserManagement.Tests.trx" as Result4
file "Features.Emails.Tests.trx" as Result5
note bottom note bottom
TRX format TRX format
@@ -170,17 +164,16 @@ DevAPI .up.> DevSeed : depends_on
TestMig --> TestDB : 1. Migrate TestMig --> TestDB : 1. Migrate
TestSeed --> TestDB : 2. Seed TestSeed --> TestDB : 2. Seed
Specs --> TestDB : 3. Integration test Specs --> TestDB : 3. Integration test
RepoTests ..> TestDB : Mock (no connection) UnitTests ..> TestDB : depends_on for startup\nordering only (Moq/DbMocker,\nno real connection)
SvcTests ..> TestDB : Mock (no connection)
TestMig .up.> TestDB : depends_on TestMig .up.> TestDB : depends_on
TestSeed .up.> TestMig : depends_on TestSeed .up.> TestMig : depends_on
Specs .up.> TestSeed : depends_on Specs .up.> TestSeed : depends_on
UnitTests .up.> TestSeed : depends_on
' Test results export ' Test results export
Specs --> Results : Export TRX Specs --> Results : Export TRX
RepoTests --> Results : Export TRX UnitTests --> Results : Export TRX (one .trx per slice)
SvcTests --> Results : Export TRX
' Network notes ' Network notes
note bottom of DevDB note bottom of DevDB

View File

@@ -13,7 +13,7 @@ The project uses Docker Compose to orchestrate multiple services:
- .NET API - .NET API
- Test runners - Test runners
See the [deployment diagram](diagrams/pdf/deployment.pdf) for visual See the [deployment diagram](diagrams-out/deployment.svg) for visual
representation. representation.
## Docker Compose Environments ## Docker Compose Environments
@@ -25,9 +25,10 @@ representation.
**Features**: **Features**:
- Persistent SQL Server volume - Persistent SQL Server volume
- Hot reload support - NuGet package cache volume (speeds up rebuilds)
- Swagger UI enabled - Swagger UI enabled
- Seed data included - Seed data included
- Local Mailpit SMTP server for dev email testing
- `CLEAR_DATABASE=true` (drops and recreates schema) - `CLEAR_DATABASE=true` (drops and recreates schema)
**Services**: **Services**:
@@ -37,28 +38,30 @@ sqlserver # SQL Server 2022 (port 1433)
database.migrations # DbUp migrations database.migrations # DbUp migrations
database.seed # Seed initial data database.seed # Seed initial data
api.core # Web API (ports 8080, 8081) api.core # Web API (ports 8080, 8081)
mailpit # Local dev SMTP server + UI (port 8025)
``` ```
**Start Development Environment**: **Start Development Environment**:
```bash ```bash
docker compose -f docker-compose.dev.yaml up -d docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml up -d
``` ```
**Access**: **Access**:
- API Swagger: http://localhost:8080/swagger - API Swagger: http://localhost:8080/swagger
- Health Check: http://localhost:8080/health - Health Check: http://localhost:8080/health
- Mailpit UI: http://localhost:8025
- SQL Server: localhost:1433 (sa credentials from .env.dev) - SQL Server: localhost:1433 (sa credentials from .env.dev)
**Stop Environment**: **Stop Environment**:
```bash ```bash
# Stop services (keep volumes) # Stop services (keep volumes)
docker compose -f docker-compose.dev.yaml down docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml down
# Stop and remove volumes (fresh start) # Stop and remove volumes (fresh start)
docker compose -f docker-compose.dev.yaml down -v docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml down -v
``` ```
### 2. Testing (`docker-compose.test.yaml`) ### 2. Testing (`docker-compose.test.yaml`)
@@ -80,24 +83,22 @@ sqlserver # Test database
database.migrations # Fresh schema database.migrations # Fresh schema
database.seed # Test data database.seed # Test data
api.specs # Reqnroll BDD tests api.specs # Reqnroll BDD tests
repository.tests # Repository unit tests unit.tests # All Features.*.Tests unit test projects
service.auth.tests # Service unit tests
``` ```
**Run Tests**: **Run Tests**:
```bash ```bash
# Run all tests # Run all tests
docker compose -f docker-compose.test.yaml up --abort-on-container-exit docker compose --env-file web/.env.test -f web/docker-compose.test.yaml up --abort-on-container-exit
# View results # View results
ls -la test-results/ ls -la test-results/
cat test-results/api-specs/results.trx cat test-results/api-specs/results.trx
cat test-results/repository-tests/results.trx cat test-results/Features.Auth.Tests.trx
cat test-results/service-auth-tests/results.trx
# Clean up # Clean up
docker compose -f docker-compose.test.yaml down -v docker compose --env-file web/.env.test -f web/docker-compose.test.yaml down -v
``` ```
### 3. Production (`docker-compose.prod.yaml`) ### 3. Production (`docker-compose.prod.yaml`)
@@ -124,7 +125,7 @@ api.core # Production API
**Deploy Production**: **Deploy Production**:
```bash ```bash
docker compose -f docker-compose.prod.yaml up -d docker compose --env-file web/.env.prod -f web/docker-compose.prod.yaml up -d
``` ```
## Service Dependencies ## Service Dependencies
@@ -194,13 +195,6 @@ volumes:
Test results are written to host filesystem for CI/CD integration. 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 ## Networks
Each environment uses isolated bridge networks: Each environment uses isolated bridge networks:
@@ -223,7 +217,9 @@ environment:
DB_NAME: "${DB_NAME}" DB_NAME: "${DB_NAME}"
DB_USER: "${DB_USER}" DB_USER: "${DB_USER}"
DB_PASSWORD: "${DB_PASSWORD}" DB_PASSWORD: "${DB_PASSWORD}"
JWT_SECRET: "${JWT_SECRET}" ACCESS_TOKEN_SECRET: "${ACCESS_TOKEN_SECRET}"
REFRESH_TOKEN_SECRET: "${REFRESH_TOKEN_SECRET}"
CONFIRMATION_TOKEN_SECRET: "${CONFIRMATION_TOKEN_SECRET}"
``` ```
For complete list, see [Environment Variables](environment-variables.md). For complete list, see [Environment Variables](environment-variables.md).
@@ -234,7 +230,7 @@ For complete list, see [Environment Variables](environment-variables.md).
```bash ```bash
# Running services # Running services
docker compose -f docker-compose.dev.yaml ps docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml ps
# All containers (including stopped) # All containers (including stopped)
docker ps -a docker ps -a
@@ -244,13 +240,13 @@ docker ps -a
```bash ```bash
# All services # All services
docker compose -f docker-compose.dev.yaml logs -f docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml logs -f
# Specific service # Specific service
docker compose -f docker-compose.dev.yaml logs -f api.core docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml logs -f api.core
# Last 100 lines # Last 100 lines
docker compose -f docker-compose.dev.yaml logs --tail=100 api.core docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml logs --tail=100 api.core
``` ```
### Execute Commands in Container ### Execute Commands in Container
@@ -267,39 +263,39 @@ docker exec dev-env-sqlserver /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -
```bash ```bash
# Restart all services # Restart all services
docker compose -f docker-compose.dev.yaml restart docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml restart
# Restart specific service # Restart specific service
docker compose -f docker-compose.dev.yaml restart api.core docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml restart api.core
# Rebuild and restart # Rebuild and restart
docker compose -f docker-compose.dev.yaml up -d --build api.core docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml up -d --build api.core
``` ```
### Build Images ### Build Images
```bash ```bash
# Build all images # Build all images
docker compose -f docker-compose.dev.yaml build docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml build
# Build specific service # Build specific service
docker compose -f docker-compose.dev.yaml build api.core docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml build api.core
# Build without cache # Build without cache
docker compose -f docker-compose.dev.yaml build --no-cache docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml build --no-cache
``` ```
### Clean Up ### Clean Up
```bash ```bash
# Stop and remove containers # Stop and remove containers
docker compose -f docker-compose.dev.yaml down docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml down
# Remove containers and volumes # Remove containers and volumes
docker compose -f docker-compose.dev.yaml down -v docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml down -v
# Remove containers, volumes, and images # Remove containers, volumes, and images
docker compose -f docker-compose.dev.yaml down -v --rmi all docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml down -v --rmi all
# System-wide cleanup # System-wide cleanup
docker system prune -af --volumes docker system prune -af --volumes

View File

@@ -17,7 +17,7 @@ The application uses environment variables for:
Direct environment variable access via `Environment.GetEnvironmentVariable()`. Direct environment variable access via `Environment.GetEnvironmentVariable()`.
### Frontend (`src/Website`) ### Frontend (`web/frontend`)
The active website reads runtime values from the server environment for its auth The active website reads runtime values from the server environment for its auth
and API integration. and API integration.
@@ -130,7 +130,7 @@ ASPNETCORE_URLS=http://0.0.0.0:8080 # Binding address and port
DOTNET_RUNNING_IN_CONTAINER=true # Flag for container execution DOTNET_RUNNING_IN_CONTAINER=true # Flag for container execution
``` ```
## Frontend Variables (`src/Website`) ## Frontend Variables (`web/frontend`)
The active website does not use the old Next.js/Prisma environment model. Its The active website does not use the old Next.js/Prisma environment model. Its
core runtime variables are: core runtime variables are:
@@ -147,7 +147,7 @@ NODE_ENV=development # Standard Node runtime mode
- **Required**: Yes for local development - **Required**: Yes for local development
- **Default in code**: `http://localhost:8080` - **Default in code**: `http://localhost:8080`
- **Used by**: `src/Website/app/lib/auth.server.ts` - **Used by**: `web/frontend/app/lib/auth.server.ts`
- **Purpose**: Routes website auth actions to the .NET API - **Purpose**: Routes website auth actions to the .NET API
#### `SESSION_SECRET` #### `SESSION_SECRET`
@@ -163,15 +163,27 @@ NODE_ENV=development # Standard Node runtime mode
- **Typical values**: `development`, `production`, `test` - **Typical values**: `development`, `production`, `test`
- **Purpose**: Controls secure cookie behavior and runtime mode - **Purpose**: Controls secure cookie behavior and runtime mode
### Admin Account (Seeding) ### SMTP Configuration (Backend)
Read by `Infrastructure.Email/SmtpEmailProvider.cs` for sending confirmation
and account emails.
```bash ```bash
ADMIN_PASSWORD=SecureAdminPassword123! # Initial admin password for seeding SMTP_HOST=mailpit # Required, no default
SMTP_PORT=1025 # Optional, defaults to 587
SMTP_USERNAME=<username> # Optional, no default
SMTP_PASSWORD=<password> # Optional, no default
SMTP_USE_SSL=false # Optional, defaults to true
SMTP_FROM_EMAIL=noreply@thebiergarten.app # Required, no default
SMTP_FROM_NAME=The Biergarten App # Optional, defaults to "The Biergarten"
``` ```
- **Required**: No (only needed for seeding) - **Implementation**: `Infrastructure.Email/SmtpEmailProvider.cs` throws on
- **Purpose**: Sets admin account password during database seeding startup if `SMTP_HOST` or `SMTP_FROM_EMAIL` is missing
- **Security**: Use strong password, change immediately in production - **Local dev**: point at the `mailpit` Docker service (SMTP on port 1025, web
UI on http://localhost:8025)
- **Production**: point at a real provider (SendGrid, Mailgun, Amazon SES,
etc.)
## Docker-Specific Variables ## Docker-Specific Variables
@@ -191,34 +203,33 @@ MSSQL_PID=Express # SQL Server edition (Express, Developer, Ente
## Environment File Structure ## Environment File Structure
### Backend/Docker (Root Directory) ### Backend/Docker (`web/` Directory)
``` ```
.env.example # Template (tracked in Git) web/.env.example # Template (tracked in Git)
.env.dev # Development config (gitignored) web/.env.dev # Development config (gitignored)
.env.test # Testing config (gitignored) web/.env.test # Testing config (gitignored)
.env.prod # Production config (gitignored) web/.env.prod # Production config (gitignored)
``` ```
**Setup**: **Setup**:
```bash ```bash
cp .env.example .env.dev cp web/.env.example web/.env.dev
# Edit .env.dev with your values # Edit web/.env.dev with your values
``` ```
## Legacy Frontend Variables ## Legacy Frontend Variables
Variables for the archived Next.js frontend (`src/Website-v1`) have been removed Variables for the archived Next.js frontend (`archive/next-js-web-app/`) have
from this active reference. See been removed from this active reference, since that app is retained for
[archive/legacy-website-v1.md](archive/legacy-website-v1.md) if you need the reference only and is not run as part of the active stack.
legacy Prisma, Cloudinary, Mapbox, or SparkPost notes.
**Docker Compose Mapping**: **Docker Compose Mapping**:
- `docker-compose.dev.yaml``.env.dev` - `web/docker-compose.dev.yaml``web/.env.dev`
- `docker-compose.test.yaml``.env.test` - `web/docker-compose.test.yaml``web/.env.test`
- `docker-compose.prod.yaml``.env.prod` - `web/docker-compose.prod.yaml``web/.env.prod`
## Variable Reference Table ## Variable Reference Table
@@ -234,6 +245,13 @@ legacy Prisma, Cloudinary, Mapbox, or SparkPost notes.
| `REFRESH_TOKEN_SECRET` | ✓ | | ✓ | Yes | Refresh token signing | | `REFRESH_TOKEN_SECRET` | ✓ | | ✓ | Yes | Refresh token signing |
| `CONFIRMATION_TOKEN_SECRET` | ✓ | | ✓ | Yes | Confirmation token signing | | `CONFIRMATION_TOKEN_SECRET` | ✓ | | ✓ | Yes | Confirmation token signing |
| `WEBSITE_BASE_URL` | ✓ | | | Yes | Website URL for emails | | `WEBSITE_BASE_URL` | ✓ | | | Yes | Website URL for emails |
| `SMTP_HOST` | ✓ | | ✓ | Yes | SMTP server host |
| `SMTP_PORT` | ✓ | | ✓ | No | Defaults to `587` |
| `SMTP_USERNAME` | ✓ | | ✓ | No | SMTP auth username |
| `SMTP_PASSWORD` | ✓ | | ✓ | No | SMTP auth password |
| `SMTP_USE_SSL` | ✓ | | ✓ | No | Defaults to `true` |
| `SMTP_FROM_EMAIL` | ✓ | | ✓ | Yes | Email sender address |
| `SMTP_FROM_NAME` | ✓ | | ✓ | No | Defaults to `The Biergarten` |
| `API_BASE_URL` | | ✓ | | Yes | Website-to-API base URL | | `API_BASE_URL` | | ✓ | | Yes | Website-to-API base URL |
| `SESSION_SECRET` | | ✓ | | Yes | Website session signing | | `SESSION_SECRET` | | ✓ | | Yes | Website session signing |
| `NODE_ENV` | | ✓ | | No | Runtime mode | | `NODE_ENV` | | ✓ | | No | Runtime mode |
@@ -254,9 +272,12 @@ legacy Prisma, Cloudinary, Mapbox, or SparkPost notes.
Variables are validated at startup: Variables are validated at startup:
- Missing required variables cause application to fail - Missing required variables (`ACCESS_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`,
- JWT_SECRET length is enforced (min 32 chars) `CONFIRMATION_TOKEN_SECRET`, `SMTP_HOST`, `SMTP_FROM_EMAIL`, DB connection
- Connection string format is validated values) cause the application to fail with an `InvalidOperationException`
- No minimum length is enforced on the token secrets in code; the
"minimum 32 characters" guidance above is a recommendation, not an
enforced check
### Frontend Validation ### Frontend Validation
@@ -284,6 +305,13 @@ REFRESH_TOKEN_SECRET=<generated-with-openssl>
CONFIRMATION_TOKEN_SECRET=<generated-with-openssl> CONFIRMATION_TOKEN_SECRET=<generated-with-openssl>
WEBSITE_BASE_URL=http://localhost:3000 WEBSITE_BASE_URL=http://localhost:3000
# SMTP (Mailpit in dev)
SMTP_HOST=mailpit
SMTP_PORT=1025
SMTP_USE_SSL=false
SMTP_FROM_EMAIL=noreply@thebiergarten.app
SMTP_FROM_NAME=The Biergarten App
# Migration # Migration
CLEAR_DATABASE=true CLEAR_DATABASE=true

View File

@@ -1,7 +1,7 @@
# Getting Started # Getting Started
This guide covers local setup for the current Biergarten stack: the .NET backend This guide covers local setup for the current Biergarten stack: the .NET backend
in `src/Core` and the active React Router frontend in `src/Website`. in `web/backend` and the active React Router frontend in `web/frontend`.
## Prerequisites ## Prerequisites
@@ -22,7 +22,7 @@ cd the-biergarten-app
### 2. Configure Backend Environment Variables ### 2. Configure Backend Environment Variables
```bash ```bash
cp .env.example .env.dev cp web/.env.example web/.env.dev
``` ```
At minimum, ensure `.env.dev` includes valid database and token values: At minimum, ensure `.env.dev` includes valid database and token values:
@@ -43,20 +43,21 @@ See [Environment Variables](environment-variables.md) for the full list.
### 3. Start the Backend Stack ### 3. Start the Backend Stack
```bash ```bash
docker compose -f docker-compose.dev.yaml up -d docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml up -d
``` ```
This starts SQL Server, migrations, seeding, and the API. This starts SQL Server, migrations, seeding, the API, and Mailpit (local dev SMTP).
Available endpoints: Available endpoints:
- API Swagger: http://localhost:8080/swagger - API Swagger: http://localhost:8080/swagger
- Health Check: http://localhost:8080/health - Health Check: http://localhost:8080/health
- Mailpit UI: http://localhost:8025
### 4. Start the Active Frontend ### 4. Start the Active Frontend
```bash ```bash
cd src/Website cd web/frontend
npm install npm install
API_BASE_URL=http://localhost:8080 SESSION_SECRET=dev-secret-change-me npm run dev API_BASE_URL=http://localhost:8080 SESSION_SECRET=dev-secret-change-me npm run dev
``` ```
@@ -71,7 +72,7 @@ Required frontend runtime variables for local work:
### 5. Optional: Run Storybook ### 5. Optional: Run Storybook
```bash ```bash
cd src/Website cd web/frontend
npm run storybook npm run storybook
``` ```
@@ -82,15 +83,15 @@ Storybook runs at http://localhost:6006 by default.
### Backend ### Backend
```bash ```bash
docker compose -f docker-compose.dev.yaml logs -f docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml logs -f
docker compose -f docker-compose.dev.yaml down docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml down
docker compose -f docker-compose.dev.yaml down -v docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml down -v
``` ```
### Frontend ### Frontend
```bash ```bash
cd src/Website cd web/frontend
npm run lint npm run lint
npm run typecheck npm run typecheck
npm run format:check npm run format:check
@@ -115,7 +116,7 @@ export WEBSITE_BASE_URL="http://localhost:3000"
### 2. Run Migrations and Seed ### 2. Run Migrations and Seed
```bash ```bash
cd src/Core cd web/backend
dotnet run --project Database/Database.Migrations/Database.Migrations.csproj dotnet run --project Database/Database.Migrations/Database.Migrations.csproj
dotnet run --project Database/Database.Seed/Database.Seed.csproj dotnet run --project Database/Database.Seed/Database.Seed.csproj
``` ```
@@ -128,12 +129,11 @@ dotnet run --project API/API.Core/API.Core.csproj
## Legacy Frontend Note ## Legacy Frontend Note
The previous Next.js frontend now lives in `src/Website-v1` and is not the The previous Next.js frontend lives in `archive/next-js-web-app/` for reference
active website. Legacy setup details have been moved to only and is not the active website.
[docs/archive/legacy-website-v1.md](archive/legacy-website-v1.md).
## Next Steps ## Next Steps
- Review [Architecture](architecture.md) - Review [Architecture](../architecture.md)
- Run backend and frontend checks from [Testing](testing.md) - Run backend and frontend checks from [Testing](testing.md)
- Use [Docker Guide](docker.md) for container troubleshooting - Use [Docker Guide](docker.md) for container troubleshooting

View File

@@ -7,9 +7,13 @@ Biergarten App.
The project uses a multi-layered testing approach across backend and frontend: The project uses a multi-layered testing approach across backend and frontend:
- **API.Specs** - BDD integration tests using Reqnroll (Gherkin) - **API.Specs** - BDD integration tests using Reqnroll (Gherkin), run against
- **Infrastructure.Repository.Tests** - Unit tests for data access layer a live, seeded database
- **Service.Auth.Tests** - Unit tests for authentication business logic - **Features.\*.Tests** - One unit test project per backend feature slice
(`Features.Auth.Tests`, `Features.Breweries.Tests`,
`Features.UserManagement.Tests`, `Features.Emails.Tests`), covering that
slice's command/query handlers and its own repository (mocked with Moq /
DbMocker, no real database required)
- **Storybook Vitest project** - Browser-based interaction tests for shared - **Storybook Vitest project** - Browser-based interaction tests for shared
website stories website stories
- **Storybook Playwright suite** - Browser checks against Storybook-rendered - **Storybook Playwright suite** - Browser checks against Storybook-rendered
@@ -21,7 +25,7 @@ The easiest way to run all tests is using Docker Compose, which sets up an
isolated test environment: isolated test environment:
```bash ```bash
docker compose -f docker-compose.test.yaml up --abort-on-container-exit docker compose --env-file web/.env.test -f web/docker-compose.test.yaml up --abort-on-container-exit
``` ```
This command: This command:
@@ -41,15 +45,17 @@ ls -la test-results/
# View specific test results # View specific test results
cat test-results/api-specs/results.trx cat test-results/api-specs/results.trx
cat test-results/repository-tests/results.trx cat test-results/Features.Auth.Tests.trx
cat test-results/service-auth-tests/results.trx cat test-results/Features.Breweries.Tests.trx
cat test-results/Features.UserManagement.Tests.trx
cat test-results/Features.Emails.Tests.trx
``` ```
### Clean Up ### Clean Up
```bash ```bash
# Remove test containers and volumes # Remove test containers and volumes
docker compose -f docker-compose.test.yaml down -v docker compose --env-file web/.env.test -f web/docker-compose.test.yaml down -v
``` ```
## Running Tests Locally ## Running Tests Locally
@@ -59,7 +65,7 @@ You can run individual test projects locally without Docker:
### Integration Tests (API.Specs) ### Integration Tests (API.Specs)
```bash ```bash
cd src/Core cd web/backend
dotnet test API/API.Specs/API.Specs.csproj dotnet test API/API.Specs/API.Specs.csproj
``` ```
@@ -69,32 +75,31 @@ dotnet test API/API.Specs/API.Specs.csproj
- Database migrated and seeded - Database migrated and seeded
- Environment variables set (DB connection, JWT secret) - Environment variables set (DB connection, JWT secret)
### Repository Tests ### Feature Slice Unit Tests
Each feature slice has its own test project, covering its command/query
handlers and repository:
```bash ```bash
cd src/Core cd web/backend
dotnet test Infrastructure/Infrastructure.Repository.Tests/Infrastructure.Repository.Tests.csproj dotnet test Features/Features.Auth.Tests/Features.Auth.Tests.csproj
dotnet test Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj
dotnet test Features/Features.UserManagement.Tests/Features.UserManagement.Tests.csproj
dotnet test Features/Features.Emails.Tests/Features.Emails.Tests.csproj
# Or run all of them at once via the solution:
for proj in Features/*.Tests; do dotnet test "$proj"; done
``` ```
**Requirements**: **Requirements**:
- SQL Server instance running (uses mock data) - No database required (handlers use Moq; repository tests use DbMocker to
simulate SQL Server responses)
### Service Tests
```bash
cd src/Core
dotnet test Service/Service.Auth.Tests/Service.Auth.Tests.csproj
```
**Requirements**:
- No database required (uses Moq for mocking)
### Frontend Storybook Tests ### Frontend Storybook Tests
```bash ```bash
cd src/Website cd web/frontend
npm install npm install
npm run test:storybook npm run test:storybook
``` ```
@@ -108,7 +113,7 @@ npm run test:storybook
### Frontend Playwright Storybook Tests ### Frontend Playwright Storybook Tests
```bash ```bash
cd src/Website cd web/frontend
npm install npm install
npm run test:storybook:playwright npm run test:storybook:playwright
``` ```
@@ -124,27 +129,28 @@ npm run test:storybook:playwright
### Current Coverage ### Current Coverage
**Authentication & User Management**: **Features.Auth.Tests**:
- User registration with validation - User registration with validation
- User login with JWT token generation - User login with JWT token generation
- Password hashing and verification (Argon2id) - Password hashing and verification (Argon2id)
- JWT token generation and claims - Email confirmation and confirmation-email resend
- Invalid credentials handling - Refresh token exchange
- 404 error responses - JWT token generation, validation, and claims handling
- `IAuthRepository` data access (DbMocker-backed)
- Invalid credentials and 404 error responses (via API.Specs)
**Repository Layer**: **Features.Breweries.Tests**:
- User account creation - Brewery create/update/delete commands and get-by-id/get-all queries
- User credential management
- GetUserByUsername queries
- Stored procedure execution
**Service Layer**: **Features.UserManagement.Tests**:
- Login service with password verification - User get-by-id/get-all queries and the (currently unrouted) update command
- Register service with validation
- Business logic for authentication flow **Features.Emails.Tests**:
- Registration and resend-confirmation email dispatch handlers
**Frontend UI Coverage**: **Frontend UI Coverage**:
@@ -156,10 +162,7 @@ npm run test:storybook:playwright
### Planned Coverage ### Planned Coverage
- [ ] Email verification workflow
- [ ] Password reset functionality - [ ] Password reset functionality
- [ ] Token refresh mechanism
- [ ] Brewery data management
- [ ] Beer post operations - [ ] Beer post operations
- [ ] User follow/unfollow - [ ] User follow/unfollow
- [ ] Image upload service - [ ] Image upload service
@@ -204,21 +207,28 @@ npm run test:storybook:playwright
``` ```
API.Specs/ API.Specs/
├── Features/ ├── Features/
│ ├── Authentication.feature # Login/register scenarios │ ├── Registration.feature # Registration scenarios
── UserManagement.feature # User CRUD scenarios ── Login.feature # Login scenarios
│ ├── Confirmation.feature # Email confirmation scenarios
│ ├── ResendConfirmation.feature # Resend-confirmation scenarios
│ ├── TokenRefresh.feature # Refresh token scenarios
│ ├── AccessTokenValidation.feature # Protected endpoint access scenarios
│ └── NotFound.feature # 404 handling
├── Steps/ ├── Steps/
│ ├── AuthenticationSteps.cs # Step definitions │ ├── AuthSteps.cs # Step definitions for the Auth features
│ └── UserManagementSteps.cs │ └── ApiGeneralSteps.cs # Shared/general step definitions
── Mocks/ ── Mocks/
── TestApiFactory.cs # Test server setup ── MockEmailDispatcher.cs # Substitutes Features.Emails' IEmailDispatcher
│ └── MockEmailProvider.cs # Substitutes Infrastructure.Email's IEmailProvider
└── TestApiFactory.cs # Test server setup (swaps in the mocks above)
``` ```
**Example Feature**: **Example Feature**:
```gherkin ```gherkin
Feature: User Authentication Feature: User Registration
As a user As a user
I want to register and login I want to register
So that I can access the platform So that I can access the platform
Scenario: Successful user registration Scenario: Successful user registration
@@ -228,45 +238,51 @@ Scenario: Successful user registration
And my account should be created And my account should be created
``` ```
### Infrastructure.Repository.Tests ### Features.Auth.Tests
``` ```
Infrastructure.Repository.Tests/ Features.Auth.Tests/
├── AuthRepositoryTests.cs # Auth repository tests ├── Commands/
├── UserAccountRepositoryTests.cs # User account tests │ ├── RegisterUserHandlerTests.cs
└── TestFixtures/ │ ├── ConfirmUserHandlerTests.cs
── DatabaseFixture.cs # Shared test setup ── ResendConfirmationEmailHandlerTests.cs
│ └── RefreshTokenHandlerTests.cs
├── Queries/
│ └── LoginHandlerTests.cs
├── Repository/
│ └── AuthRepositoryTests.cs # DbMocker-backed repository tests
└── Services/
├── TokenServiceRefreshTests.cs
└── TokenServiceValidationTests.cs
``` ```
### Service.Auth.Tests Each of the other three slices (`Features.Breweries.Tests`,
`Features.UserManagement.Tests`, `Features.Emails.Tests`) follows the same
``` shape: a `Commands/`/`Queries/` folder with one test file per handler.
Service.Auth.Tests/
├── LoginService.test.cs # Login business logic tests
└── RegisterService.test.cs # Registration business logic tests
```
## Writing Tests ## Writing Tests
### Unit Test Example (xUnit) ### Unit Test Example (xUnit)
```csharp ```csharp
public class LoginServiceTests public class LoginHandlerTests
{ {
[Fact] [Fact]
public async Task LoginAsync_ValidCredentials_ReturnsToken() public async Task Handle_WithValidData_ReturnsPayloadWithMatchingUsername()
{ {
// Arrange // Arrange
var mockRepo = new Mock<IAuthRepository>(); var authRepoMock = new Mock<IAuthRepository>();
var mockJwt = new Mock<IJwtService>(); var passwordInfraMock = new Mock<IPasswordInfrastructure>();
var service = new AuthService(mockRepo.Object, mockJwt.Object); var tokenServiceMock = new Mock<ITokenService>();
var handler = new LoginHandler(authRepoMock.Object, passwordInfraMock.Object, tokenServiceMock.Object);
// ...set up mocks for a known user/credential...
// Act // Act
var result = await service.LoginAsync("testuser", "password123"); var result = await handler.Handle(new LoginQuery("testuser", "password123"), CancellationToken.None);
// Assert // Assert
result.Should().NotBeNull(); result.Should().NotBeNull();
result.Token.Should().NotBeNullOrEmpty(); result.AccessToken.Should().NotBeNullOrEmpty();
} }
} }
``` ```
@@ -288,9 +304,9 @@ configuration:
```bash ```bash
# CI/CD command # CI/CD command
docker compose -f docker-compose.test.yaml build docker compose --env-file web/.env.test -f web/docker-compose.test.yaml build
docker compose -f docker-compose.test.yaml up --abort-on-container-exit docker compose --env-file web/.env.test -f web/docker-compose.test.yaml up --abort-on-container-exit
docker compose -f docker-compose.test.yaml down -v docker compose --env-file web/.env.test -f web/docker-compose.test.yaml down -v
``` ```
Exit codes: Exit codes:
@@ -302,7 +318,7 @@ Frontend UI checks should also be included in CI for the active website
workspace: workspace:
```bash ```bash
cd src/Website cd web/frontend
npm ci npm ci
npm run test:storybook npm run test:storybook
npm run test:storybook:playwright npm run test:storybook:playwright
@@ -315,7 +331,7 @@ npm run test:storybook:playwright
Ensure SQL Server is running and environment variables are set: Ensure SQL Server is running and environment variables are set:
```bash ```bash
docker compose -f docker-compose.test.yaml ps docker compose --env-file web/.env.test -f web/docker-compose.test.yaml ps
``` ```
### Port Conflicts ### Port Conflicts
@@ -328,13 +344,13 @@ If port 1433 is in use, stop other SQL Server instances or modify the port in
Clean up test database: Clean up test database:
```bash ```bash
docker compose -f docker-compose.test.yaml down -v docker compose --env-file web/.env.test -f web/docker-compose.test.yaml down -v
``` ```
### View Container Logs ### View Container Logs
```bash ```bash
docker compose -f docker-compose.test.yaml logs <service-name> docker compose --env-file web/.env.test -f web/docker-compose.test.yaml logs <service-name>
``` ```
## Best Practices ## Best Practices

View File

@@ -2,8 +2,7 @@
## Overview ## Overview
The Core project implements comprehensive JWT token validation across three The Core project validates JWTs across three token types:
token types:
- **Access Tokens**: Short-lived (1 hour) tokens for API authentication - **Access Tokens**: Short-lived (1 hour) tokens for API authentication
- **Refresh Tokens**: Long-lived (21 days) tokens for obtaining new access - **Refresh Tokens**: Long-lived (21 days) tokens for obtaining new access
@@ -15,7 +14,7 @@ token types:
### Infrastructure Layer ### Infrastructure Layer
#### [ITokenInfrastructure](Infrastructure.Jwt/ITokenInfrastructure.cs) #### [ITokenInfrastructure](../../web/backend/Infrastructure/Infrastructure.Jwt/ITokenInfrastructure.cs)
Low-level JWT operations. Low-level JWT operations.
@@ -25,77 +24,92 @@ Low-level JWT operations.
- `ValidateJwtAsync()` - Validates token signature, expiration, and format - `ValidateJwtAsync()` - Validates token signature, expiration, and format
**Implementation:** **Implementation:**
[JwtInfrastructure.cs](Infrastructure.Jwt/JwtInfrastructure.cs) [JwtInfrastructure.cs](../../web/backend/Infrastructure/Infrastructure.Jwt/JwtInfrastructure.cs)
- Uses Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler - Uses Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler
- Algorithm: HS256 (HMAC-SHA256) - Algorithm: HS256 (HMAC-SHA256)
- Validates token lifetime, signature, and well-formedness - Validates token lifetime, signature, and well-formedness
### Service Layer ### Features.Auth Slice
#### [ITokenValidationService](Service.Auth/ITokenValidationService.cs) #### [ITokenService](../../web/backend/Features/Features.Auth/Services/ITokenService.cs)
High-level token validation with context (token type, user extraction). Both token generation and validation live on the same slice-internal
service (there is no separate validation service/class).
**Methods:** **Generation methods:**
- `ValidateAccessTokenAsync(string token)` - Validates access tokens
- `ValidateRefreshTokenAsync(string token)` - Validates refresh tokens
- `ValidateConfirmationTokenAsync(string token)` - Validates confirmation tokens
**Returns:** `ValidatedToken` record containing:
- `UserId` (Guid)
- `Username` (string)
- `Principal` (ClaimsPrincipal) - Full JWT claims
**Implementation:**
[TokenValidationService.cs](Service.Auth/TokenValidationService.cs)
- Reads token secrets from environment variables
- Extracts and validates claims (Sub, UniqueName)
- Throws `UnauthorizedException` on validation failure
#### [ITokenService](Service.Auth/ITokenService.cs)
Token generation (existing service extended).
**Methods:**
- `GenerateAccessToken(UserAccount)` - Creates 1-hour access token - `GenerateAccessToken(UserAccount)` - Creates 1-hour access token
- `GenerateRefreshToken(UserAccount)` - Creates 21-day refresh token - `GenerateRefreshToken(UserAccount)` - Creates 21-day refresh token
- `GenerateConfirmationToken(UserAccount)` - Creates 30-minute confirmation - `GenerateConfirmationToken(UserAccount)` - Creates 30-minute confirmation
token token
**Validation methods:**
- `ValidateAccessTokenAsync(string token)` - Validates access tokens
- `ValidateRefreshTokenAsync(string token)` - Validates refresh tokens
- `ValidateConfirmationTokenAsync(string token)` - Validates confirmation tokens
**Returns (validation):** `ValidatedToken` record containing:
- `UserId` (Guid)
- `Username` (string)
- `Principal` (ClaimsPrincipal) - Full JWT claims
**Implementation:** [TokenService.cs](../../web/backend/Features/Features.Auth/Services/TokenService.cs)
- Reads token secrets from environment variables
- Extracts and validates claims (Sub, UniqueName)
- Throws `UnauthorizedException` on validation failure
`TokenService` is registered by `Features.Auth`'s own
`AddFeaturesAuth()` extension method, except for the lower-level
`ITokenInfrastructure` (JWT signing/verification) it depends on, which is
registered by the host (`API.Core/Program.cs`) since
`JwtAuthenticationHandler` (host-level auth middleware) also depends on
it directly.
### Integration Points ### Integration Points
#### [ConfirmationService](Service.Auth/IConfirmationService.cs) #### [ConfirmUserHandler](../../web/backend/Features/Features.Auth/Commands/ConfirmUser/ConfirmUserHandler.cs)
**Flow:** **Flow:**
1. Receives confirmation token from user 1. Receives confirmation token from user via `ConfirmUserCommand`
2. Calls `TokenValidationService.ValidateConfirmationTokenAsync()` 2. Calls `ITokenService.ValidateConfirmationTokenAsync()`
3. Extracts user ID from validated token 3. Extracts user ID from validated token
4. Calls `AuthRepository.ConfirmUserAccountAsync()` to update database 4. Calls `IAuthRepository.ConfirmUserAccountAsync()` to update database
5. Returns confirmation result 5. Returns confirmation result
#### [RefreshTokenService](Service.Auth/RefreshTokenService.cs) #### [ResendConfirmationEmailHandler](../../web/backend/Features/Features.Auth/Commands/ResendConfirmationEmail/ResendConfirmationEmailHandler.cs)
**Flow:** **Flow:**
1. Receives refresh token from user 1. Receives a user ID via `ResendConfirmationEmailCommand`
2. Calls `TokenValidationService.ValidateRefreshTokenAsync()` 2. Looks up the user and checks they aren't already verified
3. Retrieves user account via `AuthRepository.GetUserByIdAsync()` 3. Generates a fresh confirmation token via `ITokenService`
4. Issues new access and refresh tokens via `TokenService` 4. Sends `SendResendConfirmationEmailCommand` over MediatR, handled by the
5. Returns new token pair `Features.Emails` slice, with no direct project reference between the two
slices
#### [AuthController](API.Core/Controllers/AuthController.cs) #### [RefreshTokenHandler](../../web/backend/Features/Features.Auth/Commands/RefreshToken/RefreshTokenHandler.cs)
**Flow:**
1. Receives a refresh token via `RefreshTokenCommand`
2. Delegates to `ITokenService.RefreshTokenAsync()`, which validates the
token, retrieves the user account via `IAuthRepository.GetUserByIdAsync()`,
and issues a new access/refresh token pair
3. Maps the result onto `LoginPayload`
#### [AuthController](../../web/backend/Features/Features.Auth/Controllers/AuthController.cs)
**Endpoints:** **Endpoints:**
- `POST /api/auth/register` - Register new user - `POST /api/auth/register` - Register new user
- `POST /api/auth/login` - Authenticate user - `POST /api/auth/login` - Authenticate user
- `POST /api/auth/confirm?token=...` - Confirm email - `POST /api/auth/confirm?token=...` - Confirm email
- `POST /api/auth/confirm/resend?userId=...` - Resend the confirmation email
- `POST /api/auth/refresh` - Refresh access token - `POST /api/auth/refresh` - Refresh access token
## Validation Security ## Validation Security
@@ -158,32 +172,44 @@ Validation failures return HTTP 401 Unauthorized:
1. **Generation**: During user registration (30-minute validity) 1. **Generation**: During user registration (30-minute validity)
2. **Delivery**: Emailed to user in confirmation link 2. **Delivery**: Emailed to user in confirmation link
3. **Usage**: User clicks link, token posted to `/api/auth/confirm` 3. **Usage**: User clicks link, token posted to `/api/auth/confirm`
4. **Validation**: Validated by ConfirmationService 4. **Validation**: Validated by `ConfirmUserHandler`
5. **Completion**: User account marked as confirmed 5. **Completion**: User account marked as confirmed
6. **Expiration**: Token becomes invalid after 30 minutes 6. **Expiration**: Token becomes invalid after 30 minutes
## Testing ## Testing
All of the following live in `Features.Auth.Tests`.
### Unit Tests ### Unit Tests
**TokenValidationService.test.cs** **Services/TokenServiceValidationTests.cs**
- Happy path: Valid token extraction - Happy path: Valid token extraction
- Error cases: Invalid, expired, malformed tokens - Error cases: Invalid, expired, malformed tokens
- Missing/invalid claims scenarios - Missing/invalid claims scenarios
**RefreshTokenService.test.cs** **Services/TokenServiceRefreshTests.cs**
- Successful refresh with valid token - Successful refresh with valid token
- Invalid/expired refresh token rejection - Invalid/expired refresh token rejection
- Non-existent user handling - Non-existent user handling
**ConfirmationService.test.cs** **Commands/RefreshTokenHandlerTests.cs**
- Verifies the handler maps `ITokenService.RefreshTokenAsync()`'s result onto
`LoginPayload`
**Commands/ConfirmUserHandlerTests.cs**
- Successful confirmation with valid token - Successful confirmation with valid token
- Token validation failures - Token validation failures
- User not found scenarios - User not found scenarios
**Commands/ResendConfirmationEmailHandlerTests.cs**
- Sends a fresh confirmation email when the user exists and is unverified
- No-ops when the user doesn't exist or is already verified
### BDD Tests (Reqnroll) ### BDD Tests (Reqnroll)
**TokenRefresh.feature** **TokenRefresh.feature**