mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-15 01:54:43 +00:00
Cleanup prose in written documentation
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
# Architecture
|
||||
|
||||
This document describes the active architecture of The Biergarten App.
|
||||
This document describes the architecture of The Biergarten App.
|
||||
|
||||
## High-level overview
|
||||
|
||||
The Biergarten App is a monorepo with a clear split between the backend and the
|
||||
active website:
|
||||
The Biergarten App is a monorepo split between the backend and the website:
|
||||
|
||||
- **Backend**: .NET 10 Web API with SQL Server, organized as vertical feature
|
||||
slices with MediatR
|
||||
@@ -13,8 +12,8 @@ active website:
|
||||
- **Architecture Style**: Vertical-slice backend plus server-rendered React
|
||||
frontend
|
||||
|
||||
The legacy Next.js frontend has been retained in `archive/next-js-web-app/` for
|
||||
reference only.
|
||||
`archive/next-js-web-app/` contains an archived Next.js frontend, kept for
|
||||
reference.
|
||||
|
||||
## Diagrams
|
||||
|
||||
@@ -24,11 +23,25 @@ For visual representations, see:
|
||||
architecture diagram
|
||||
- [deployment.svg](website/diagrams-out/deployment.svg) - Docker deployment
|
||||
diagram
|
||||
- [authentication-flow.svg](website/diagrams-out/authentication-flow.svg) -
|
||||
Authentication workflow
|
||||
- [database-schema.svg](website/diagrams-out/database-schema.svg) - Database
|
||||
relationships
|
||||
|
||||
Authentication workflow diagrams, one per flow, under
|
||||
`website/diagrams-out/auth/`:
|
||||
|
||||
- [registration.svg](website/diagrams-out/auth/registration.svg)
|
||||
- [login.svg](website/diagrams-out/auth/login.svg)
|
||||
- [refresh-token.svg](website/diagrams-out/auth/refresh-token.svg)
|
||||
- [confirm-user.svg](website/diagrams-out/auth/confirm-user.svg)
|
||||
- [resend-confirmation.svg](website/diagrams-out/auth/resend-confirmation.svg)
|
||||
- [update-username.svg](website/diagrams-out/auth/update-username.svg)
|
||||
- [update-email.svg](website/diagrams-out/auth/update-email.svg)
|
||||
- [update-password.svg](website/diagrams-out/auth/update-password.svg)
|
||||
- [update-profile.svg](website/diagrams-out/auth/update-profile.svg)
|
||||
- [delete-account.svg](website/diagrams-out/auth/delete-account.svg)
|
||||
- [error-handling.svg](website/diagrams-out/auth/error-handling.svg) - shared
|
||||
`GlobalExceptionFilter` mapping used by all flows above
|
||||
|
||||
## Backend architecture
|
||||
|
||||
### Vertical-slice architecture pattern
|
||||
@@ -38,7 +51,7 @@ technical layers. Each feature (`Features.Users`, `Features.Breweries`,
|
||||
`Features.Emails`, `Features.Locations`, `Features.PhotoUpload`) is a single
|
||||
project that owns its own MediatR commands/queries/handlers, validators, and
|
||||
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:
|
||||
|
||||
```
|
||||
@@ -128,12 +141,12 @@ own
|
||||
|
||||
`Features.Emails` has no `Controllers/` folder. It's invoked only via MediatR
|
||||
commands sent from other slices, never over HTTP. `Features.Locations` also has
|
||||
no `Controllers/` folder and no MediatR handlers — it exposes
|
||||
no `Controllers/` folder and no MediatR handlers; it exposes
|
||||
`ILocationRepository` directly as a plain service, currently consumed only by
|
||||
`Database.Seed` to look up cities while seeding brewery locations. Other slices
|
||||
(for example, `Features.Breweries`) do not yet depend on it; they run their own
|
||||
`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
|
||||
bound to an HTTP route directly. The one exception today is the `/upload-test`
|
||||
minimal API endpoint wired directly in `API.Core/Program.cs`, a temporary route
|
||||
@@ -320,12 +333,12 @@ method that registers its repository and slice-internal services
|
||||
#### Direct SQL via Dapper/ADO.NET
|
||||
|
||||
**Purpose**: Keep data access explicit and colocated with the slice that owns
|
||||
it, without an ORM's change-tracking or mapping magic
|
||||
it
|
||||
|
||||
**Strategy**:
|
||||
|
||||
- No ORM (Entity Framework not used); each repository issues inline SQL
|
||||
through Dapper, with no manual `IDbCommand` parameter binding. The one
|
||||
- No ORM; each repository issues inline SQL through Dapper, which handles
|
||||
parameter binding. The one
|
||||
wrinkle is the `GEOGRAPHY` column on `BreweryPostLocation`, which Dapper
|
||||
can't deserialize as a UDT: reads select `CONVERT(varbinary(max),
|
||||
Coordinates)` so the value comes back as a plain `byte[]` Dapper can bind.
|
||||
@@ -334,26 +347,25 @@ it, without an ORM's change-tracking or mapping magic
|
||||
avoiding a client-side distance calculation over every row. No spatial
|
||||
index backs the query yet; `schema.sql` sketches one, commented out, for
|
||||
when the table grows large enough to need it
|
||||
- Referential checks that a stored procedure used to perform (e.g. "does this
|
||||
`CityId` exist?") are now explicit `SELECT 1 ...` existence checks in the
|
||||
repository method, run inside the same transaction as the write
|
||||
- Referential checks (for example, "does this `CityId` exist?") are explicit
|
||||
`SELECT 1 ...` existence checks in the repository method, run inside the
|
||||
same transaction as the write
|
||||
- Optimistic concurrency uses each table's `RowVersion` (`ROWVERSION`) column,
|
||||
checked in the `UPDATE ... WHERE ... AND RowVersion = @RowVersion` clause
|
||||
- Repositories throw `Domain.Exceptions` types (`NotFoundException`,
|
||||
`ConflictException`, ...) directly when a check fails, rather than relying on
|
||||
a database-side `THROW`; `API.Core`'s `GlobalExceptionFilter` maps those
|
||||
exception types to HTTP status codes
|
||||
`ConflictException`, ...) directly when a check fails; `API.Core`'s
|
||||
`GlobalExceptionFilter` maps those exception types to HTTP status codes
|
||||
- Application focuses on orchestration; the database enforces integrity via
|
||||
keys, `CHECK` constraints, and cascades
|
||||
|
||||
See [Database](website/database.md) for the schema and the app's current SQL
|
||||
See [Database](website/database.md) for the schema and the app's SQL
|
||||
error-handling approach in more detail.
|
||||
|
||||
## Frontend architecture
|
||||
|
||||
### Active website (`web/frontend`)
|
||||
### Website (`web/frontend`)
|
||||
|
||||
The current website is a React Router 7 application with server-side rendering
|
||||
The website is a React Router 7 application with server-side rendering
|
||||
enabled.
|
||||
|
||||
```text
|
||||
@@ -397,7 +409,7 @@ web/frontend/
|
||||
|
||||
### Theme system
|
||||
|
||||
The active website uses semantic DaisyUI theme tokens backed by four Biergarten
|
||||
The website uses semantic DaisyUI theme tokens backed by four Biergarten
|
||||
themes:
|
||||
|
||||
- Biergarten Lager
|
||||
@@ -408,11 +420,10 @@ themes:
|
||||
All component styling should prefer semantic tokens such as `primary`,
|
||||
`success`, `surface`, and `highlight` instead of hard-coded color values.
|
||||
|
||||
### Legacy frontend
|
||||
### Archived frontend
|
||||
|
||||
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`.
|
||||
`archive/next-js-web-app/` contains an archived Next.js frontend, kept for
|
||||
reference. Product and engineering documentation points to `web/frontend`.
|
||||
|
||||
## Security architecture
|
||||
|
||||
@@ -431,8 +442,8 @@ for reference only. Active product and engineering documentation should point to
|
||||
3. **Login**:
|
||||
- User submits credentials
|
||||
- Password verified against hash
|
||||
- Access and refresh JWTs are issued; the refresh token is stored server-side
|
||||
against the user's credential row
|
||||
- Access and refresh JWTs are issued and returned to the client; neither is
|
||||
stored server-side
|
||||
|
||||
4. **Token refresh**:
|
||||
- Client exchanges a valid, unexpired refresh token for a new access/refresh
|
||||
|
||||
@@ -47,7 +47,7 @@ as a source of truth for brewing techniques or cultural representation.**
|
||||
|
||||
**Natural language processing is a powerful tool for data analysis and
|
||||
generation, but its output should be treated with scrutiny. Human language is
|
||||
not simply data points to be analyzed — it carries cultural and human meaning
|
||||
not simply data points to be analyzed: it carries cultural and human meaning
|
||||
that artificial intelligence cannot capture.**
|
||||
|
||||
---
|
||||
@@ -102,7 +102,7 @@ vendored verbatim, unmodified, from
|
||||
(the `common-forenames-by-country.json` / `common-surnames-by-country.json`
|
||||
release assets), released under **CC0** (public domain). That dataset's own
|
||||
forename/surname lists are pulled from Wikipedia's "Lists of most common
|
||||
surnames" and "List of most popular given names" as of the week of 2023-07-08 —
|
||||
surnames" and "List of most popular given names" as of the week of 2023-07-08;
|
||||
see that project's README for full provenance. Names are not LLM-generated; this
|
||||
is curated fixture data per ROADMAP.md §2. Per-forename gender from the source
|
||||
data is preserved through to the sampled `Name` (rather than discarded during
|
||||
@@ -113,7 +113,7 @@ for surnames) rather than trimmed to `locations.json`'s current country list, so
|
||||
it doesn't need re-sourcing if more countries are added later. `SampleName()` (a
|
||||
free helper in `generate_users.cc`) returns no result for a country present in
|
||||
neither file; of the countries in `locations.json`, that's currently `KE`, `SE`,
|
||||
`SG`, `TH`, `VN`, and `ZA` — `GenerateUsers` skips cities in those countries the
|
||||
`SG`, `TH`, `VN`, and `ZA`; `GenerateUsers` skips cities in those countries the
|
||||
same way brewery generation skips cities whose enrichment lookup fails.
|
||||
|
||||
---
|
||||
@@ -290,8 +290,8 @@ 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
|
||||
`latitude`/`longitude` from `locations.json`.
|
||||
|
||||
The resulting point is **plausible by construction** — it always falls within
|
||||
the city's vicinity — but it is not looked up from any real address registry.
|
||||
The resulting point is **plausible by construction**, always falling within
|
||||
the city's vicinity, but it is not looked up from any real address registry.
|
||||
It does not correspond to a real address, business, or resident, and should
|
||||
not be treated as accurate geocoding data. This is consistent with the rest of
|
||||
the dataset: fixture data for a proof-of-concept, not a source of truth.
|
||||
|
||||
@@ -7,12 +7,12 @@ deterministic mock.
|
||||
|
||||
> **This pipeline produces AI-generated data.** It is not a source of truth for
|
||||
> brewing techniques, cultural representation, or local-language accuracy. See
|
||||
> [ETHICS-AND-KNOWN-ISSUES.md](./ETHICS-AND-KNOWN-ISSUES.md) for a full
|
||||
> [ETHICS-AND-KNOWN-ISSUES.md](./ETHICS-AND-KNOWN-ISSUES.md) for full
|
||||
> documentation of limitations, hallucination patterns, and bias.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
## Table of contents
|
||||
|
||||
- [How it fits the main app](#how-it-fits-the-main-app)
|
||||
- [Quick start](#quick-start)
|
||||
@@ -176,7 +176,7 @@ output to confirm the fast path was taken.
|
||||
### Run the container
|
||||
|
||||
The container always runs the model-backed path; there is no `--mocked`
|
||||
container mode (use a native build for that — see [Quick start](#quick-start)).
|
||||
container mode (use a native build for that; see [Quick start](#quick-start)).
|
||||
The entrypoint, `runpod/start.sh`, downloads the GGUF model automatically if it
|
||||
is not already present at the configured path.
|
||||
|
||||
@@ -191,7 +191,7 @@ docker run --rm \
|
||||
|
||||
By default this downloads `google_gemma-4-E4B-it-Q6_K.gguf` to `./models/` on
|
||||
first run if it isn't already there. To use a pre-downloaded model, place it at
|
||||
that path first — see [Model](#model) above.
|
||||
that path first; see [Model](#model).
|
||||
|
||||
#### Environment variables
|
||||
|
||||
@@ -211,8 +211,9 @@ configurable via environment variable.
|
||||
|
||||
Use a GPU pod template. Mount persistent storage for `/workspace/models`,
|
||||
`/workspace/output`, and `/workspace/logs`. See
|
||||
`tooling/pipeline/runpod/pod-template.yaml` for a starter template — set the
|
||||
environment variables listed above to match your run.
|
||||
`tooling/pipeline/runpod/pod-template.yaml` for a starter template; set the
|
||||
environment variables listed in [Environment variables](#environment-variables)
|
||||
to match your run.
|
||||
|
||||
---
|
||||
|
||||
@@ -236,24 +237,24 @@ skipped and the pipeline continues. `GenerateUsers()` runs before
|
||||
|
||||
### Key components
|
||||
|
||||
- `src/main.cc` — argument parsing and Boost.DI composition root.
|
||||
- `CuratedJsonDataService` — implements `ICuratedDataService`; takes a
|
||||
- `src/main.cc`: argument parsing and Boost.DI composition root.
|
||||
- `CuratedJsonDataService`: implements `ICuratedDataService`; takes a
|
||||
`CuratedDataFilePaths` DTO (locations/personas/forenames/surnames paths) in
|
||||
its constructor, then parses and validates curated location, persona, and
|
||||
forename/surname JSON, memoizing each result after its first load on a given
|
||||
instance. `MockCuratedDataService` is the in-memory substitute (4 fixed
|
||||
locations, 3 personas, and name data for `US`/`DE`/`FR`/`BE`) used in
|
||||
`--mocked` runs.
|
||||
- `WikipediaEnrichmentService` — queries Wikipedia extracts, caches results,
|
||||
- `WikipediaEnrichmentService`: queries Wikipedia extracts, caches results,
|
||||
returns empty context on failure. `MockEnrichmentService` is the no-op
|
||||
substitute used in `--mocked` runs.
|
||||
- `LlamaGenerator` — formats prompts for Gemma 4, validates JSON output for both
|
||||
- `LlamaGenerator`: formats prompts for Gemma 4, validates JSON output for both
|
||||
`GenerateBrewery` and `GenerateUser`, retries malformed responses up to three
|
||||
times with corrective feedback in the retry prompt. The token budget is fixed
|
||||
across attempts; it is not raised automatically on truncation.
|
||||
- `MockGenerator` — stable hash-based output so the same city/persona/name input
|
||||
- `MockGenerator`: stable hash-based output so the same city/persona/name input
|
||||
always produces the same brewery or user.
|
||||
- `SqliteExportService` — creates a dated SQLite file per run and persists each
|
||||
- `SqliteExportService`: creates a dated SQLite file per run and persists each
|
||||
successful user and brewery into normalized tables.
|
||||
- Brewery payloads include English and local-language name and description
|
||||
fields. User payloads carry a sampled first/last name and gender, an
|
||||
@@ -281,13 +282,13 @@ runs.
|
||||
`CuratedJsonDataService` memoizes each of `LoadLocations()`, `LoadPersonas()`,
|
||||
`LoadForenamesByCountry()`, and `LoadSurnamesByCountry()` independently the
|
||||
first time each is called, since `BiergartenPipelineOrchestrator` owns a single
|
||||
`ICuratedDataService` instance for the whole run — later calls return the cached
|
||||
`ICuratedDataService` instance for the whole run; later calls return the cached
|
||||
result instead of re-parsing.
|
||||
|
||||
`GenerateUsers()` samples a forename/surname pair per city via `SampleName()`,
|
||||
keyed by the city's ISO 3166-1 code. Countries present in `locations.json` but
|
||||
absent from either name fixture (currently `KE`, `SE`, `SG`, `TH`, `VN`, `ZA`)
|
||||
are skipped, the same way a failed enrichment or generation call skips a city —
|
||||
are skipped, the same way a failed enrichment or generation call skips a city;
|
||||
see ETHICS-AND-KNOWN-ISSUES.md's Names-by-Country Dataset section.
|
||||
|
||||
### Process flow - activity diagram
|
||||
@@ -357,15 +358,14 @@ The build fetches Boost.DI, spdlog, and SQLite via CMake. llama.cpp is fetched
|
||||
only when a system installation is not detected. Metal is enabled on Apple
|
||||
Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
|
||||
|
||||
> **Code Style:** Code style generally follows the Google Style Guide and
|
||||
> features modern C++20 throughout. Formatting follows the Google C++ Style
|
||||
> Guide, with the exception of indentation width (3 spaces vs. Google's 2).
|
||||
> **Code style:** The code follows the Google C++ Style Guide, except that
|
||||
> indentation is 3 spaces instead of Google's 2, and uses C++20 throughout.
|
||||
|
||||
---
|
||||
|
||||
## Tested hardware
|
||||
|
||||
### ARM macOS — M1 Pro
|
||||
### ARM macOS (M1 Pro)
|
||||
|
||||
| | |
|
||||
| --------- | --------------------------------- |
|
||||
@@ -376,7 +376,7 @@ Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
|
||||
| Model | Gemma 4 E4B |
|
||||
| Inference | llama.cpp with Metal |
|
||||
|
||||
### x86_64 Linux — NVIDIA RTX 2000
|
||||
### x86_64 Linux (NVIDIA RTX 2000)
|
||||
|
||||
| | |
|
||||
| --------- | ------------------------------ |
|
||||
@@ -387,7 +387,7 @@ Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
|
||||
| Model | Gemma 4 E4B |
|
||||
| Inference | llama.cpp with CUDA 12.x |
|
||||
|
||||
### x86_64 Linux — Docker / RunPod (NVIDIA CUDA)
|
||||
### x86_64 Linux (Docker / RunPod, NVIDIA CUDA)
|
||||
|
||||
| | |
|
||||
| --------- | ------------------------------------------- |
|
||||
@@ -407,7 +407,7 @@ Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
|
||||
- Keep `locations.json` structured enough to support discovery and future
|
||||
filtering.
|
||||
- `personas.json`, `forenames-by-country.json`, and `surnames-by-country.json`
|
||||
are curated/vendored fixture data, not LLM-generated — see
|
||||
are curated/vendored fixture data, not LLM-generated; see
|
||||
ETHICS-AND-KNOWN-ISSUES.md's Names-by-Country Dataset section for provenance.
|
||||
- Treat SQLite output as seed material for the app's brewery and user domains,
|
||||
not production data.
|
||||
@@ -435,18 +435,18 @@ Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
|
||||
|
||||
Paths below are relative to `tooling/pipeline/`.
|
||||
|
||||
- `src/main.cc` — argument parsing and DI composition root.
|
||||
- `src/biergarten_pipeline_orchestrator/` — orchestration, sampling, logging,
|
||||
- `src/main.cc`: argument parsing and DI composition root.
|
||||
- `src/biergarten_pipeline_orchestrator/`: orchestration, sampling, logging,
|
||||
and export.
|
||||
- `src/services/curated_data/` — `CuratedJsonDataService`, the file-backed
|
||||
- `src/services/curated_data/`: `CuratedJsonDataService`, the file-backed
|
||||
`ICuratedDataService`, and `MockCuratedDataService`, the in-memory
|
||||
`ICuratedDataService` used in `--mocked` runs.
|
||||
- `src/services/enrichment/wikipedia/` — enrichment service and cache.
|
||||
- `src/services/sqlite/` — SQLite export implementation.
|
||||
- `src/data_generation/llama/` — local inference, prompt loading, output
|
||||
- `src/services/enrichment/wikipedia/`: enrichment service and cache.
|
||||
- `src/services/sqlite/`: SQLite export implementation.
|
||||
- `src/data_generation/llama/`: local inference, prompt loading, output
|
||||
validation.
|
||||
- `src/data_generation/mock/` — deterministic fallback.
|
||||
- `runpod/` — container build and runtime launcher.
|
||||
- `src/data_generation/mock/`: deterministic fallback.
|
||||
- `runpod/`: container build and runtime launcher.
|
||||
|
||||
---
|
||||
|
||||
@@ -458,7 +458,7 @@ exercise the full brewery and social domains without live data. For the detailed
|
||||
engineering breakdown of what's needed to reach the architecture in
|
||||
[`diagrams/planned/`](./diagrams/planned/), see [ROADMAP.md](./ROADMAP.md).
|
||||
|
||||
### Testing — very high priority
|
||||
### Testing (very high priority)
|
||||
|
||||
- Unit test JSON validation and retry logic against malformed, truncated, and
|
||||
empty model outputs.
|
||||
@@ -481,7 +481,7 @@ enough to exercise search, sort, and category filters.
|
||||
### Check-in system
|
||||
|
||||
Produce timestamped check-in events between users and breweries. Use a J-curve
|
||||
activity profile — a small set of users accounts for most check-ins, the rest
|
||||
activity profile: a small set of users accounts for most check-ins, the rest
|
||||
appear occasionally. Add bursty behaviour around weekends and travel periods.
|
||||
|
||||
### Beer ratings
|
||||
|
||||
@@ -59,8 +59,8 @@ Scripts live under `scripts/`, applied in order and tracked so a script never
|
||||
re-runs once it has succeeded. Today that's `01-schema/`, with one script per
|
||||
table, numbered so a table's foreign keys always point at an earlier-numbered
|
||||
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 editing
|
||||
an already-applied script means it won't re-run against existing databases.
|
||||
editing an existing one in place: DbUp's tracking is per-script, so an edited
|
||||
already-applied script never re-runs against existing databases.
|
||||
|
||||
## Data seeding
|
||||
|
||||
@@ -68,7 +68,7 @@ an already-applied script means it won't re-run against existing databases.
|
||||
pipeline (see [Pipeline README](../pipeline/README.md)): countries,
|
||||
state/provinces, and cities (created on demand while resolving each brewery's
|
||||
location), user accounts, and brewery posts with locations. It doesn't call
|
||||
every feature slice that has a write path — `Photo`, for example, has a
|
||||
every feature slice that has a write path: `Photo`, for example, has a
|
||||
repository (`Features.PhotoUpload`) but no seed data.
|
||||
|
||||
## Database error handling
|
||||
@@ -78,10 +78,10 @@ repository (`Features.PhotoUpload`) but no seed data.
|
||||
- On failure, the repository throws a `Domain.Exceptions` type directly
|
||||
(`NotFoundException`, `ConflictException`, `ArgumentException`, ...).
|
||||
`API.Core`'s `GlobalExceptionFilter` maps these to HTTP status codes (404,
|
||||
409, 400, ...); an unmapped `SqlException` (e.g. a genuine connectivity or
|
||||
constraint-violation failure) maps to 503.
|
||||
409, 400, ...); an unmapped `SqlException` (for example, a genuine
|
||||
connectivity or constraint-violation failure) maps to 503.
|
||||
|
||||
- One exception: `Features.Users`' `DapperUserStore` does catch
|
||||
`SqlException` directly, to detect a duplicate-key race (SQL Server error
|
||||
2601 or 2627) when two requests try to verify the same account
|
||||
concurrently — see `DapperUserStore.IsDuplicateKeyViolation`.
|
||||
concurrently; see `DapperUserStore.IsDuplicateKeyViolation`.
|
||||
|
||||
@@ -100,7 +100,8 @@ frontend.tests # Storybook Vitest + Playwright suites (own Playwright image)
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
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 up -d
|
||||
docker compose --env-file web/.env.test -f web/docker-compose.test.yaml wait api.specs unit.tests frontend.tests
|
||||
|
||||
# View results
|
||||
ls -la test-results/
|
||||
@@ -111,6 +112,13 @@ cat test-results/Features.Users.Tests.trx
|
||||
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
|
||||
stopped, regardless of which one finishes first; `up --abort-on-container-exit`
|
||||
isn't used here because the one-shot `database.migrations`/`database.seed`
|
||||
jobs and `frontend.tests` (no database dependency) can exit before the other
|
||||
test containers, which would end the run prematurely. See
|
||||
[Testing](testing.md#running-tests-with-docker-recommended) for details.
|
||||
|
||||
### 3. Production (`docker-compose.prod.yaml`)
|
||||
|
||||
**Purpose**: Production-ready deployment
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Environment variables
|
||||
|
||||
This document covers the active environment variables used by the current
|
||||
Biergarten stack.
|
||||
This document covers the environment variables used by the Biergarten stack.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -19,10 +18,10 @@ Direct environment variable access via `Environment.GetEnvironmentVariable()`.
|
||||
|
||||
### Frontend (`web/frontend`)
|
||||
|
||||
The active website reads runtime values from the server environment for its auth
|
||||
and API integration. In Docker, the `frontend` service is wired into the same
|
||||
The website reads runtime values from the server environment for its auth and
|
||||
API integration. In Docker, the `frontend` service is wired into the same
|
||||
`web/.env.dev` / `.env.test` / `.env.prod` files as the backend; running it
|
||||
locally with `npm run dev` still reads from the shell environment.
|
||||
locally with `npm run dev` reads from the shell environment.
|
||||
|
||||
### Docker
|
||||
|
||||
@@ -104,7 +103,8 @@ WEBSITE_BASE_URL=https://thebiergarten.app # Base URL for the website
|
||||
**Security Requirements**:
|
||||
|
||||
- Use at least 32 characters per secret
|
||||
- Use 127 or more characters in production
|
||||
- Use secrets generated from 127 or more random bytes in production (base64
|
||||
encoding turns 127 bytes into a longer string, well over 127 characters)
|
||||
- Generate secrets with a cryptographically secure random function
|
||||
- Never reuse secrets across token types or environments
|
||||
- Rotate secrets periodically in production
|
||||
@@ -170,10 +170,17 @@ ASPNETCORE_URLS=http://0.0.0.0:8080 # Binding address and port
|
||||
DOTNET_RUNNING_IN_CONTAINER=true # Flag for container execution
|
||||
```
|
||||
|
||||
`ASPNETCORE_URLS` is hardcoded directly in `docker-compose.dev.yaml` and
|
||||
`docker-compose.prod.yaml` (not sourced from the `.env` file), so it's
|
||||
required only for Docker. Outside Docker, `dotnet run` falls back to the
|
||||
`http` profile in `API.Core/Properties/launchSettings.json`
|
||||
(`http://localhost:5069`), so manual backend setup (see
|
||||
[Getting Started](getting-started.md#manual-backend-setup)) doesn't need to
|
||||
set it.
|
||||
|
||||
## Frontend variables (`web/frontend`)
|
||||
|
||||
The active website does not use the old Next.js/Prisma environment model. Its
|
||||
core runtime variables are:
|
||||
The website's core runtime variables are:
|
||||
|
||||
```bash
|
||||
API_BASE_URL=http://api.core:8080 # Base URL for the .NET API (container DNS name in Docker)
|
||||
@@ -291,12 +298,6 @@ cp web/.env.example web/.env.dev
|
||||
# Edit web/.env.dev with your values
|
||||
```
|
||||
|
||||
## Legacy frontend variables
|
||||
|
||||
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**:
|
||||
|
||||
- `web/docker-compose.dev.yaml` → `web/.env.dev`
|
||||
@@ -334,7 +335,7 @@ reference only and is not run as part of the active stack.
|
||||
| `PORT` | | ✓ | ✓ | No | Frontend server port |
|
||||
| `CLEAR_DATABASE` | ✓ | | ✓ | No | Dev/test reset flag |
|
||||
| `ASPNETCORE_ENVIRONMENT` | ✓ | | ✓ | Yes | ASP.NET environment |
|
||||
| `ASPNETCORE_URLS` | ✓ | | ✓ | Yes | API binding address |
|
||||
| `ASPNETCORE_URLS` | ✓ | | ✓ | Yes\*\* | API binding address |
|
||||
| `SA_PASSWORD` | | | ✓ | Yes | SQL Server container |
|
||||
| `ACCEPT_EULA` | | | ✓ | Yes | SQL Server EULA |
|
||||
| `MSSQL_PID` | | | ✓ | No | SQL Server edition |
|
||||
@@ -343,6 +344,10 @@ reference only and is not run as part of the active stack.
|
||||
\* Either `DB_CONNECTION_STRING` OR the component variables (`DB_SERVER`,
|
||||
`DB_NAME`, `DB_USER`, `DB_PASSWORD`) must be provided.
|
||||
|
||||
\*\* Required for Docker, where it's hardcoded in the compose files. Outside
|
||||
Docker, `dotnet run` falls back to the `applicationUrl` in
|
||||
`launchSettings.json`.
|
||||
|
||||
## Validation
|
||||
|
||||
### Backend validation
|
||||
@@ -359,8 +364,8 @@ Variables are validated at startup:
|
||||
|
||||
### Frontend validation
|
||||
|
||||
The active website relies on runtime defaults for local development and the
|
||||
surrounding server environment in deployed environments.
|
||||
The website relies on runtime defaults for local development and on the server
|
||||
environment when deployed.
|
||||
|
||||
- `API_BASE_URL` defaults to `http://localhost:8080`
|
||||
- `SESSION_SECRET` falls back to a development-only local secret
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Getting started
|
||||
|
||||
This guide covers local setup for the current Biergarten stack: the .NET backend
|
||||
in `web/backend` and the active React Router frontend in `web/frontend`.
|
||||
This guide covers local setup for the Biergarten stack: the .NET backend in
|
||||
`web/backend` and the React Router frontend in `web/frontend`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -69,7 +69,7 @@ Available endpoints:
|
||||
- Mailpit UI: http://localhost:8025
|
||||
- SeaweedFS S3 API: http://localhost:8333
|
||||
|
||||
### 4. Start the active frontend
|
||||
### 4. Start the frontend
|
||||
|
||||
```bash
|
||||
cd web/frontend
|
||||
@@ -146,10 +146,10 @@ dotnet run --project Database/Database.Seed/Database.Seed.csproj
|
||||
dotnet run --project API/API.Core/API.Core.csproj
|
||||
```
|
||||
|
||||
## Legacy frontend note
|
||||
## Archived frontend
|
||||
|
||||
The previous Next.js frontend lives in `archive/next-js-web-app/` for reference
|
||||
only and is not the active website.
|
||||
`archive/next-js-web-app/` contains an archived Next.js frontend, kept for
|
||||
reference; it is not part of the running stack.
|
||||
|
||||
## Next steps
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ The project uses a multi-layered testing approach across backend and frontend:
|
||||
|
||||
## Running tests with Docker (recommended)
|
||||
|
||||
The easiest way to run all tests is using Docker Compose, which sets up an
|
||||
isolated test environment:
|
||||
To run all tests, use Docker Compose, which sets up an isolated test
|
||||
environment:
|
||||
|
||||
```bash
|
||||
docker compose --env-file web/.env.test -f web/docker-compose.test.yaml up -d
|
||||
@@ -43,7 +43,7 @@ This command:
|
||||
stopped, regardless of which one finishes first. Long-running services
|
||||
(`sqlserver`, `seaweedfs`) and the one-shot `database.migrations` /
|
||||
`database.seed` jobs keep running in the background alongside them and are
|
||||
torn down separately (see below) — `wait` doesn't touch them.
|
||||
torn down separately (see below); `wait` doesn't touch them.
|
||||
|
||||
### View test results
|
||||
|
||||
@@ -355,15 +355,12 @@ The workflow inspects the exit code of each test container
|
||||
after `wait` returns and fails the job if any of them is non-zero.
|
||||
`./test-results/` is uploaded as a build artifact regardless of outcome.
|
||||
|
||||
Earlier versions of this workflow used
|
||||
`up --abort-on-container-exit`, which stops the entire stack the instant
|
||||
*any* watched container exits. That's wrong here: the one-shot
|
||||
`database.migrations`/`database.seed` jobs are expected to exit 0 partway
|
||||
through, and even scoping the flag to just the three test services doesn't
|
||||
help, since `frontend.tests` (no database dependency) reliably finishes
|
||||
before `api.specs`/`unit.tests` even start, aborting them prematurely.
|
||||
`up -d` + `wait` avoids both races by not tying container teardown to any
|
||||
single container's exit.
|
||||
`up -d` + `wait` keeps container teardown independent of any single
|
||||
container's exit. The one-shot `database.migrations`/`database.seed` jobs
|
||||
exit 0 partway through the run, and `frontend.tests` (which has no database
|
||||
dependency) reliably finishes before `api.specs`/`unit.tests` start; because
|
||||
`wait` blocks on all three test containers, neither early exit ends the run
|
||||
prematurely.
|
||||
|
||||
Exit codes:
|
||||
|
||||
@@ -414,13 +411,13 @@ act push \
|
||||
`sqlserver` service's image is amd64-only, and act needs to know to
|
||||
emulate that platform for the whole job container, not just that one
|
||||
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.
|
||||
|
||||
**Known act-only limitation**: the "Upload test results" step
|
||||
(`actions/upload-artifact@v4`) fails locally with `Unable to get the
|
||||
ACTIONS_RUNTIME_TOKEN env variable`. act doesn't provide a real Actions
|
||||
artifact backend, so this step — and only this step — is expected to fail
|
||||
artifact backend, so this step, and only this step, is expected to fail
|
||||
under act even when everything else passes. It works normally on
|
||||
GitHub-hosted runners.
|
||||
|
||||
@@ -429,7 +426,7 @@ names and named volumes (`sqlserverdata-test`, `seaweedfsdata-test`) scoped
|
||||
to the `web` compose project. If a previous local run (via `act` or a
|
||||
manual `docker compose` invocation) didn't get torn down, its SQL Server
|
||||
volume can persist with an old `SA_PASSWORD` baked in, while a fresh
|
||||
`.env.test` generates a new one each run — causing `Login failed for user
|
||||
`.env.test` generates a new one each run, causing `Login failed for user
|
||||
'sa'` errors that look like a test bug but are really a leftover volume.
|
||||
Clear it before re-running:
|
||||
|
||||
|
||||
@@ -34,8 +34,7 @@ Low-level JWT operations.
|
||||
|
||||
#### [ITokenService](../../web/backend/Features/Features.Users/Services/ITokenService.cs)
|
||||
|
||||
Both token generation and validation live on the same slice-internal service
|
||||
(there is no separate validation service/class).
|
||||
Token generation and validation live on the same slice-internal service.
|
||||
|
||||
**Generation methods:**
|
||||
|
||||
@@ -79,9 +78,17 @@ directly.
|
||||
1. Receives confirmation token from user via `ConfirmUserCommand`
|
||||
2. Calls `ITokenService.ValidateConfirmationTokenAsync()`
|
||||
3. Extracts user ID from validated token and looks it up via `UserManager<ApplicationUser>`
|
||||
4. Marks the account confirmed via `IUserEmailStore<ApplicationUser>.SetEmailConfirmedAsync()`
|
||||
4. Marks the account confirmed via `IUserEmailStore<ApplicationUser>.SetEmailConfirmedAsync()`,
|
||||
then persists it via `UserManager<ApplicationUser>.UpdateAsync()`
|
||||
5. Returns confirmation result
|
||||
|
||||
`UserManager<ApplicationUser>` and `IUserEmailStore<ApplicationUser>` are
|
||||
ASP.NET Core Identity's abstractions, not EF Core's default Identity store:
|
||||
the app backs them with its own `DapperUserStore` (see
|
||||
[Database](database.md#database-error-handling)), which persists against the
|
||||
existing `UserAccount`/`UserCredential`/`UserVerification` tables via Dapper,
|
||||
consistent with the rest of the app's repository pattern.
|
||||
|
||||
#### [ResendConfirmationEmailHandler](../../web/backend/Features/Features.Users/Commands/Authentication/ResendConfirmationEmail/ResendConfirmationEmailHandler.cs)
|
||||
|
||||
**Flow:**
|
||||
|
||||
Reference in New Issue
Block a user