From 07aedcb866601436398b7b3f77bcf8ee45f2842c Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Sat, 20 Jun 2026 01:33:05 -0400 Subject: [PATCH] Update documentation to reflect new architecture and previous refactors --- README.md | 5 +- docs/architecture.md | 276 +++++++++++++------- docs/website/diagrams-out/architecture.svg | 2 +- docs/website/diagrams-src/architecture.puml | 105 ++++++-- docs/website/diagrams-src/deployment.puml | 45 ++-- docs/website/docker.md | 62 ++--- docs/website/environment-variables.md | 78 ++++-- docs/website/getting-started.md | 30 +-- docs/website/testing.md | 168 ++++++------ docs/website/token-validation.md | 118 +++++---- 10 files changed, 543 insertions(+), 346 deletions(-) diff --git a/README.md b/README.md index 8782ec9..b5fec45 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Data generation pipeline (C++): 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) - Shared Biergarten theme system + Storybook coverage - Auth flows and account/email integration (local Mailpit in dev compose) @@ -105,7 +105,8 @@ npm run test:storybook:playwright ```text 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 tooling/ diff --git a/docs/architecture.md b/docs/architecture.md index 6f9d774..00e102f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 active website: -- **Backend**: .NET 10 Web API with SQL Server and a layered architecture -- **Frontend**: React 19 + React Router 7 website in `src/Website` -- **Architecture Style**: Layered backend plus server-rendered React frontend +- **Backend**: .NET 10 Web API with SQL Server, organized as vertical feature + slices with MediatR +- **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 -only and is documented in -[archive/legacy-website-v1.md](archive/legacy-website-v1.md). +The legacy Next.js frontend has been retained in `archive/next-js-web-app/` +for reference only. ## Diagrams 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 -- [deployment.svg](diagrams-out/deployment.svg) - Docker deployment diagram -- [authentication-flow.svg](diagrams-out/authentication-flow.svg) - +- [authentication-flow.svg](website/diagrams-out/authentication-flow.svg) - Authentication workflow -- [database-schema.svg](diagrams-out/database-schema.svg) - Database +- [database-schema.svg](website/diagrams-out/database-schema.svg) - Database relationships ## 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) │ -│ - HTTP Endpoints │ -│ - Request/Response mapping │ -│ - Swagger/OpenAPI │ -└─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────┐ -│ Service Layer (Business Logic) │ -│ - Authentication logic │ -│ - User management │ -│ - Validation & orchestration │ -└─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────┐ -│ Infrastructure Layer (Tools) │ -│ - JWT token generation │ -│ - Password hashing (Argon2id) │ -│ - Email services │ -│ - Repository implementations │ -└─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────┐ -│ Domain Layer (Entities) │ -│ - UserAccount, UserCredential │ -│ - Pure POCO classes │ -│ - No external dependencies │ -└─────────────────────────────────────┘ +┌───────────────────────────────────────────────────────────────────────┐ +│ API.Core (thin host) │ +│ - Program.cs wiring: MediatR + AddApplicationPart per slice │ +│ - Swagger/OpenAPI, JWT auth middleware, global exception filter │ +└───────────────────────────────────────────────────────────────────────┘ + ↓ 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) │ │ +└─────────────────────────┴─────────────────────────┴────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────────────┐ +│ Infrastructure.Sql, Infrastructure.Jwt, Infrastructure.PasswordHashing, │ +│ Infrastructure.Email, Infrastructure.Email.Templates │ +└─────────────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ 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 #### 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**: -- Controllers (`AuthController`, `UserController`) -- Middleware for error handling -- Swagger/OpenAPI documentation -- Health check endpoints +- `Program.cs`: registers MediatR (scanning every `Features.*` assembly), + FluentValidation, and uses `AddApplicationPart` so each slice's controllers + are discovered by MVC +- `GlobalException.cs`: global exception filter (host-level cross-cutting + concern) +- `Authentication/JwtAuthenticationHandler.cs`: JWT auth scheme middleware +- Swagger/OpenAPI documentation, health check endpoints **Dependencies**: -- Service layer -- ASP.NET Core framework +- Every `Features.*` project (for controller/MediatR discovery) +- `Shared.Contracts`, `Shared.Application` +- `Infrastructure.Jwt` (for the auth middleware) **Rules**: -- No business logic -- Only request/response transformation -- Delegates all work to Service layer +- No controllers, no business logic, no feature-specific contracts +- Exists purely to host and wire up the feature slices -#### 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//` and `Queries//`: 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**: -- Authentication services (login, registration) -- User management services -- Business rule validation -- Transaction coordination - -**Dependencies**: - -- Infrastructure layer (repositories, JWT, password hashing) -- Domain entities +- `Shared.Contracts`: `ResponseBody`/`ResponseBody`, the API response + envelope every controller returns +- `Shared.Application`: `ValidationBehavior` (the + MediatR pipeline behavior that runs FluentValidation before a handler + executes) and the cross-slice email commands + (`SendRegistrationEmailCommand`, `SendResendConfirmationEmailCommand`) **Rules**: -- Contains all business logic -- Coordinates multiple infrastructure components -- No direct database access (uses repositories) -- Returns domain models, not DTOs +- Kept deliberately small: this is the exception to "no slice depends on + another slice," not a general-purpose dumping ground #### Infrastructure Layer -**Purpose**: Technical capabilities and external integrations +**Purpose**: Technical capabilities and external integrations, shared by +whichever slices need them **Components**: -- **Infrastructure.Repository**: Data access via stored procedures +- **Infrastructure.Sql**: generic ADO.NET connection/command plumbing + (`ISqlConnectionFactory`, the abstract `Repository` base class), not + domain-specific; each slice's own `Repository/` folder builds on this - **Infrastructure.Jwt**: JWT token generation and validation - **Infrastructure.PasswordHashing**: Argon2id password hashing -- **Infrastructure.Email**: Email sending capabilities -- **Infrastructure.Email.Templates**: Email template rendering +- **Infrastructure.Email**: Email sending capabilities (SMTP/MailKit) +- **Infrastructure.Email.Templates**: Email template rendering (Razor + components) **Dependencies**: - Domain entities -- External libraries (ADO.NET, JWT, Argon2, etc.) +- External libraries (ADO.NET, JWT, Argon2, MailKit, etc.) **Rules**: -- Implements technical concerns -- No business logic -- Reusable across services +- Implements technical concerns only, no business logic +- Reusable across slices #### Domain Layer (`Domain.Entities`) @@ -163,21 +215,55 @@ The backend follows a strict layered architecture: ### 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` + (`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; + +public class CreateBreweryHandler(IBreweryRepository repository) : IRequestHandler +{ + public async Task Handle(CreateBreweryCommand request, CancellationToken cancellationToken) { ... } +} +``` + #### Repository Pattern **Purpose**: Abstract database access behind interfaces -**Implementation**: +**Implementation**: each slice owns its own repository, scoped to that +feature only: -- `IAuthRepository` - Authentication queries -- `IUserAccountRepository` - User account queries -- `DefaultSqlConnectionFactory` - Connection management +- `Features.Auth/Repository/IAuthRepository.cs` +- `Features.Breweries/Repository/IBreweryRepository.cs` +- `Features.UserManagement/Repository/IUserAccountRepository.cs` +- `Infrastructure.Sql/DefaultSqlConnectionFactory.cs`: the generic + connection factory every slice's repository builds on **Benefits**: - Testable (easy to mock) - SQL-first approach (stored procedures) -- Centralized data access logic +- Each slice's data access logic is self-contained **Example**: @@ -193,17 +279,19 @@ public interface IAuthRepository **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**: -- Scoped: Repositories, Services (per request) -- Singleton: Connection factories, JWT configuration +- Scoped: Repositories, slice-internal services (per request) +- Singleton: `ISqlConnectionFactory` - Transient: Utilities, helpers #### SQL-First Approach -**Purpose**: Leverage database capabilities +**Purpose**: Push complex logic into the database **Strategy**: @@ -220,13 +308,13 @@ public interface IAuthRepository ## Frontend Architecture -### Active Website (`src/Website`) +### Active Website (`web/frontend`) The current website is a React Router 7 application with server-side rendering enabled. ```text -src/Website/ +web/frontend/ ├── app/ │ ├── components/ Shared UI such as Navbar, FormField, SubmitButton, ToastProvider │ ├── lib/ Auth helpers, schemas, and theme metadata @@ -262,10 +350,9 @@ All component styling should prefer semantic tokens such as `primary`, ### Legacy Frontend -The previous Next.js frontend has been archived at `src/Website-v1`. Active -product and engineering documentation should point to `src/Website`, while -legacy notes live in -[archive/legacy-website-v1.md](archive/legacy-website-v1.md). +The previous Next.js frontend has been archived at `archive/next-js-web-app/` +for reference only. Active product and engineering documentation should point +to `web/frontend`. ## Security Architecture @@ -390,7 +477,7 @@ scripts/ - Testing (`docker-compose.test.yaml`) - Production (`docker-compose.prod.yaml`) -For details, see [Docker Guide](docker.md). +For details, see [Docker Guide](website/docker.md). ### Health Checks @@ -416,11 +503,10 @@ healthcheck: │ Integration │ ← API.Specs (Reqnroll) │ Tests │ ├──────────────┤ - │ Unit Tests │ ← Service.Auth.Tests - │ (Service) │ Repository.Tests - ├──────────────┤ - │ Unit Tests │ - │ (Repository) │ + │ Unit Tests │ ← Features.Auth.Tests, Features.Breweries.Tests, + │ (per slice, │ Features.UserManagement.Tests, Features.Emails.Tests + │ handlers + │ (commands/queries/handlers + that slice's own + │ repository) │ repository, mocked with Moq/DbMocker) └──────────────┘ ``` @@ -431,4 +517,4 @@ healthcheck: - Mock external dependencies - Test database for integration tests -For details, see [Testing Guide](testing.md). +For details, see [Testing Guide](website/testing.md). diff --git a/docs/website/diagrams-out/architecture.svg b/docs/website/diagrams-out/architecture.svg index f7b8cdd..028ae98 100644 --- a/docs/website/diagrams-out/architecture.svg +++ b/docs/website/diagrams-out/architecture.svg @@ -1 +1 @@ -The Biergarten App - Layered ArchitectureThe Biergarten App - Layered ArchitectureAPI LayerService LayerInfrastructure LayerDomain LayerSQL ServerAPI.CoreASP.NET Core Web API- Controllers (Auth, User)- Swagger/OpenAPI- Middleware- Health ChecksService.AuthService.UserManagement- Business Logic- Validation- OrchestrationInfrastructure.RepositoryInfrastructure.JwtInfrastructure.PasswordHashingInfrastructure.EmailDomain.Entities- UserAccount- UserCredential- UserVerificationStored ProceduresTablesSQL-first approachAll queries viastored procedures \ No newline at end of file +The Biergarten App - Vertical Slice ArchitectureThe Biergarten App - Vertical Slice ArchitectureAPI.Core (thin host)Feature SlicesSharedInfrastructure LayerDomain LayerSQL ServerAPI.CoreASP.NET Core Web API host- Program.cs wiring only- MediatR + AddApplicationPartper Features.* assembly- Swagger/OpenAPI, health checks- JWT auth middleware- Global exception filterFeatures.AuthFeatures.BreweriesFeatures.UserManagementFeatures.EmailsEach slice owns its own:- Controller (HTTP endpoints)- Commands/Queries + Handlers- Validators- RepositoryNo controller - invoked onlyvia MediatR commands sentfrom other slicesShared.ContractsShared.Application- ValidationBehavior(MediatR pipeline)- Cross-slice email commands(SendRegistrationEmailCommand,SendResendConfirmationEmailCommand)Infrastructure.SqlInfrastructure.JwtInfrastructure.PasswordHashingInfrastructure.EmailInfrastructure.Email.TemplatesDomain.EntitiesDomain.Exceptions- UserAccount- UserCredential- UserVerification- BreweryPostStored ProceduresTablesSQL-first approachAll queries viastored proceduresNo Features.* projectever references anotherFeatures.* project directlyAddApplicationPart+ MediatR scanAddApplicationPart+ MediatR scanAddApplicationPart+ MediatR scanMediatR scan only(no controller)sendsSendRegistrationEmailCommandhandlesSendRegistrationEmailCommand \ No newline at end of file diff --git a/docs/website/diagrams-src/architecture.puml b/docs/website/diagrams-src/architecture.puml index ebb79db..60f7c5e 100644 --- a/docs/website/diagrams-src/architecture.puml +++ b/docs/website/diagrams-src/architecture.puml @@ -4,41 +4,67 @@ skinparam backgroundColor #FFFFFF skinparam defaultFontName Arial skinparam packageStyle rectangle -title The Biergarten App - Layered Architecture +title The Biergarten App - Vertical Slice Architecture -package "API Layer" #E3F2FD { - [API.Core\nASP.NET Core Web API] as API +package "API.Core (thin host)" #E3F2FD { + [API.Core\nASP.NET Core Web API host] as API note right of API - - Controllers (Auth, User) - - Swagger/OpenAPI - - Middleware - - Health Checks + - Program.cs wiring only + - MediatR + AddApplicationPart + per Features.* assembly + - Swagger/OpenAPI, health checks + - JWT auth middleware + - Global exception filter end note } -package "Service Layer" #F3E5F5 { - [Service.Auth] as AuthSvc - [Service.UserManagement] as UserSvc - note right of AuthSvc - - Business Logic - - Validation - - Orchestration +package "Feature Slices" #F3E5F5 { + [Features.Auth] as AuthSlice + [Features.Breweries] as BrewerySlice + [Features.UserManagement] as UserSlice + [Features.Emails] as EmailsSlice + note right of AuthSlice + 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 } package "Infrastructure Layer" #FFF3E0 { - [Infrastructure.Repository] as Repo + [Infrastructure.Sql] as Sql [Infrastructure.Jwt] as JWT [Infrastructure.PasswordHashing] as PwdHash [Infrastructure.Email] as Email + [Infrastructure.Email.Templates] as EmailTemplates } package "Domain Layer" #E8F5E9 { [Domain.Entities] as Domain + [Domain.Exceptions] as DomainExceptions note right of Domain - UserAccount - UserCredential - UserVerification + - BreweryPost end note } @@ -48,28 +74,53 @@ database "SQL Server" { } ' Relationships -API --> AuthSvc -API --> UserSvc +API ..> AuthSlice : AddApplicationPart\n+ MediatR scan +API ..> BrewerySlice : AddApplicationPart\n+ MediatR scan +API ..> UserSlice : AddApplicationPart\n+ MediatR scan +API ..> EmailsSlice : MediatR scan only\n(no controller) -AuthSvc --> Repo -AuthSvc --> JWT -AuthSvc --> PwdHash -AuthSvc --> Email +AuthSlice --> Sql +AuthSlice --> JWT +AuthSlice --> PwdHash +AuthSlice --> SharedContracts +AuthSlice --> SharedApp -UserSvc --> Repo +BrewerySlice --> Sql +BrewerySlice --> SharedContracts +BrewerySlice --> SharedApp -Repo --> SP -Repo --> Domain +UserSlice --> Sql +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 -AuthSvc --> Domain -UserSvc --> Domain +AuthSlice --> Domain +BrewerySlice --> Domain +UserSlice --> Domain +AuthSlice --> DomainExceptions ' Notes -note left of Repo +note left of Sql SQL-first approach All queries via stored procedures end note +note bottom of AuthSlice + No Features.* project + ever references another + Features.* project directly +end note + @enduml diff --git a/docs/website/diagrams-src/deployment.puml b/docs/website/diagrams-src/deployment.puml index 2d6db2a..970b916 100644 --- a/docs/website/diagrams-src/deployment.puml +++ b/docs/website/diagrams-src/deployment.puml @@ -112,38 +112,32 @@ package "Test Environment\n(docker-compose.test.yaml)" #FFF3E0 { end note } - node "Infrastructure.Repository.Tests\n(Unit Tests)" as RepoTests { - component "xUnit + DbMocker" as RepoComp + node "unit.tests\n(Features.*.Tests, one container)" as UnitTests { + component "xUnit + Moq + DbMocker" as UnitComp note right - Tests: - - AuthRepository - - UserAccountRepository - - SQL command building + Builds Dockerfile.tests, which + restores/builds against Core.slnx + then runs `dotnet test` for every + 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 end note } - node "Service.Auth.Tests\n(Unit Tests)" as SvcTests { - component "xUnit + Moq" as SvcComp - note right - Tests: - - RegisterService - - LoginService - - Token generation - - Uses: Mocked dependencies - No database or infrastructure - end note - } - } folder "test-results/\n(mounted volume)" as Results { file "api-specs/\n results.trx" as Result1 - file "repository-tests/\n results.trx" as Result2 - file "service-auth-tests/\n results.trx" as Result3 + file "Features.Auth.Tests.trx" as Result2 + file "Features.Breweries.Tests.trx" as Result3 + file "Features.UserManagement.Tests.trx" as Result4 + file "Features.Emails.Tests.trx" as Result5 note bottom TRX format @@ -170,17 +164,16 @@ DevAPI .up.> DevSeed : depends_on TestMig --> TestDB : 1. Migrate TestSeed --> TestDB : 2. Seed Specs --> TestDB : 3. Integration test -RepoTests ..> TestDB : Mock (no connection) -SvcTests ..> TestDB : Mock (no connection) +UnitTests ..> TestDB : depends_on for startup\nordering only (Moq/DbMocker,\nno real connection) TestMig .up.> TestDB : depends_on TestSeed .up.> TestMig : depends_on Specs .up.> TestSeed : depends_on +UnitTests .up.> TestSeed : depends_on ' Test results export Specs --> Results : Export TRX -RepoTests --> Results : Export TRX -SvcTests --> Results : Export TRX +UnitTests --> Results : Export TRX (one .trx per slice) ' Network notes note bottom of DevDB diff --git a/docs/website/docker.md b/docs/website/docker.md index a66ce68..b4b9de0 100644 --- a/docs/website/docker.md +++ b/docs/website/docker.md @@ -13,7 +13,7 @@ The project uses Docker Compose to orchestrate multiple services: - .NET API - Test runners -See the [deployment diagram](diagrams/pdf/deployment.pdf) for visual +See the [deployment diagram](diagrams-out/deployment.svg) for visual representation. ## Docker Compose Environments @@ -25,9 +25,10 @@ representation. **Features**: - Persistent SQL Server volume -- Hot reload support +- NuGet package cache volume (speeds up rebuilds) - Swagger UI enabled - Seed data included +- Local Mailpit SMTP server for dev email testing - `CLEAR_DATABASE=true` (drops and recreates schema) **Services**: @@ -37,28 +38,30 @@ sqlserver # SQL Server 2022 (port 1433) database.migrations # DbUp migrations database.seed # Seed initial data api.core # Web API (ports 8080, 8081) +mailpit # Local dev SMTP server + UI (port 8025) ``` **Start Development Environment**: ```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**: - API Swagger: http://localhost:8080/swagger - Health Check: http://localhost:8080/health +- Mailpit UI: http://localhost:8025 - SQL Server: localhost:1433 (sa credentials from .env.dev) **Stop Environment**: ```bash # 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) -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`) @@ -80,24 +83,22 @@ sqlserver # Test database database.migrations # Fresh schema database.seed # Test data api.specs # Reqnroll BDD tests -repository.tests # Repository unit tests -service.auth.tests # Service unit tests +unit.tests # All Features.*.Tests unit test projects ``` **Run Tests**: ```bash # 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 ls -la test-results/ cat test-results/api-specs/results.trx -cat test-results/repository-tests/results.trx -cat test-results/service-auth-tests/results.trx +cat test-results/Features.Auth.Tests.trx # 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`) @@ -124,7 +125,7 @@ api.core # Production API **Deploy Production**: ```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 @@ -194,13 +195,6 @@ volumes: Test results are written to host filesystem for CI/CD integration. -**Code Volumes** (development only): - -```yaml -volumes: - - ./src:/app/src # Hot reload for development -``` - ## Networks Each environment uses isolated bridge networks: @@ -223,7 +217,9 @@ environment: DB_NAME: "${DB_NAME}" DB_USER: "${DB_USER}" 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). @@ -234,7 +230,7 @@ For complete list, see [Environment Variables](environment-variables.md). ```bash # 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) docker ps -a @@ -244,13 +240,13 @@ docker ps -a ```bash # 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 -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 -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 @@ -267,39 +263,39 @@ docker exec dev-env-sqlserver /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa - ```bash # 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 -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 -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 ```bash # 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 -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 -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 ```bash # 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 -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 -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 docker system prune -af --volumes diff --git a/docs/website/environment-variables.md b/docs/website/environment-variables.md index ef7cb54..4026143 100644 --- a/docs/website/environment-variables.md +++ b/docs/website/environment-variables.md @@ -17,7 +17,7 @@ The application uses environment variables for: 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 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 ``` -## Frontend Variables (`src/Website`) +## Frontend Variables (`web/frontend`) The active website does not use the old Next.js/Prisma environment model. Its core runtime variables are: @@ -147,7 +147,7 @@ NODE_ENV=development # Standard Node runtime mode - **Required**: Yes for local development - **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 #### `SESSION_SECRET` @@ -163,15 +163,27 @@ NODE_ENV=development # Standard Node runtime mode - **Typical values**: `development`, `production`, `test` - **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 -ADMIN_PASSWORD=SecureAdminPassword123! # Initial admin password for seeding +SMTP_HOST=mailpit # Required, no default +SMTP_PORT=1025 # Optional, defaults to 587 +SMTP_USERNAME= # Optional, no default +SMTP_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) -- **Purpose**: Sets admin account password during database seeding -- **Security**: Use strong password, change immediately in production +- **Implementation**: `Infrastructure.Email/SmtpEmailProvider.cs` throws on + startup if `SMTP_HOST` or `SMTP_FROM_EMAIL` is missing +- **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 @@ -191,34 +203,33 @@ MSSQL_PID=Express # SQL Server edition (Express, Developer, Ente ## Environment File Structure -### Backend/Docker (Root Directory) +### Backend/Docker (`web/` Directory) ``` -.env.example # Template (tracked in Git) -.env.dev # Development config (gitignored) -.env.test # Testing config (gitignored) -.env.prod # Production config (gitignored) +web/.env.example # Template (tracked in Git) +web/.env.dev # Development config (gitignored) +web/.env.test # Testing config (gitignored) +web/.env.prod # Production config (gitignored) ``` **Setup**: ```bash -cp .env.example .env.dev -# Edit .env.dev with your values +cp web/.env.example web/.env.dev +# Edit web/.env.dev with your values ``` ## Legacy Frontend Variables -Variables for the archived Next.js frontend (`src/Website-v1`) have been removed -from this active reference. See -[archive/legacy-website-v1.md](archive/legacy-website-v1.md) if you need the -legacy Prisma, Cloudinary, Mapbox, or SparkPost notes. +Variables for the archived Next.js frontend (`archive/next-js-web-app/`) have +been removed from this active reference, since that app is retained for +reference only and is not run as part of the active stack. **Docker Compose Mapping**: -- `docker-compose.dev.yaml` → `.env.dev` -- `docker-compose.test.yaml` → `.env.test` -- `docker-compose.prod.yaml` → `.env.prod` +- `web/docker-compose.dev.yaml` → `web/.env.dev` +- `web/docker-compose.test.yaml` → `web/.env.test` +- `web/docker-compose.prod.yaml` → `web/.env.prod` ## Variable Reference Table @@ -234,6 +245,13 @@ legacy Prisma, Cloudinary, Mapbox, or SparkPost notes. | `REFRESH_TOKEN_SECRET` | ✓ | | ✓ | Yes | Refresh token signing | | `CONFIRMATION_TOKEN_SECRET` | ✓ | | ✓ | Yes | Confirmation token signing | | `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 | | `SESSION_SECRET` | | ✓ | | Yes | Website session signing | | `NODE_ENV` | | ✓ | | No | Runtime mode | @@ -254,9 +272,12 @@ legacy Prisma, Cloudinary, Mapbox, or SparkPost notes. Variables are validated at startup: -- Missing required variables cause application to fail -- JWT_SECRET length is enforced (min 32 chars) -- Connection string format is validated +- Missing required variables (`ACCESS_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`, + `CONFIRMATION_TOKEN_SECRET`, `SMTP_HOST`, `SMTP_FROM_EMAIL`, DB connection + 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 @@ -284,6 +305,13 @@ REFRESH_TOKEN_SECRET= CONFIRMATION_TOKEN_SECRET= 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 CLEAR_DATABASE=true diff --git a/docs/website/getting-started.md b/docs/website/getting-started.md index 2ed58a3..b57fdee 100644 --- a/docs/website/getting-started.md +++ b/docs/website/getting-started.md @@ -1,7 +1,7 @@ # Getting Started 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 @@ -22,7 +22,7 @@ cd the-biergarten-app ### 2. Configure Backend Environment Variables ```bash -cp .env.example .env.dev +cp web/.env.example web/.env.dev ``` 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 ```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: - API Swagger: http://localhost:8080/swagger - Health Check: http://localhost:8080/health +- Mailpit UI: http://localhost:8025 ### 4. Start the Active Frontend ```bash -cd src/Website +cd web/frontend npm install 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 ```bash -cd src/Website +cd web/frontend npm run storybook ``` @@ -82,15 +83,15 @@ Storybook runs at http://localhost:6006 by default. ### Backend ```bash -docker compose -f docker-compose.dev.yaml logs -f -docker compose -f 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 logs -f +docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml down +docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml down -v ``` ### Frontend ```bash -cd src/Website +cd web/frontend npm run lint npm run typecheck npm run format:check @@ -115,7 +116,7 @@ export WEBSITE_BASE_URL="http://localhost:3000" ### 2. Run Migrations and Seed ```bash -cd src/Core +cd web/backend dotnet run --project Database/Database.Migrations/Database.Migrations.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 -The previous Next.js frontend now lives in `src/Website-v1` and is not the -active website. Legacy setup details have been moved to -[docs/archive/legacy-website-v1.md](archive/legacy-website-v1.md). +The previous Next.js frontend lives in `archive/next-js-web-app/` for reference +only and is not the active website. ## Next Steps -- Review [Architecture](architecture.md) +- Review [Architecture](../architecture.md) - Run backend and frontend checks from [Testing](testing.md) - Use [Docker Guide](docker.md) for container troubleshooting diff --git a/docs/website/testing.md b/docs/website/testing.md index ecd2e3e..ac1cedf 100644 --- a/docs/website/testing.md +++ b/docs/website/testing.md @@ -7,9 +7,13 @@ Biergarten App. The project uses a multi-layered testing approach across backend and frontend: -- **API.Specs** - BDD integration tests using Reqnroll (Gherkin) -- **Infrastructure.Repository.Tests** - Unit tests for data access layer -- **Service.Auth.Tests** - Unit tests for authentication business logic +- **API.Specs** - BDD integration tests using Reqnroll (Gherkin), run against + a live, seeded database +- **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 website stories - **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: ```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: @@ -41,15 +45,17 @@ ls -la test-results/ # View specific test results cat test-results/api-specs/results.trx -cat test-results/repository-tests/results.trx -cat test-results/service-auth-tests/results.trx +cat test-results/Features.Auth.Tests.trx +cat test-results/Features.Breweries.Tests.trx +cat test-results/Features.UserManagement.Tests.trx +cat test-results/Features.Emails.Tests.trx ``` ### Clean Up ```bash # 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 @@ -59,7 +65,7 @@ You can run individual test projects locally without Docker: ### Integration Tests (API.Specs) ```bash -cd src/Core +cd web/backend dotnet test API/API.Specs/API.Specs.csproj ``` @@ -69,32 +75,31 @@ dotnet test API/API.Specs/API.Specs.csproj - Database migrated and seeded - 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 -cd src/Core -dotnet test Infrastructure/Infrastructure.Repository.Tests/Infrastructure.Repository.Tests.csproj +cd web/backend +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**: -- SQL Server instance running (uses mock data) - -### Service Tests - -```bash -cd src/Core -dotnet test Service/Service.Auth.Tests/Service.Auth.Tests.csproj -``` - -**Requirements**: - -- No database required (uses Moq for mocking) +- No database required (handlers use Moq; repository tests use DbMocker to + simulate SQL Server responses) ### Frontend Storybook Tests ```bash -cd src/Website +cd web/frontend npm install npm run test:storybook ``` @@ -108,7 +113,7 @@ npm run test:storybook ### Frontend Playwright Storybook Tests ```bash -cd src/Website +cd web/frontend npm install npm run test:storybook:playwright ``` @@ -124,27 +129,28 @@ npm run test:storybook:playwright ### Current Coverage -**Authentication & User Management**: +**Features.Auth.Tests**: - User registration with validation - User login with JWT token generation - Password hashing and verification (Argon2id) -- JWT token generation and claims -- Invalid credentials handling -- 404 error responses +- Email confirmation and confirmation-email resend +- Refresh token exchange +- 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 -- User credential management -- GetUserByUsername queries -- Stored procedure execution +- Brewery create/update/delete commands and get-by-id/get-all queries -**Service Layer**: +**Features.UserManagement.Tests**: -- Login service with password verification -- Register service with validation -- Business logic for authentication flow +- User get-by-id/get-all queries and the (currently unrouted) update command + +**Features.Emails.Tests**: + +- Registration and resend-confirmation email dispatch handlers **Frontend UI Coverage**: @@ -156,10 +162,7 @@ npm run test:storybook:playwright ### Planned Coverage -- [ ] Email verification workflow - [ ] Password reset functionality -- [ ] Token refresh mechanism -- [ ] Brewery data management - [ ] Beer post operations - [ ] User follow/unfollow - [ ] Image upload service @@ -204,21 +207,28 @@ npm run test:storybook:playwright ``` API.Specs/ ├── Features/ -│ ├── Authentication.feature # Login/register scenarios -│ └── UserManagement.feature # User CRUD scenarios +│ ├── Registration.feature # Registration 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/ -│ ├── AuthenticationSteps.cs # Step definitions -│ └── UserManagementSteps.cs -└── Mocks/ - └── TestApiFactory.cs # Test server setup +│ ├── AuthSteps.cs # Step definitions for the Auth features +│ └── ApiGeneralSteps.cs # Shared/general step definitions +├── Mocks/ +│ ├── 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**: ```gherkin -Feature: User Authentication +Feature: User Registration As a user - I want to register and login + I want to register So that I can access the platform Scenario: Successful user registration @@ -228,45 +238,51 @@ Scenario: Successful user registration And my account should be created ``` -### Infrastructure.Repository.Tests +### Features.Auth.Tests ``` -Infrastructure.Repository.Tests/ -├── AuthRepositoryTests.cs # Auth repository tests -├── UserAccountRepositoryTests.cs # User account tests -└── TestFixtures/ - └── DatabaseFixture.cs # Shared test setup +Features.Auth.Tests/ +├── Commands/ +│ ├── RegisterUserHandlerTests.cs +│ ├── ConfirmUserHandlerTests.cs +│ ├── ResendConfirmationEmailHandlerTests.cs +│ └── RefreshTokenHandlerTests.cs +├── Queries/ +│ └── LoginHandlerTests.cs +├── Repository/ +│ └── AuthRepositoryTests.cs # DbMocker-backed repository tests +└── Services/ + ├── TokenServiceRefreshTests.cs + └── TokenServiceValidationTests.cs ``` -### Service.Auth.Tests - -``` -Service.Auth.Tests/ -├── LoginService.test.cs # Login business logic tests -└── RegisterService.test.cs # Registration business logic 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. ## Writing Tests ### Unit Test Example (xUnit) ```csharp -public class LoginServiceTests +public class LoginHandlerTests { [Fact] - public async Task LoginAsync_ValidCredentials_ReturnsToken() + public async Task Handle_WithValidData_ReturnsPayloadWithMatchingUsername() { // Arrange - var mockRepo = new Mock(); - var mockJwt = new Mock(); - var service = new AuthService(mockRepo.Object, mockJwt.Object); + var authRepoMock = new Mock(); + var passwordInfraMock = new Mock(); + var tokenServiceMock = new Mock(); + var handler = new LoginHandler(authRepoMock.Object, passwordInfraMock.Object, tokenServiceMock.Object); + // ...set up mocks for a known user/credential... // Act - var result = await service.LoginAsync("testuser", "password123"); + var result = await handler.Handle(new LoginQuery("testuser", "password123"), CancellationToken.None); // Assert result.Should().NotBeNull(); - result.Token.Should().NotBeNullOrEmpty(); + result.AccessToken.Should().NotBeNullOrEmpty(); } } ``` @@ -288,9 +304,9 @@ configuration: ```bash # CI/CD command -docker compose -f docker-compose.test.yaml build -docker compose -f docker-compose.test.yaml up --abort-on-container-exit -docker compose -f docker-compose.test.yaml down -v +docker compose --env-file web/.env.test -f web/docker-compose.test.yaml build +docker compose --env-file web/.env.test -f web/docker-compose.test.yaml up --abort-on-container-exit +docker compose --env-file web/.env.test -f web/docker-compose.test.yaml down -v ``` Exit codes: @@ -302,7 +318,7 @@ Frontend UI checks should also be included in CI for the active website workspace: ```bash -cd src/Website +cd web/frontend npm ci npm run test:storybook npm run test:storybook:playwright @@ -315,7 +331,7 @@ npm run test:storybook:playwright Ensure SQL Server is running and environment variables are set: ```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 @@ -328,13 +344,13 @@ If port 1433 is in use, stop other SQL Server instances or modify the port in Clean up test database: ```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 ```bash -docker compose -f docker-compose.test.yaml logs +docker compose --env-file web/.env.test -f web/docker-compose.test.yaml logs ``` ## Best Practices diff --git a/docs/website/token-validation.md b/docs/website/token-validation.md index 384009f..d8388cc 100644 --- a/docs/website/token-validation.md +++ b/docs/website/token-validation.md @@ -2,8 +2,7 @@ ## Overview -The Core project implements comprehensive JWT token validation across three -token types: +The Core project validates JWTs across three token types: - **Access Tokens**: Short-lived (1 hour) tokens for API authentication - **Refresh Tokens**: Long-lived (21 days) tokens for obtaining new access @@ -15,7 +14,7 @@ token types: ### Infrastructure Layer -#### [ITokenInfrastructure](Infrastructure.Jwt/ITokenInfrastructure.cs) +#### [ITokenInfrastructure](../../web/backend/Infrastructure/Infrastructure.Jwt/ITokenInfrastructure.cs) Low-level JWT operations. @@ -25,77 +24,92 @@ Low-level JWT operations. - `ValidateJwtAsync()` - Validates token signature, expiration, and format **Implementation:** -[JwtInfrastructure.cs](Infrastructure.Jwt/JwtInfrastructure.cs) +[JwtInfrastructure.cs](../../web/backend/Infrastructure/Infrastructure.Jwt/JwtInfrastructure.cs) - Uses Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler - Algorithm: HS256 (HMAC-SHA256) - 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:** - -- `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:** +**Generation methods:** - `GenerateAccessToken(UserAccount)` - Creates 1-hour access token - `GenerateRefreshToken(UserAccount)` - Creates 21-day refresh token - `GenerateConfirmationToken(UserAccount)` - Creates 30-minute confirmation 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 -#### [ConfirmationService](Service.Auth/IConfirmationService.cs) +#### [ConfirmUserHandler](../../web/backend/Features/Features.Auth/Commands/ConfirmUser/ConfirmUserHandler.cs) **Flow:** -1. Receives confirmation token from user -2. Calls `TokenValidationService.ValidateConfirmationTokenAsync()` +1. Receives confirmation token from user via `ConfirmUserCommand` +2. Calls `ITokenService.ValidateConfirmationTokenAsync()` 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 -#### [RefreshTokenService](Service.Auth/RefreshTokenService.cs) +#### [ResendConfirmationEmailHandler](../../web/backend/Features/Features.Auth/Commands/ResendConfirmationEmail/ResendConfirmationEmailHandler.cs) **Flow:** -1. Receives refresh token from user -2. Calls `TokenValidationService.ValidateRefreshTokenAsync()` -3. Retrieves user account via `AuthRepository.GetUserByIdAsync()` -4. Issues new access and refresh tokens via `TokenService` -5. Returns new token pair +1. Receives a user ID via `ResendConfirmationEmailCommand` +2. Looks up the user and checks they aren't already verified +3. Generates a fresh confirmation token via `ITokenService` +4. Sends `SendResendConfirmationEmailCommand` over MediatR, handled by the + `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:** - `POST /api/auth/register` - Register new user - `POST /api/auth/login` - Authenticate user - `POST /api/auth/confirm?token=...` - Confirm email +- `POST /api/auth/confirm/resend?userId=...` - Resend the confirmation email - `POST /api/auth/refresh` - Refresh access token ## Validation Security @@ -158,32 +172,44 @@ Validation failures return HTTP 401 Unauthorized: 1. **Generation**: During user registration (30-minute validity) 2. **Delivery**: Emailed to user in confirmation link 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 6. **Expiration**: Token becomes invalid after 30 minutes ## Testing +All of the following live in `Features.Auth.Tests`. + ### Unit Tests -**TokenValidationService.test.cs** +**Services/TokenServiceValidationTests.cs** - Happy path: Valid token extraction - Error cases: Invalid, expired, malformed tokens - Missing/invalid claims scenarios -**RefreshTokenService.test.cs** +**Services/TokenServiceRefreshTests.cs** - Successful refresh with valid token - Invalid/expired refresh token rejection - 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 - Token validation failures - 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) **TokenRefresh.feature**