mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-07-16 17:47:22 +00:00
Update documentation to reflect new architecture and previous refactors
This commit is contained in:
@@ -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/
|
||||||
|
|||||||
@@ -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 │
|
||||||
│ Service Layer (Business Logic) │
|
│ │Breweries │ UserManagement │ (no controller, │
|
||||||
│ - Authentication logic │
|
│ Controller │ Controller │ Controller │ internal only) │
|
||||||
│ - User management │
|
│ Commands/ │ Commands/ │ Commands/ │ Commands/ │
|
||||||
│ - Validation & orchestration │
|
│ Queries + │ Queries + │ Queries + │ Handlers │
|
||||||
└─────────────────────────────────────┘
|
│ Handlers │ Handlers │ Handlers │ │
|
||||||
↓
|
│ Repository │ Repository │ Repository │ EmailDispatcher │
|
||||||
┌─────────────────────────────────────┐
|
└───────────────┴───────────────┴───────────────────┴───────────────────┘
|
||||||
│ Infrastructure Layer (Tools) │
|
↓ each slice depends only on shared/domain/infra, never on another slice
|
||||||
│ - JWT token generation │
|
┌─────────────────────────┬─────────────────────────┬────────────────────┐
|
||||||
│ - Password hashing (Argon2id) │
|
│ Shared.Contracts │ Shared.Application │ Domain.Entities / │
|
||||||
│ - Email services │
|
│ (ResponseBody envelope) │ (ValidationBehavior, │ Domain.Exceptions │
|
||||||
│ - Repository implementations │
|
│ │ cross-slice email cmds) │ │
|
||||||
└─────────────────────────────────────┘
|
└─────────────────────────┴─────────────────────────┴────────────────────┘
|
||||||
↓
|
↓
|
||||||
┌─────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
│ Domain Layer (Entities) │
|
│ Infrastructure.Sql, Infrastructure.Jwt, Infrastructure.PasswordHashing, │
|
||||||
│ - UserAccount, UserCredential │
|
│ Infrastructure.Email, Infrastructure.Email.Templates │
|
||||||
│ - 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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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**
|
||||||
|
|||||||
Reference in New Issue
Block a user