mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-15 01:54:43 +00:00
updates
This commit is contained in:
@@ -1,4 +1,13 @@
|
|||||||
# Architecture
|
---
|
||||||
|
title: System architecture — backend, frontend, database, and deployment
|
||||||
|
last-updated: 2026-08-31
|
||||||
|
tags:
|
||||||
|
- architecture
|
||||||
|
- backend
|
||||||
|
- frontend
|
||||||
|
- vertical-slice
|
||||||
|
- deployment
|
||||||
|
---
|
||||||
|
|
||||||
This document describes the architecture of The Biergarten App.
|
This document describes the architecture of The Biergarten App.
|
||||||
|
|
||||||
@@ -54,38 +63,60 @@ repository end to end. `Features.Emails`, `Features.Locations`, and
|
|||||||
`Features.PhotoUpload` have no `Controllers/` folder; all three are invoked
|
`Features.PhotoUpload` have no `Controllers/` folder; all three are invoked
|
||||||
internally by other slices via MediatR/DI, never over HTTP:
|
internally by other slices via MediatR/DI, never over HTTP:
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
┌───────────────────────────────────────────────────────────────────────┐
|
flowchart TB
|
||||||
│ API.Core (thin host) │
|
subgraph HOST["API.Core (thin host)"]
|
||||||
│ - Program.cs wiring: MediatR + AddApplicationPart per slice │
|
direction LR
|
||||||
│ - Swagger/OpenAPI, JWT auth middleware, global exception filter │
|
H1["Program.cs wiring:<br/>MediatR + AddApplicationPart per slice"]
|
||||||
└───────────────────────────────────────────────────────────────────────┘
|
H2["Swagger / OpenAPI"]
|
||||||
↓ discovers controllers via AddApplicationPart
|
H3["JWT auth middleware"]
|
||||||
┌────────────────┬───────────────┬──────────────────┬──────────────────┬──────────────────┐
|
H4["GlobalExceptionFilter"]
|
||||||
│ Features.Users │Features. │ Features.Emails │ Features. │ Features. │
|
end
|
||||||
│ │Breweries │ (no controller, │ Locations │ PhotoUpload │
|
|
||||||
│ Controllers │ Controller │ internal only) │ (no controller, │ (no controller, │
|
subgraph SLICES["Feature Slices"]
|
||||||
│ Commands/ │ Commands/ │ Commands/ │ internal only) │ internal only) │
|
direction LR
|
||||||
│ Queries + │ Queries + │ Handlers │ Repository │ Commands/ │
|
S1["Features.Users<br/>Controllers, Commands/Queries,<br/>Handlers, Repository"]
|
||||||
│ Handlers │ Handlers │ │ │ Handlers │
|
S2["Features.Breweries<br/>Controller, Commands/Queries,<br/>Handlers, Repository"]
|
||||||
│ Repository │ Repository │ EmailDispatcher │ │ Repository │
|
S3["Features.Emails<br/>(internal only)<br/>Commands, Handlers,<br/>EmailDispatcher"]
|
||||||
└────────────────┴───────────────┴──────────────────┴──────────────────┴──────────────────┘
|
S4["Features.Locations<br/>(internal only)<br/>Repository"]
|
||||||
↓ each slice depends only on shared/domain/infra, never on another slice
|
S5["Features.PhotoUpload<br/>(internal only)<br/>Commands, Handlers,<br/>Repository"]
|
||||||
┌─────────────────────────┬─────────────────────────┬────────────────────┐
|
end
|
||||||
│ Shared.Contracts │ Shared.Application │ Domain.Entities / │
|
|
||||||
│ (ResponseBody envelope) │ (ValidationBehavior, │ Domain.Exceptions │
|
subgraph SHARED["Shared Projects"]
|
||||||
│ │ cross-slice email cmds) │ │
|
direction LR
|
||||||
└─────────────────────────┴─────────────────────────┴────────────────────┘
|
SH1["Shared.Contracts<br/>ResponseBody envelope"]
|
||||||
↓
|
SH2["Shared.Application<br/>ValidationBehavior,<br/>cross-slice email commands"]
|
||||||
┌─────────────────────────────────────────────────────────────────────────┐
|
end
|
||||||
│ Database.Connection, Infrastructure.Jwt, Infrastructure.PasswordHashing,│
|
|
||||||
│ Infrastructure.Email, Infrastructure.Email.Templates │
|
subgraph DOMAIN["Domain Layer"]
|
||||||
└─────────────────────────────────────────────────────────────────────────┘
|
direction LR
|
||||||
↓
|
D1["Domain.Entities"]
|
||||||
┌─────────────────────────────────────┐
|
D2["Domain.Exceptions"]
|
||||||
│ Database (SQL Server) │
|
end
|
||||||
│ - Tables & constraints │
|
|
||||||
└─────────────────────────────────────┘
|
subgraph INFRA["Infrastructure Layer"]
|
||||||
|
direction LR
|
||||||
|
I1["Database.Connection"]
|
||||||
|
I2["Infrastructure.Jwt"]
|
||||||
|
I3["Infrastructure.PasswordHashing"]
|
||||||
|
I4["Infrastructure.Email /<br/>Email.Templates"]
|
||||||
|
I5["Infrastructure.FileUpload"]
|
||||||
|
end
|
||||||
|
|
||||||
|
DB[("Database (SQL Server)")]
|
||||||
|
|
||||||
|
HOST -- "discovers controllers via<br/>AddApplicationPart" --> SLICES
|
||||||
|
S1 -- "SendRegistrationEmailCommand<br/>(via Shared.Application contract)" --> S3
|
||||||
|
|
||||||
|
SLICES --> SHARED
|
||||||
|
SLICES --> DOMAIN
|
||||||
|
SLICES --> INFRA
|
||||||
|
|
||||||
|
INFRA --> DB
|
||||||
|
|
||||||
|
style S3 stroke-dasharray: 4 3
|
||||||
|
style S4 stroke-dasharray: 4 3
|
||||||
|
style S5 stroke-dasharray: 4 3
|
||||||
```
|
```
|
||||||
|
|
||||||
Slices never reference each other's project. The one cross-slice interaction
|
Slices never reference each other's project. The one cross-slice interaction
|
||||||
@@ -147,10 +178,10 @@ no `Controllers/` folder and no MediatR handlers; it exposes
|
|||||||
(for example, `Features.Breweries`) do not yet depend on it; they run their own
|
(for example, `Features.Breweries`) do not yet depend on it; they run their own
|
||||||
`CityID` existence checks inline. `Features.PhotoUpload` also has no
|
`CityID` existence checks inline. `Features.PhotoUpload` also has no
|
||||||
`Controllers/` folder; its `UploadPhotoCommand` is sent by other features'
|
`Controllers/` folder; its `UploadPhotoCommand` is sent by other features'
|
||||||
handlers (for example, a future brewery photo upload command) rather than
|
handlers (for example, a future brewery photo upload command) rather than bound
|
||||||
bound to an HTTP route directly. The one exception today is the `/upload-test`
|
to an HTTP route directly. The one exception today is the `/upload-test` minimal
|
||||||
minimal API endpoint wired directly in `API.Core/Program.cs`, a temporary route
|
API endpoint wired directly in `API.Core/Program.cs`, a temporary route for
|
||||||
for exercising the upload path end to end.
|
exercising the upload path end to end.
|
||||||
|
|
||||||
**Dependencies**:
|
**Dependencies**:
|
||||||
|
|
||||||
@@ -196,17 +227,17 @@ whichever slices need them
|
|||||||
**Components**:
|
**Components**:
|
||||||
|
|
||||||
- **Database.Connection**: generic ADO.NET connection/command plumbing
|
- **Database.Connection**: generic ADO.NET connection/command plumbing
|
||||||
(`DefaultSqlConnectionFactory`, the abstract `DapperRepository` base class), not
|
(`DefaultSqlConnectionFactory`, the abstract `DapperRepository` base class),
|
||||||
domain-specific; each slice's own `Repository/` folder builds on this
|
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 (SMTP/MailKit)
|
- **Infrastructure.Email**: Email sending capabilities (SMTP/MailKit)
|
||||||
- **Infrastructure.Email.Templates**: Email template rendering (Razor
|
- **Infrastructure.Email.Templates**: Email template rendering (Razor
|
||||||
components)
|
components)
|
||||||
- **Infrastructure.FileUpload**: `IFileStorageProvider` abstraction over
|
- **Infrastructure.FileUpload**: `IFileStorageProvider` abstraction over
|
||||||
S3-compatible object storage, implemented by `S3FileStorageProvider`
|
S3-compatible object storage, implemented by `S3FileStorageProvider` (targets
|
||||||
(targets the SeaweedFS container in dev; any S3-compatible endpoint in
|
the SeaweedFS container in dev; any S3-compatible endpoint in other
|
||||||
other environments) using the AWS SDK's `AmazonS3Client` with
|
environments) using the AWS SDK's `AmazonS3Client` with
|
||||||
`ForcePathStyle = true`
|
`ForcePathStyle = true`
|
||||||
|
|
||||||
**Dependencies**:
|
**Dependencies**:
|
||||||
@@ -234,8 +265,8 @@ whichever slices need them
|
|||||||
- `StateProvince` - A state/province referenced by a city
|
- `StateProvince` - A state/province referenced by a city
|
||||||
- `Country` - A country referenced by a state/province
|
- `Country` - A country referenced by a state/province
|
||||||
- `BeerStyle` - A beer style/category (schema table exists; no repository yet)
|
- `BeerStyle` - A beer style/category (schema table exists; no repository yet)
|
||||||
- `BeerPost` - A beer listing tied to a brewery and style (schema table
|
- `BeerPost` - A beer listing tied to a brewery and style (schema table exists;
|
||||||
exists; no repository yet)
|
no repository yet)
|
||||||
- `Photo` - A photo uploaded by a user account, written via
|
- `Photo` - A photo uploaded by a user account, written via
|
||||||
`Features.PhotoUpload`'s `IPhotoUploadRepository`
|
`Features.PhotoUpload`'s `IPhotoUploadRepository`
|
||||||
|
|
||||||
@@ -297,9 +328,9 @@ only:
|
|||||||
factory every slice's repository builds on
|
factory every slice's repository builds on
|
||||||
- `Database.Connection/DapperRepository.cs`: shared abstract base class every
|
- `Database.Connection/DapperRepository.cs`: shared abstract base class every
|
||||||
repository extends, providing `CreateConnection()` and a
|
repository extends, providing `CreateConnection()` and a
|
||||||
`RollbackQuietlyAsync()` helper that rolls back a transaction while
|
`RollbackQuietlyAsync()` helper that rolls back a transaction while swallowing
|
||||||
swallowing any exception the rollback itself raises, so the exception that
|
any exception the rollback itself raises, so the exception that triggered the
|
||||||
triggered the rollback is what propagates
|
rollback is what propagates
|
||||||
|
|
||||||
**Benefits**:
|
**Benefits**:
|
||||||
|
|
||||||
@@ -332,24 +363,23 @@ method that registers its repository and slice-internal services
|
|||||||
|
|
||||||
#### Direct SQL via Dapper/ADO.NET
|
#### Direct SQL via Dapper/ADO.NET
|
||||||
|
|
||||||
**Purpose**: Keep data access explicit and colocated with the slice that owns
|
**Purpose**: Keep data access explicit and colocated with the slice that owns it
|
||||||
it
|
|
||||||
|
|
||||||
**Strategy**:
|
**Strategy**:
|
||||||
|
|
||||||
- No ORM; each repository issues inline SQL through Dapper, which handles
|
- No ORM; each repository issues inline SQL through Dapper, which handles
|
||||||
parameter binding. The one
|
parameter binding. The one wrinkle is the `GEOGRAPHY` column on
|
||||||
wrinkle is the `GEOGRAPHY` column on `BreweryPostLocation`, which Dapper
|
`BreweryPostLocation`, which Dapper can't deserialize as a UDT: reads select
|
||||||
can't deserialize as a UDT: reads select `CONVERT(varbinary(max),
|
`CONVERT(varbinary(max), Coordinates)` so the value comes back as a plain
|
||||||
Coordinates)` so the value comes back as a plain `byte[]` Dapper can bind.
|
`byte[]` Dapper can bind. Proximity search
|
||||||
Proximity search (`GET /api/brewery/locations/nearby`) filters and orders
|
(`GET /api/brewery/locations/nearby`) filters and orders rows server-side with
|
||||||
rows server-side with `Coordinates.STDistance(@Origin) <= @RangeInMetres`,
|
`Coordinates.STDistance(@Origin) <= @RangeInMetres`, avoiding a client-side
|
||||||
avoiding a client-side distance calculation over every row. No spatial
|
distance calculation over every row. No spatial index backs the query yet;
|
||||||
index backs the query yet; `schema.sql` sketches one, commented out, for
|
`schema.sql` sketches one, commented out, for when the table grows large
|
||||||
when the table grows large enough to need it
|
enough to need it
|
||||||
- Referential checks (for example, "does this `CityId` exist?") are explicit
|
- Referential checks (for example, "does this `CityId` exist?") are explicit
|
||||||
`SELECT 1 ...` existence checks in the repository method, run inside the
|
`SELECT 1 ...` existence checks in the repository method, run inside the same
|
||||||
same transaction as the write
|
transaction as the write
|
||||||
- Optimistic concurrency uses each table's `RowVersion` (`ROWVERSION`) column,
|
- Optimistic concurrency uses each table's `RowVersion` (`ROWVERSION`) column,
|
||||||
checked in the `UPDATE ... WHERE ... AND RowVersion = @RowVersion` clause
|
checked in the `UPDATE ... WHERE ... AND RowVersion = @RowVersion` clause
|
||||||
- Repositories throw `Domain.Exceptions` types (`NotFoundException`,
|
- Repositories throw `Domain.Exceptions` types (`NotFoundException`,
|
||||||
@@ -365,8 +395,7 @@ error-handling approach in more detail.
|
|||||||
|
|
||||||
### Website (`web/frontend`)
|
### Website (`web/frontend`)
|
||||||
|
|
||||||
The website is a React Router 7 application with server-side rendering
|
The website is a React Router 7 application with server-side rendering enabled.
|
||||||
enabled.
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
web/frontend/
|
web/frontend/
|
||||||
@@ -409,8 +438,7 @@ web/frontend/
|
|||||||
|
|
||||||
### Theme system
|
### Theme system
|
||||||
|
|
||||||
The website uses semantic DaisyUI theme tokens backed by four Biergarten
|
The website uses semantic DaisyUI theme tokens backed by four Biergarten themes:
|
||||||
themes:
|
|
||||||
|
|
||||||
- Biergarten Lager
|
- Biergarten Lager
|
||||||
- Biergarten Stout
|
- Biergarten Stout
|
||||||
@@ -550,8 +578,8 @@ pipeline under `tooling/pipeline/` (see [Pipeline README](pipeline/README.md))
|
|||||||
**Seed Data**:
|
**Seed Data**:
|
||||||
|
|
||||||
- Countries, state/provinces, and cities (via `Features.Locations`'
|
- Countries, state/provinces, and cities (via `Features.Locations`'
|
||||||
`ILocationRepository`, which creates `Country`/`StateProvince` rows on
|
`ILocationRepository`, which creates `Country`/`StateProvince` rows on demand
|
||||||
demand while resolving a city)
|
while resolving a city)
|
||||||
- User accounts (via `Features.Users`' repository)
|
- User accounts (via `Features.Users`' repository)
|
||||||
- Brewery posts with locations (via `Features.Breweries`' repository)
|
- Brewery posts with locations (via `Features.Breweries`' repository)
|
||||||
|
|
||||||
@@ -601,16 +629,12 @@ healthcheck:
|
|||||||
|
|
||||||
### Test pyramid
|
### Test pyramid
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
┌──────────────┐
|
flowchart TB
|
||||||
│ Integration │ ← API.Specs (Reqnroll)
|
IT["Integration Tests<br/>API.Specs (Reqnroll)"]
|
||||||
│ Tests │
|
UT["Unit Tests (per slice, handler tests)<br/>Features.Users.Tests, Features.Breweries.Tests,<br/>Features.Emails.Tests, Features.PhotoUpload.Tests<br/>(commands/queries/handlers, with repository/service<br/>dependencies mocked via Moq)"]
|
||||||
├──────────────┤
|
|
||||||
│ Unit Tests │ ← Features.Users.Tests, Features.Breweries.Tests,
|
IT --- UT
|
||||||
│ (per slice, │ Features.Emails.Tests, Features.PhotoUpload.Tests
|
|
||||||
│ handler │ (commands/queries/handlers, with repository/service
|
|
||||||
│ tests) │ dependencies mocked via Moq)
|
|
||||||
└──────────────┘
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Strategy**:
|
**Strategy**:
|
||||||
|
|||||||
15
docs/index.md
Normal file
15
docs/index.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: The Biergarten App
|
||||||
|
last-updated: 2026-08-31
|
||||||
|
tags:
|
||||||
|
- overview
|
||||||
|
- index
|
||||||
|
---
|
||||||
|
|
||||||
|
Welcome to the documentation for The Biergarten App.
|
||||||
|
|
||||||
|
## Documentation Sections
|
||||||
|
|
||||||
|
- **[Architecture](architecture.md)** — System design and components
|
||||||
|
- **[Website](website/getting-started.md)** — Backend and frontend documentation
|
||||||
|
- **[Pipeline](pipeline/README.md)** — Data pipeline and ETL processes
|
||||||
@@ -1,4 +1,13 @@
|
|||||||
# Ethics, bias, and known issues
|
---
|
||||||
|
title: Pipeline ethics, bias, and known issues
|
||||||
|
last-updated: 2026-08-31
|
||||||
|
tags:
|
||||||
|
- pipeline
|
||||||
|
- ethics
|
||||||
|
- bias
|
||||||
|
- known-issues
|
||||||
|
- ai-generated
|
||||||
|
---
|
||||||
|
|
||||||
This document covers the ethical context of the Biergarten Pipeline's output,
|
This document covers the ethical context of the Biergarten Pipeline's output,
|
||||||
the model's biases, and known issues including hallucinated brewing science and
|
the model's biases, and known issues including hallucinated brewing science and
|
||||||
@@ -290,11 +299,11 @@ Each brewery and user address carries a longitude/latitude pair, sampled
|
|||||||
uniformly at random within a 5 km disc centred on the city's curated
|
uniformly at random within a 5 km disc centred on the city's curated
|
||||||
`latitude`/`longitude` from `locations.json`.
|
`latitude`/`longitude` from `locations.json`.
|
||||||
|
|
||||||
The resulting point is **plausible by construction**, always falling within
|
The resulting point is **plausible by construction**, always falling within the
|
||||||
the city's vicinity, but it is not looked up from any real address registry.
|
city's vicinity, but it is not looked up from any real address registry. It does
|
||||||
It does not correspond to a real address, business, or resident, and should
|
not correspond to a real address, business, or resident, and should not be
|
||||||
not be treated as accurate geocoding data. This is consistent with the rest of
|
treated as accurate geocoding data. This is consistent with the rest of the
|
||||||
the dataset: fixture data for a proof-of-concept, not a source of truth.
|
dataset: fixture data for a proof-of-concept, not a source of truth.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
# Biergarten Pipeline
|
---
|
||||||
|
title: Biergarten Pipeline — C++ CLI for generating seed data
|
||||||
|
last-updated: 2026-08-31
|
||||||
|
tags:
|
||||||
|
- pipeline
|
||||||
|
- cpp
|
||||||
|
- llm
|
||||||
|
- seed-data
|
||||||
|
- docker
|
||||||
|
---
|
||||||
|
|
||||||
A C++20 command-line pipeline that samples city records from local JSON,
|
A C++20 command-line pipeline that samples city records from local JSON,
|
||||||
enriches each with Wikipedia context, and generates bilingual brewery names and
|
enriches each with Wikipedia context, and generates bilingual brewery names and
|
||||||
@@ -436,8 +445,8 @@ Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
|
|||||||
Paths below are relative to `tooling/pipeline/`.
|
Paths below are relative to `tooling/pipeline/`.
|
||||||
|
|
||||||
- `src/main.cc`: argument parsing and DI composition root.
|
- `src/main.cc`: argument parsing and DI composition root.
|
||||||
- `src/biergarten_pipeline_orchestrator/`: orchestration, sampling, logging,
|
- `src/biergarten_pipeline_orchestrator/`: orchestration, sampling, logging, and
|
||||||
and export.
|
export.
|
||||||
- `src/services/curated_data/`: `CuratedJsonDataService`, the file-backed
|
- `src/services/curated_data/`: `CuratedJsonDataService`, the file-backed
|
||||||
`ICuratedDataService`, and `MockCuratedDataService`, the in-memory
|
`ICuratedDataService`, and `MockCuratedDataService`, the in-memory
|
||||||
`ICuratedDataService` used in `--mocked` runs.
|
`ICuratedDataService` used in `--mocked` runs.
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
# Pipeline Roadmap — Reaching the Planned Architecture
|
---
|
||||||
|
title: Pipeline roadmap — reaching the planned architecture
|
||||||
|
last-updated: 2026-08-31
|
||||||
|
tags:
|
||||||
|
- pipeline
|
||||||
|
- roadmap
|
||||||
|
- cpp
|
||||||
|
- concurrency
|
||||||
|
---
|
||||||
|
|
||||||
This is the engineering breakdown for closing the gap between the **current**
|
This is the engineering breakdown for closing the gap between the **current**
|
||||||
pipeline and the **planned** architecture documented in
|
pipeline and the **planned** architecture documented in
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
# Database
|
---
|
||||||
|
title: SQL Server database schema, migrations, and error handling
|
||||||
|
last-updated: 2026-08-31
|
||||||
|
tags:
|
||||||
|
- sql
|
||||||
|
- repository
|
||||||
|
- database
|
||||||
|
- sql-server
|
||||||
|
- schema
|
||||||
|
---
|
||||||
|
|
||||||
SQL Server 2022 schema, migration approach, and the app's SQL error-handling
|
SQL Server 2022 schema, migration approach, and the app's SQL error-handling
|
||||||
convention. For the vertical-slice code that talks to this schema, see
|
convention. For the vertical-slice code that talks to this schema, see
|
||||||
@@ -7,15 +16,36 @@ convention. For the vertical-slice code that talks to this schema, see
|
|||||||
## Schema
|
## Schema
|
||||||
|
|
||||||
The full schema lives under
|
The full schema lives under
|
||||||
`web/backend/Database/Database.Migrations/scripts/01-schema/`, one numbered
|
`web/backend/Database/Database.Migrations/scripts/01-schema/` with one numbered
|
||||||
script per table (`01-UserAccount.sql`, `02-Photo.sql`, ...), run in order by
|
script per table.
|
||||||
`Database.Migrations` (DbUp) on startup. Each table is queried directly by the
|
|
||||||
owning feature slice's repository (see
|
```
|
||||||
|
└── 01-schema
|
||||||
|
├── 01-UserAccount.sql
|
||||||
|
├── 02-Photo.sql
|
||||||
|
├── 03-UserVerification.sql
|
||||||
|
├── 04-UserCredential.sql
|
||||||
|
├── 05-UserFollow.sql
|
||||||
|
├── 06-Country.sql
|
||||||
|
├── 07-StateProvince.sql
|
||||||
|
├── 08-City.sql
|
||||||
|
├── 09-BreweryPost.sql
|
||||||
|
├── 10-BreweryPostLocation.sql
|
||||||
|
├── 11-BreweryPostPhoto.sql
|
||||||
|
├── 12-BeerStyle.sql
|
||||||
|
├── 13-BeerPost.sql
|
||||||
|
├── 14-BeerPostPhoto.sql
|
||||||
|
├── 15-BeerPostComment.sql
|
||||||
|
├── 16-UserProfile.sql
|
||||||
|
└── 17-UserAvatar.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
Each script is run in order by `Database.Migrations` (DbUp) on startup. Each
|
||||||
|
table is queried directly by the owning feature slice's repository (see
|
||||||
[Direct SQL via Dapper/ADO.NET](../architecture.md#direct-sql-via-dapperadonet)).
|
[Direct SQL via Dapper/ADO.NET](../architecture.md#direct-sql-via-dapperadonet)).
|
||||||
|
|
||||||
A generated DBML copy of the schema (for import into a diagramming tool such
|
A generated DBML copy of the schema (for import into a diagramming tool such as
|
||||||
as dbdiagram.io) lives at
|
dbdiagram.io) lives at `web/backend/Database/Database.Migrations/schema.dbml`.
|
||||||
`web/backend/Database/Database.Migrations/schema.dbml`.
|
|
||||||
|
|
||||||
All tables use a `UNIQUEIDENTIFIER` primary key defaulting to `NEWID()`, and
|
All tables use a `UNIQUEIDENTIFIER` primary key defaulting to `NEWID()`, and
|
||||||
most carry a `RowVersion ROWVERSION` column used for optimistic concurrency on
|
most carry a `RowVersion ROWVERSION` column used for optimistic concurrency on
|
||||||
@@ -23,44 +53,52 @@ updates (`UPDATE ... WHERE ... AND RowVersion = @RowVersion`).
|
|||||||
|
|
||||||
### Tables backed by the API today
|
### Tables backed by the API today
|
||||||
|
|
||||||
| Table | Backed by | Notes |
|
| Table | Backed by | Notes |
|
||||||
| --------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| --------------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `UserAccount` | `Domain.Entities.UserAccount`, `Features.Users` | `Username` and `Email` are unique |
|
| `UserAccount` | `Domain.Entities.UserAccount`, `Features.Users` | `Username` and `Email` are unique |
|
||||||
| `UserCredential` | `Domain.Entities.UserCredential`, `Features.Users` | Argon2id password hash; `IsRevoked`/`RevokedAt` track credential rotation, not JWTs |
|
| `UserCredential` | `Domain.Entities.UserCredential`, `Features.Users` | Argon2id password hash; `IsRevoked`/`RevokedAt` track credential rotation, not JWTs |
|
||||||
| `UserVerification` | `Domain.Entities.UserVerification`, `Features.Users` | Written when a user confirms their email |
|
| `UserVerification` | `Domain.Entities.UserVerification`, `Features.Users` | Written when a user confirms their email |
|
||||||
| `UserProfile` | `Domain.Entities.UserProfile`, `Features.Users` | 1:1 with `UserAccount`; holds the user's biography; written via `IUserProfileRepository` |
|
| `UserProfile` | `Domain.Entities.UserProfile`, `Features.Users` | 1:1 with `UserAccount`; holds the user's biography; written via `IUserProfileRepository` |
|
||||||
| `UserAvatar` | `Domain.Entities.UserAvatar`, `Features.Users` | Links a `UserProfile` to a `Photo` as its avatar; written via `IUserProfileRepository.SaveAvatarAsync` |
|
| `UserAvatar` | `Domain.Entities.UserAvatar`, `Features.Users` | Links a `UserProfile` to a `Photo` as its avatar; written via `IUserProfileRepository.SaveAvatarAsync` |
|
||||||
| `BreweryPost` | `Domain.Entities.BreweryPost`, `Features.Breweries` | A user-submitted brewery listing |
|
| `BreweryPost` | `Domain.Entities.BreweryPost`, `Features.Breweries` | A user-submitted brewery listing |
|
||||||
| `BreweryPostLocation` | `Domain.Entities.BreweryPostLocation`, `Features.Breweries` | 1:1 with `BreweryPost`; `Coordinates` is a `GEOGRAPHY` column, read via a plain Dapper query that selects `CONVERT(varbinary(max), Coordinates)`, since Dapper can't deserialize SQL Server UDTs directly |
|
| `BreweryPostLocation` | `Domain.Entities.BreweryPostLocation`, `Features.Breweries` | 1:1 with `BreweryPost`; `Coordinates` is a `GEOGRAPHY` column, read via a plain Dapper query that selects `CONVERT(varbinary(max), Coordinates)`, since Dapper can't deserialize SQL Server UDTs directly |
|
||||||
| `City` | `Domain.Entities.City`, `Features.Locations` | Referenced by `BreweryPostLocation.CityID`; written via `ILocationRepository`, read via `ILocationRepository` and `IBreweryRepository`'s location joins, currently written only by `Database.Seed` |
|
| `City` | `Domain.Entities.City`, `Features.Locations` | Referenced by `BreweryPostLocation.CityID`; written via `ILocationRepository`, read via `ILocationRepository` and `IBreweryRepository`'s location joins, currently written only by `Database.Seed` |
|
||||||
| `StateProvince` | `Domain.Entities.StateProvince`, `Features.Locations` | Referenced by `City.StateProvinceID`; created on demand by `ILocationRepository.GetOrCreateCityIdAsync`, read via `IBreweryRepository`'s location joins |
|
| `StateProvince` | `Domain.Entities.StateProvince`, `Features.Locations` | Referenced by `City.StateProvinceID`; created on demand by `ILocationRepository.GetOrCreateCityIdAsync`, read via `IBreweryRepository`'s location joins |
|
||||||
| `Country` | `Domain.Entities.Country`, `Features.Locations` | Referenced by `StateProvince.CountryID`; created on demand by `ILocationRepository.GetOrCreateCityIdAsync`, read via `IBreweryRepository`'s location joins |
|
| `Country` | `Domain.Entities.Country`, `Features.Locations` | Referenced by `StateProvince.CountryID`; created on demand by `ILocationRepository.GetOrCreateCityIdAsync`, read via `IBreweryRepository`'s location joins |
|
||||||
| `Photo` | `Domain.Entities.Photo`, `Features.PhotoUpload` | Written via `IPhotoUploadRepository.CreateAsync`, sent as an `UploadPhotoCommand` from other features' handlers; not yet seeded by `Database.Seed` |
|
| `Photo` | `Domain.Entities.Photo`, `Features.PhotoUpload` | Written via `IPhotoUploadRepository.CreateAsync`, sent as an `UploadPhotoCommand` from other features' handlers; not yet seeded by `Database.Seed` |
|
||||||
|
|
||||||
### Tables defined in the schema but not yet wired to a feature slice
|
### Tables defined in the schema but not yet wired to a feature slice
|
||||||
|
|
||||||
These exist so the schema can support planned functionality, but have no
|
These exist so the schema can support planned functionality, but have no
|
||||||
repository or seed data yet:
|
repository or seed data yet:
|
||||||
|
|
||||||
| Table | Purpose (from schema) |
|
| Table | Purpose (from schema) |
|
||||||
| ------------------ | -------------------------------------------------------------------------------------------------------- |
|
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `UserFollow` | One user following another (`CannotFollowOwnAccount` check constraint) |
|
| `UserFollow` | One user following another (`CannotFollowOwnAccount` check constraint) |
|
||||||
| `BeerStyle` | A beer style/category; has a `Domain.Entities.BeerStyle` type, but no repository yet |
|
| `BeerStyle` | A beer style/category; has a `Domain.Entities.BeerStyle` type, but no repository yet |
|
||||||
| `BeerPost` | A beer listing, linked to a `BreweryPost` and `BeerStyle`; `ABV`/`IBU` are constrained (`0-67`, `0-120`); has a `Domain.Entities.BeerPost` type, but no repository yet |
|
| `BeerPost` | A beer listing, linked to a `BreweryPost` and `BeerStyle`; `ABV`/`IBU` are constrained (`0-67`, `0-120`); has a `Domain.Entities.BeerPost` type, but no repository yet |
|
||||||
| `BeerPostPhoto` | Links a `BeerPost` to a `Photo` |
|
| `BeerPostPhoto` | Links a `BeerPost` to a `Photo` |
|
||||||
| `BeerPostComment` | A comment + 1-5 `Rating` on a `BeerPost` |
|
| `BeerPostComment` | A comment + 1-5 `Rating` on a `BeerPost` |
|
||||||
| `BreweryPostPhoto` | Links a `BreweryPost` to a `Photo` |
|
| `BreweryPostPhoto` | Links a `BreweryPost` to a `Photo` |
|
||||||
|
|
||||||
## Migrations
|
## Managing Migrations (DbUp)
|
||||||
|
|
||||||
**Tool**: DbUp, run by the `Database.Migrations` project/container.
|
Migrations are handled by the `Database.Migrations` container using DbUp.
|
||||||
|
Scripts are placed in the `scripts/` directory, applied in order, and tracked so
|
||||||
|
that each one runs only once.
|
||||||
|
|
||||||
Scripts live under `scripts/`, applied in order and tracked so a script never
|
**Note:** The schema is currently considered volatile. At this stage of
|
||||||
re-runs once it has succeeded. Today that's `01-schema/`, with one script per
|
development, fresh database instances are provisioned for both development and
|
||||||
table, numbered so a table's foreign keys always point at an earlier-numbered
|
testing, so no production data exists yet.
|
||||||
script. When the schema needs to change, add a new numbered script rather than
|
|
||||||
editing an existing one in place: DbUp's tracking is per-script, so an edited
|
### How to Make Schema Changes
|
||||||
already-applied script never re-runs against existing databases.
|
|
||||||
|
- **Now (no production data):** Update the schema by editing the original DDL
|
||||||
|
statements directly.
|
||||||
|
- **Later (with production data):** Add a new, numbered script for any schema
|
||||||
|
changes. Never edit an existing script in place — DbUp tracks completion by
|
||||||
|
script name, so modifying an already-applied script means the changes will be
|
||||||
|
ignored on existing databases.
|
||||||
|
|
||||||
## Data seeding
|
## Data seeding
|
||||||
|
|
||||||
@@ -81,7 +119,7 @@ repository (`Features.PhotoUpload`) but no seed data.
|
|||||||
409, 400, ...); an unmapped `SqlException` (for example, a genuine
|
409, 400, ...); an unmapped `SqlException` (for example, a genuine
|
||||||
connectivity or constraint-violation failure) maps to 503.
|
connectivity or constraint-violation failure) maps to 503.
|
||||||
|
|
||||||
- One exception: `Features.Users`' `DapperUserStore` does catch
|
- One exception: `Features.Users`' `DapperUserStore` does catch `SqlException`
|
||||||
`SqlException` directly, to detect a duplicate-key race (SQL Server error
|
directly, to detect a duplicate-key race (SQL Server error 2601 or 2627)
|
||||||
2601 or 2627) when two requests try to verify the same account
|
when two requests try to verify the same account concurrently; see
|
||||||
concurrently; see `DapperUserStore.IsDuplicateKeyViolation`.
|
`DapperUserStore.IsDuplicateKeyViolation`.
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
# Docker guide
|
---
|
||||||
|
title: Docker deployment, configuration, and troubleshooting
|
||||||
|
last-updated: 2026-08-31
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
- docker-compose
|
||||||
|
- deployment
|
||||||
|
---
|
||||||
|
|
||||||
This document covers Docker deployment, configuration, and troubleshooting for
|
This document covers Docker deployment, configuration, and troubleshooting for
|
||||||
The Biergarten App.
|
The Biergarten App.
|
||||||
@@ -114,9 +121,9 @@ docker compose --env-file web/.env.test -f web/docker-compose.test.yaml down -v
|
|||||||
|
|
||||||
`wait` blocks until `api.specs`, `unit.tests`, and `frontend.tests` have all
|
`wait` blocks until `api.specs`, `unit.tests`, and `frontend.tests` have all
|
||||||
stopped, regardless of which one finishes first; `up --abort-on-container-exit`
|
stopped, regardless of which one finishes first; `up --abort-on-container-exit`
|
||||||
isn't used here because the one-shot `database.migrations`/`database.seed`
|
isn't used here because the one-shot `database.migrations`/`database.seed` jobs
|
||||||
jobs and `frontend.tests` (no database dependency) can exit before the other
|
and `frontend.tests` (no database dependency) can exit before the other test
|
||||||
test containers, which would end the run prematurely. See
|
containers, which would end the run prematurely. See
|
||||||
[Testing](testing.md#running-tests-with-docker-recommended) for details.
|
[Testing](testing.md#running-tests-with-docker-recommended) for details.
|
||||||
|
|
||||||
### 3. Production (`docker-compose.prod.yaml`)
|
### 3. Production (`docker-compose.prod.yaml`)
|
||||||
@@ -131,8 +138,8 @@ test containers, which would end the run prematurely. See
|
|||||||
- Health checks enabled
|
- Health checks enabled
|
||||||
- Restart policies (unless-stopped)
|
- Restart policies (unless-stopped)
|
||||||
- Security hardening
|
- Security hardening
|
||||||
- Only `frontend` publishes a port to the host; `sqlserver` and `api.core`
|
- Only `frontend` publishes a port to the host; `sqlserver` and `api.core` are
|
||||||
are reachable only from other containers on `prodnet`
|
reachable only from other containers on `prodnet`
|
||||||
|
|
||||||
**Services**:
|
**Services**:
|
||||||
|
|
||||||
@@ -206,8 +213,8 @@ graph TD
|
|||||||
E[seaweedfs: container started] --> D
|
E[seaweedfs: container started] --> D
|
||||||
```
|
```
|
||||||
|
|
||||||
In development, `api.core` also depends on `seaweedfs` (`condition:
|
In development, `api.core` also depends on `seaweedfs`
|
||||||
service_started`, not a health check) alongside `database.seed`.
|
(`condition: service_started`, not a health check) alongside `database.seed`.
|
||||||
|
|
||||||
**Health Check Example** (SQL Server):
|
**Health Check Example** (SQL Server):
|
||||||
|
|
||||||
@@ -241,8 +248,7 @@ api.core:
|
|||||||
|
|
||||||
- `sqlserverdata-dev` - Database files persist between restarts
|
- `sqlserverdata-dev` - Database files persist between restarts
|
||||||
- `nuget-cache-dev` - NuGet package cache (speeds up builds)
|
- `nuget-cache-dev` - NuGet package cache (speeds up builds)
|
||||||
- `seaweedfsdata-dev` - SeaweedFS object storage data persists between
|
- `seaweedfsdata-dev` - SeaweedFS object storage data persists between restarts
|
||||||
restarts
|
|
||||||
|
|
||||||
**Testing**:
|
**Testing**:
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
# Environment variables
|
---
|
||||||
|
title: Environment variables for the backend, frontend, and Docker
|
||||||
|
last-updated: 2026-08-31
|
||||||
|
tags:
|
||||||
|
- environment-variables
|
||||||
|
- configuration
|
||||||
|
- secrets
|
||||||
|
- docker
|
||||||
|
---
|
||||||
|
|
||||||
This document covers the environment variables used by the Biergarten stack.
|
This document covers the environment variables used by the Biergarten stack.
|
||||||
|
|
||||||
@@ -37,9 +45,9 @@ Environment-specific `.env` files loaded via `env_file:` in docker-compose.yaml:
|
|||||||
secret-bearing values (`DB_PASSWORD`, `ACCESS_TOKEN_SECRET`,
|
secret-bearing values (`DB_PASSWORD`, `ACCESS_TOKEN_SECRET`,
|
||||||
`REFRESH_TOKEN_SECRET`, `CONFIRMATION_TOKEN_SECRET`, `SESSION_SECRET`,
|
`REFRESH_TOKEN_SECRET`, `CONFIRMATION_TOKEN_SECRET`, `SESSION_SECRET`,
|
||||||
`SMTP_USERNAME`, `SMTP_PASSWORD`, `SEAWEEDFS_ACCESS_KEY_ID`,
|
`SMTP_USERNAME`, `SMTP_PASSWORD`, `SEAWEEDFS_ACCESS_KEY_ID`,
|
||||||
`SEAWEEDFS_SECRET_ACCESS_KEY`)
|
`SEAWEEDFS_SECRET_ACCESS_KEY`) with freshly randomized ones via `openssl`.
|
||||||
with freshly randomized ones via `openssl`. Non-secret values are left
|
Non-secret values are left untouched. The output file is written with `600`
|
||||||
untouched. The output file is written with `600` permissions.
|
permissions.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd web
|
cd web
|
||||||
@@ -171,12 +179,12 @@ DOTNET_RUNNING_IN_CONTAINER=true # Flag for container execution
|
|||||||
```
|
```
|
||||||
|
|
||||||
`ASPNETCORE_URLS` is hardcoded directly in `docker-compose.dev.yaml` and
|
`ASPNETCORE_URLS` is hardcoded directly in `docker-compose.dev.yaml` and
|
||||||
`docker-compose.prod.yaml` (not sourced from the `.env` file), so it's
|
`docker-compose.prod.yaml` (not sourced from the `.env` file), so it's required
|
||||||
required only for Docker. Outside Docker, `dotnet run` falls back to the
|
only for Docker. Outside Docker, `dotnet run` falls back to the `http` profile
|
||||||
`http` profile in `API.Core/Properties/launchSettings.json`
|
in `API.Core/Properties/launchSettings.json` (`http://localhost:5069`), so
|
||||||
(`http://localhost:5069`), so manual backend setup (see
|
manual backend setup (see
|
||||||
[Getting Started](getting-started.md#manual-backend-setup)) doesn't need to
|
[Getting Started](getting-started.md#manual-backend-setup)) doesn't need to set
|
||||||
set it.
|
it.
|
||||||
|
|
||||||
## Frontend variables (`web/frontend`)
|
## Frontend variables (`web/frontend`)
|
||||||
|
|
||||||
@@ -191,8 +199,8 @@ PORT=3000 # Port the built server listens on
|
|||||||
|
|
||||||
The `frontend` Docker service hardcodes `API_BASE_URL` to `http://api.core:8080`
|
The `frontend` Docker service hardcodes `API_BASE_URL` to `http://api.core:8080`
|
||||||
(the backend's in-network service name) and `PORT` to `3000`; only
|
(the backend's in-network service name) and `PORT` to `3000`; only
|
||||||
`SESSION_SECRET` is sourced from the env file, and `NODE_ENV` is set per
|
`SESSION_SECRET` is sourced from the env file, and `NODE_ENV` is set per compose
|
||||||
compose file (`development` in dev, `production` in prod).
|
file (`development` in dev, `production` in prod).
|
||||||
|
|
||||||
### Frontend variable details
|
### Frontend variable details
|
||||||
|
|
||||||
@@ -357,8 +365,8 @@ Variables are validated at startup:
|
|||||||
- Missing required variables (`ACCESS_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`,
|
- Missing required variables (`ACCESS_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`,
|
||||||
`CONFIRMATION_TOKEN_SECRET`, `SMTP_HOST`, `SMTP_FROM_EMAIL`,
|
`CONFIRMATION_TOKEN_SECRET`, `SMTP_HOST`, `SMTP_FROM_EMAIL`,
|
||||||
`SEAWEEDFS_SERVICE_URL`, `SEAWEEDFS_ACCESS_KEY_ID`,
|
`SEAWEEDFS_SERVICE_URL`, `SEAWEEDFS_ACCESS_KEY_ID`,
|
||||||
`SEAWEEDFS_SECRET_ACCESS_KEY`, `SEAWEEDFS_BUCKET`, DB connection values)
|
`SEAWEEDFS_SECRET_ACCESS_KEY`, `SEAWEEDFS_BUCKET`, DB connection values) cause
|
||||||
cause the application to fail with an `InvalidOperationException`
|
the application to fail with an `InvalidOperationException`
|
||||||
- No minimum length is enforced on the token secrets in code; the "minimum 32
|
- No minimum length is enforced on the token secrets in code; the "minimum 32
|
||||||
characters" guidance above is a recommendation, not an enforced check
|
characters" guidance above is a recommendation, not an enforced check
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
# Getting started
|
---
|
||||||
|
title: Getting started — local setup for backend and frontend
|
||||||
|
last-updated: 2026-08-31
|
||||||
|
tags:
|
||||||
|
- getting-started
|
||||||
|
- setup
|
||||||
|
- onboarding
|
||||||
|
---
|
||||||
|
|
||||||
This guide covers local setup for the Biergarten stack: the .NET backend in
|
This guide covers local setup for the Biergarten stack: the .NET backend in
|
||||||
`web/backend` and the React Router frontend in `web/frontend`.
|
`web/backend` and the React Router frontend in `web/frontend`.
|
||||||
@@ -58,9 +65,8 @@ section for details on `generate-env.sh`.
|
|||||||
docker compose --env-file web/.env.dev -f web/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, the API, Mailpit (local dev
|
This starts SQL Server, migrations, seeding, the API, Mailpit (local dev SMTP),
|
||||||
SMTP), and SeaweedFS (local dev S3-compatible object storage for photo
|
and SeaweedFS (local dev S3-compatible object storage for photo uploads).
|
||||||
uploads).
|
|
||||||
|
|
||||||
Available endpoints:
|
Available endpoints:
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
# Testing
|
---
|
||||||
|
title: Testing strategy — unit, integration, and Storybook suites
|
||||||
|
last-updated: 2026-08-31
|
||||||
|
tags:
|
||||||
|
- testing
|
||||||
|
- ci
|
||||||
|
- xunit
|
||||||
|
- reqnroll
|
||||||
|
- storybook
|
||||||
|
---
|
||||||
|
|
||||||
This document describes the testing strategy and how to run tests for The
|
This document describes the testing strategy and how to run tests for The
|
||||||
Biergarten App.
|
Biergarten App.
|
||||||
@@ -10,10 +19,9 @@ The project uses a multi-layered testing approach across backend and frontend:
|
|||||||
- **API.Specs** - BDD integration tests using Reqnroll (Gherkin), run against a
|
- **API.Specs** - BDD integration tests using Reqnroll (Gherkin), run against a
|
||||||
live, seeded database
|
live, seeded database
|
||||||
- **Features.\*.Tests** - One unit test project per backend feature slice
|
- **Features.\*.Tests** - One unit test project per backend feature slice
|
||||||
(`Features.Users.Tests`, `Features.Breweries.Tests`,
|
(`Features.Users.Tests`, `Features.Breweries.Tests`, `Features.Emails.Tests`,
|
||||||
`Features.Emails.Tests`, `Features.PhotoUpload.Tests`), covering that
|
`Features.PhotoUpload.Tests`), covering that slice's command/query handlers,
|
||||||
slice's command/query handlers, with dependencies mocked via Moq (no real
|
with dependencies mocked via Moq (no real database required)
|
||||||
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
|
||||||
@@ -42,8 +50,8 @@ This command:
|
|||||||
`wait` blocks until `api.specs`, `unit.tests`, and `frontend.tests` have all
|
`wait` blocks until `api.specs`, `unit.tests`, and `frontend.tests` have all
|
||||||
stopped, regardless of which one finishes first. Long-running services
|
stopped, regardless of which one finishes first. Long-running services
|
||||||
(`sqlserver`, `seaweedfs`) and the one-shot `database.migrations` /
|
(`sqlserver`, `seaweedfs`) and the one-shot `database.migrations` /
|
||||||
`database.seed` jobs keep running in the background alongside them and are
|
`database.seed` jobs keep running in the background alongside them and are torn
|
||||||
torn down separately (see below); `wait` doesn't touch them.
|
down separately (see below); `wait` doesn't touch them.
|
||||||
|
|
||||||
### View test results
|
### View test results
|
||||||
|
|
||||||
@@ -175,8 +183,8 @@ The default (`headless: true`) is set in `web/frontend/vite.config.ts`.
|
|||||||
**Features.Breweries.Tests**:
|
**Features.Breweries.Tests**:
|
||||||
|
|
||||||
- Brewery create/update/delete commands and get-by-id/get-all queries
|
- Brewery create/update/delete commands and get-by-id/get-all queries
|
||||||
- Brewery location queries: all locations, and locations within a given range
|
- Brewery location queries: all locations, and locations within a given range of
|
||||||
of a coordinate
|
a coordinate
|
||||||
|
|
||||||
**Features.Emails.Tests**:
|
**Features.Emails.Tests**:
|
||||||
|
|
||||||
@@ -186,9 +194,8 @@ The default (`headless: true`) is set in `web/frontend/vite.config.ts`.
|
|||||||
|
|
||||||
- Upload command handler: storage key construction, content type passthrough,
|
- Upload command handler: storage key construction, content type passthrough,
|
||||||
and photo persistence via the mocked storage provider and repository
|
and photo persistence via the mocked storage provider and repository
|
||||||
- Upload validator: min/max file size bounds, and PNG/JPEG/WebP signature
|
- Upload validator: min/max file size bounds, and PNG/JPEG/WebP signature checks
|
||||||
checks (including rejection of files with a mismatched or missing
|
(including rejection of files with a mismatched or missing signature)
|
||||||
signature)
|
|
||||||
|
|
||||||
**Frontend UI Coverage**:
|
**Frontend UI Coverage**:
|
||||||
|
|
||||||
@@ -295,8 +302,8 @@ Features.Users.Tests/
|
|||||||
|
|
||||||
Each of the other slices (`Features.Breweries.Tests`, `Features.Emails.Tests`,
|
Each of the other slices (`Features.Breweries.Tests`, `Features.Emails.Tests`,
|
||||||
`Features.PhotoUpload.Tests`) follows the same shape: a `Commands/`/`Queries/`
|
`Features.PhotoUpload.Tests`) follows the same shape: a `Commands/`/`Queries/`
|
||||||
folder with one test file per handler (`Features.PhotoUpload.Tests` also adds
|
folder with one test file per handler (`Features.PhotoUpload.Tests` also adds a
|
||||||
a validator test file alongside its handler test).
|
validator test file alongside its handler test).
|
||||||
|
|
||||||
## Writing tests
|
## Writing tests
|
||||||
|
|
||||||
@@ -337,10 +344,10 @@ Scenario: User login with valid credentials
|
|||||||
|
|
||||||
## Continuous integration
|
## Continuous integration
|
||||||
|
|
||||||
The `.github/workflows/tests.yml` GitHub Actions workflow runs on every push
|
The `.github/workflows/tests.yml` GitHub Actions workflow runs on every push and
|
||||||
and pull request to `main`. It runs the same test Docker Compose
|
pull request to `main`. It runs the same test Docker Compose configuration used
|
||||||
configuration used locally, so backend and frontend tests execute in the same
|
locally, so backend and frontend tests execute in the same containerized
|
||||||
containerized environment in CI as they do on a developer machine:
|
environment in CI as they do on a developer machine:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# CI/CD command
|
# CI/CD command
|
||||||
@@ -351,16 +358,15 @@ docker compose --env-file web/.env.test -f web/docker-compose.test.yaml down -v
|
|||||||
```
|
```
|
||||||
|
|
||||||
The workflow inspects the exit code of each test container
|
The workflow inspects the exit code of each test container
|
||||||
(`test-env-api-specs`, `test-env-unit-tests`, `test-env-frontend-tests`)
|
(`test-env-api-specs`, `test-env-unit-tests`, `test-env-frontend-tests`) after
|
||||||
after `wait` returns and fails the job if any of them is non-zero.
|
`wait` returns and fails the job if any of them is non-zero. `./test-results/`
|
||||||
`./test-results/` is uploaded as a build artifact regardless of outcome.
|
is uploaded as a build artifact regardless of outcome.
|
||||||
|
|
||||||
`up -d` + `wait` keeps container teardown independent of any single
|
`up -d` + `wait` keeps container teardown independent of any single container's
|
||||||
container's exit. The one-shot `database.migrations`/`database.seed` jobs
|
exit. The one-shot `database.migrations`/`database.seed` jobs exit 0 partway
|
||||||
exit 0 partway through the run, and `frontend.tests` (which has no database
|
through the run, and `frontend.tests` (which has no database dependency)
|
||||||
dependency) reliably finishes before `api.specs`/`unit.tests` start; because
|
reliably finishes before `api.specs`/`unit.tests` start; because `wait` blocks
|
||||||
`wait` blocks on all three test containers, neither early exit ends the run
|
on all three test containers, neither early exit ends the run prematurely.
|
||||||
prematurely.
|
|
||||||
|
|
||||||
Exit codes:
|
Exit codes:
|
||||||
|
|
||||||
@@ -368,9 +374,9 @@ Exit codes:
|
|||||||
- Non-zero - Test failures occurred
|
- Non-zero - Test failures occurred
|
||||||
|
|
||||||
The frontend's Storybook Vitest and Playwright suites run inside the
|
The frontend's Storybook Vitest and Playwright suites run inside the
|
||||||
`frontend.tests` container (see [Docker Guide](docker.md)) as part of that
|
`frontend.tests` container (see [Docker Guide](docker.md)) as part of that same
|
||||||
same compose run, so no separate frontend CI step is needed. To run them
|
compose run, so no separate frontend CI step is needed. To run them locally
|
||||||
locally without Docker:
|
without Docker:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd web/frontend
|
cd web/frontend
|
||||||
@@ -381,9 +387,9 @@ npm run test:storybook:playwright
|
|||||||
|
|
||||||
### Running the workflow locally with `act`
|
### Running the workflow locally with `act`
|
||||||
|
|
||||||
[`act`](https://github.com/nektos/act) replays `.github/workflows/*.yml` on
|
[`act`](https://github.com/nektos/act) replays `.github/workflows/*.yml` on your
|
||||||
your own machine using Docker, so you can reproduce a CI run (or a fix for
|
own machine using Docker, so you can reproduce a CI run (or a fix for one)
|
||||||
one) without pushing a branch.
|
without pushing a branch.
|
||||||
|
|
||||||
**Install** (macOS):
|
**Install** (macOS):
|
||||||
|
|
||||||
@@ -401,34 +407,31 @@ act push \
|
|||||||
--container-architecture linux/amd64
|
--container-architecture linux/amd64
|
||||||
```
|
```
|
||||||
|
|
||||||
- `-j containerized-tests` runs only that job (there's currently just the
|
- `-j containerized-tests` runs only that job (there's currently just the one).
|
||||||
one).
|
- `-P ubuntu-latest=catthehacker/ubuntu:act-latest` pins the runner image act
|
||||||
- `-P ubuntu-latest=catthehacker/ubuntu:act-latest` pins the runner image
|
uses to impersonate `ubuntu-latest`. Without it, act's first run prompts
|
||||||
act uses to impersonate `ubuntu-latest`. Without it, act's first run
|
interactively to choose a default image size, which hangs/fails under a
|
||||||
prompts interactively to choose a default image size, which hangs/fails
|
non-interactive shell.
|
||||||
under a non-interactive shell.
|
|
||||||
- `--container-architecture linux/amd64` is required on Apple Silicon: the
|
- `--container-architecture linux/amd64` is required on Apple Silicon: the
|
||||||
`sqlserver` service's image is amd64-only, and act needs to know to
|
`sqlserver` service's image is amd64-only, and act needs to know to emulate
|
||||||
emulate that platform for the whole job container, not just that one
|
that platform for the whole job container, not just that one service.
|
||||||
service.
|
|
||||||
- No secrets or `.actrc` are needed: `generate-env.sh` creates its own
|
- No secrets or `.actrc` are needed: `generate-env.sh` creates its own
|
||||||
`.env.test` with freshly randomized values, same as in real CI.
|
`.env.test` with freshly randomized values, same as in real CI.
|
||||||
|
|
||||||
**Known act-only limitation**: the "Upload test results" step
|
**Known act-only limitation**: the "Upload test results" step
|
||||||
(`actions/upload-artifact@v4`) fails locally with `Unable to get the
|
(`actions/upload-artifact@v4`) fails locally with
|
||||||
ACTIONS_RUNTIME_TOKEN env variable`. act doesn't provide a real Actions
|
`Unable to get the ACTIONS_RUNTIME_TOKEN env variable`. act doesn't provide a
|
||||||
artifact backend, so this step, and only this step, is expected to fail
|
real Actions artifact backend, so this step, and only this step, is expected to
|
||||||
under act even when everything else passes. It works normally on
|
fail under act even when everything else passes. It works normally on
|
||||||
GitHub-hosted runners.
|
GitHub-hosted runners.
|
||||||
|
|
||||||
**Stale local state**: `docker-compose.test.yaml` uses fixed container
|
**Stale local state**: `docker-compose.test.yaml` uses fixed container names and
|
||||||
names and named volumes (`sqlserverdata-test`, `seaweedfsdata-test`) scoped
|
named volumes (`sqlserverdata-test`, `seaweedfsdata-test`) scoped to the `web`
|
||||||
to the `web` compose project. If a previous local run (via `act` or a
|
compose project. If a previous local run (via `act` or a manual `docker compose`
|
||||||
manual `docker compose` invocation) didn't get torn down, its SQL Server
|
invocation) didn't get torn down, its SQL Server volume can persist with an old
|
||||||
volume can persist with an old `SA_PASSWORD` baked in, while a fresh
|
`SA_PASSWORD` baked in, while a fresh `.env.test` generates a new one each run,
|
||||||
`.env.test` generates a new one each run, causing `Login failed for user
|
causing `Login failed for user 'sa'` errors that look like a test bug but are
|
||||||
'sa'` errors that look like a test bug but are really a leftover volume.
|
really a leftover volume. Clear it before re-running:
|
||||||
Clear it before re-running:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose --env-file web/.env.test -f web/docker-compose.test.yaml down -v
|
docker compose --env-file web/.env.test -f web/docker-compose.test.yaml down -v
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
# Token validation architecture
|
---
|
||||||
|
title: JWT token validation architecture
|
||||||
|
last-updated: 2026-08-31
|
||||||
|
tags:
|
||||||
|
- jwt
|
||||||
|
- authentication
|
||||||
|
- security
|
||||||
|
- tokens
|
||||||
|
---
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -64,10 +72,9 @@ Token generation and validation live on the same slice-internal service.
|
|||||||
|
|
||||||
`TokenService` is registered by `Features.Users`' own `AddFeaturesUsers()`
|
`TokenService` is registered by `Features.Users`' own `AddFeaturesUsers()`
|
||||||
extension method, except for the lower-level `ITokenInfrastructure` (JWT
|
extension method, except for the lower-level `ITokenInfrastructure` (JWT
|
||||||
signing/verification) it
|
signing/verification) it depends on, which is registered by the host
|
||||||
depends on, which is registered by the host (`API.Core/Program.cs`) since
|
(`API.Core/Program.cs`) since `JwtAuthenticationHandler` (host-level auth
|
||||||
`JwtAuthenticationHandler` (host-level auth middleware) also depends on it
|
middleware) also depends on it directly.
|
||||||
directly.
|
|
||||||
|
|
||||||
### Integration points
|
### Integration points
|
||||||
|
|
||||||
@@ -77,14 +84,16 @@ directly.
|
|||||||
|
|
||||||
1. Receives confirmation token from user via `ConfirmUserCommand`
|
1. Receives confirmation token from user via `ConfirmUserCommand`
|
||||||
2. Calls `ITokenService.ValidateConfirmationTokenAsync()`
|
2. Calls `ITokenService.ValidateConfirmationTokenAsync()`
|
||||||
3. Extracts user ID from validated token and looks it up via `UserManager<ApplicationUser>`
|
3. Extracts user ID from validated token and looks it up via
|
||||||
4. Marks the account confirmed via `IUserEmailStore<ApplicationUser>.SetEmailConfirmedAsync()`,
|
`UserManager<ApplicationUser>`
|
||||||
then persists it via `UserManager<ApplicationUser>.UpdateAsync()`
|
4. Marks the account confirmed via
|
||||||
|
`IUserEmailStore<ApplicationUser>.SetEmailConfirmedAsync()`, then persists it
|
||||||
|
via `UserManager<ApplicationUser>.UpdateAsync()`
|
||||||
5. Returns confirmation result
|
5. Returns confirmation result
|
||||||
|
|
||||||
`UserManager<ApplicationUser>` and `IUserEmailStore<ApplicationUser>` are
|
`UserManager<ApplicationUser>` and `IUserEmailStore<ApplicationUser>` are
|
||||||
ASP.NET Core Identity's abstractions, not EF Core's default Identity store:
|
ASP.NET Core Identity's abstractions, not EF Core's default Identity store: the
|
||||||
the app backs them with its own `DapperUserStore` (see
|
app backs them with its own `DapperUserStore` (see
|
||||||
[Database](database.md#database-error-handling)), which persists against the
|
[Database](database.md#database-error-handling)), which persists against the
|
||||||
existing `UserAccount`/`UserCredential`/`UserVerification` tables via Dapper,
|
existing `UserAccount`/`UserCredential`/`UserVerification` tables via Dapper,
|
||||||
consistent with the rest of the app's repository pattern.
|
consistent with the rest of the app's repository pattern.
|
||||||
|
|||||||
Reference in New Issue
Block a user