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
|
# Architecture
|
||||||
|
|
||||||
This document describes the active architecture of The Biergarten App.
|
This document describes the architecture of The Biergarten App.
|
||||||
|
|
||||||
## High-level overview
|
## High-level overview
|
||||||
|
|
||||||
The Biergarten App is a monorepo with a clear split between the backend and the
|
The Biergarten App is a monorepo split between the backend and the website:
|
||||||
active website:
|
|
||||||
|
|
||||||
- **Backend**: .NET 10 Web API with SQL Server, organized as vertical feature
|
- **Backend**: .NET 10 Web API with SQL Server, organized as vertical feature
|
||||||
slices with MediatR
|
slices with MediatR
|
||||||
@@ -13,8 +12,8 @@ active website:
|
|||||||
- **Architecture Style**: Vertical-slice backend plus server-rendered React
|
- **Architecture Style**: Vertical-slice backend plus server-rendered React
|
||||||
frontend
|
frontend
|
||||||
|
|
||||||
The legacy Next.js frontend has been retained in `archive/next-js-web-app/` for
|
`archive/next-js-web-app/` contains an archived Next.js frontend, kept for
|
||||||
reference only.
|
reference.
|
||||||
|
|
||||||
## Diagrams
|
## Diagrams
|
||||||
|
|
||||||
@@ -24,11 +23,25 @@ For visual representations, see:
|
|||||||
architecture diagram
|
architecture diagram
|
||||||
- [deployment.svg](website/diagrams-out/deployment.svg) - Docker deployment
|
- [deployment.svg](website/diagrams-out/deployment.svg) - Docker deployment
|
||||||
diagram
|
diagram
|
||||||
- [authentication-flow.svg](website/diagrams-out/authentication-flow.svg) -
|
|
||||||
Authentication workflow
|
|
||||||
- [database-schema.svg](website/diagrams-out/database-schema.svg) - Database
|
- [database-schema.svg](website/diagrams-out/database-schema.svg) - Database
|
||||||
relationships
|
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
|
## Backend architecture
|
||||||
|
|
||||||
### Vertical-slice architecture pattern
|
### 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
|
`Features.Emails`, `Features.Locations`, `Features.PhotoUpload`) is a single
|
||||||
project that owns its own MediatR commands/queries/handlers, validators, and
|
project that owns its own MediatR commands/queries/handlers, validators, and
|
||||||
repository end to end. `Features.Emails`, `Features.Locations`, 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:
|
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
|
`Features.Emails` has no `Controllers/` folder. It's invoked only via MediatR
|
||||||
commands sent from other slices, never over HTTP. `Features.Locations` also has
|
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
|
`ILocationRepository` directly as a plain service, currently consumed only by
|
||||||
`Database.Seed` to look up cities while seeding brewery locations. Other slices
|
`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
|
(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 to an HTTP route directly. The one exception today is the `/upload-test`
|
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
|
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
|
#### 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, without an ORM's change-tracking or mapping magic
|
it
|
||||||
|
|
||||||
**Strategy**:
|
**Strategy**:
|
||||||
|
|
||||||
- No ORM (Entity Framework not used); each repository issues inline SQL
|
- No ORM; each repository issues inline SQL through Dapper, which handles
|
||||||
through Dapper, with no manual `IDbCommand` parameter binding. The one
|
parameter binding. The one
|
||||||
wrinkle is the `GEOGRAPHY` column on `BreweryPostLocation`, which Dapper
|
wrinkle is the `GEOGRAPHY` column on `BreweryPostLocation`, which Dapper
|
||||||
can't deserialize as a UDT: reads select `CONVERT(varbinary(max),
|
can't deserialize as a UDT: reads select `CONVERT(varbinary(max),
|
||||||
Coordinates)` so the value comes back as a plain `byte[]` Dapper can bind.
|
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
|
avoiding a client-side distance calculation over every row. No spatial
|
||||||
index backs the query yet; `schema.sql` sketches one, commented out, for
|
index backs the query yet; `schema.sql` sketches one, commented out, for
|
||||||
when the table grows large enough to need it
|
when the table grows large enough to need it
|
||||||
- Referential checks that a stored procedure used to perform (e.g. "does this
|
- Referential checks (for example, "does this `CityId` exist?") are explicit
|
||||||
`CityId` exist?") are now explicit `SELECT 1 ...` existence checks in the
|
`SELECT 1 ...` existence checks in the repository method, run inside the
|
||||||
repository method, run inside the same transaction as the write
|
same 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`,
|
||||||
`ConflictException`, ...) directly when a check fails, rather than relying on
|
`ConflictException`, ...) directly when a check fails; `API.Core`'s
|
||||||
a database-side `THROW`; `API.Core`'s `GlobalExceptionFilter` maps those
|
`GlobalExceptionFilter` maps those exception types to HTTP status codes
|
||||||
exception types to HTTP status codes
|
|
||||||
- Application focuses on orchestration; the database enforces integrity via
|
- Application focuses on orchestration; the database enforces integrity via
|
||||||
keys, `CHECK` constraints, and cascades
|
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.
|
error-handling approach in more detail.
|
||||||
|
|
||||||
## Frontend architecture
|
## 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.
|
enabled.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -397,7 +409,7 @@ web/frontend/
|
|||||||
|
|
||||||
### Theme system
|
### 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:
|
themes:
|
||||||
|
|
||||||
- Biergarten Lager
|
- Biergarten Lager
|
||||||
@@ -408,11 +420,10 @@ themes:
|
|||||||
All component styling should prefer semantic tokens such as `primary`,
|
All component styling should prefer semantic tokens such as `primary`,
|
||||||
`success`, `surface`, and `highlight` instead of hard-coded color values.
|
`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/`
|
`archive/next-js-web-app/` contains an archived Next.js frontend, kept for
|
||||||
for reference only. Active product and engineering documentation should point to
|
reference. Product and engineering documentation points to `web/frontend`.
|
||||||
`web/frontend`.
|
|
||||||
|
|
||||||
## Security architecture
|
## Security architecture
|
||||||
|
|
||||||
@@ -431,8 +442,8 @@ for reference only. Active product and engineering documentation should point to
|
|||||||
3. **Login**:
|
3. **Login**:
|
||||||
- User submits credentials
|
- User submits credentials
|
||||||
- Password verified against hash
|
- Password verified against hash
|
||||||
- Access and refresh JWTs are issued; the refresh token is stored server-side
|
- Access and refresh JWTs are issued and returned to the client; neither is
|
||||||
against the user's credential row
|
stored server-side
|
||||||
|
|
||||||
4. **Token refresh**:
|
4. **Token refresh**:
|
||||||
- Client exchanges a valid, unexpired refresh token for a new access/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
|
**Natural language processing is a powerful tool for data analysis and
|
||||||
generation, but its output should be treated with scrutiny. Human language is
|
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.**
|
that artificial intelligence cannot capture.**
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -102,7 +102,7 @@ vendored verbatim, unmodified, from
|
|||||||
(the `common-forenames-by-country.json` / `common-surnames-by-country.json`
|
(the `common-forenames-by-country.json` / `common-surnames-by-country.json`
|
||||||
release assets), released under **CC0** (public domain). That dataset's own
|
release assets), released under **CC0** (public domain). That dataset's own
|
||||||
forename/surname lists are pulled from Wikipedia's "Lists of most common
|
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
|
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
|
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
|
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
|
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
|
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`,
|
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.
|
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
|
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** — it always falls within
|
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.
|
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
|
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
|
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.
|
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
|
> **This pipeline produces AI-generated data.** It is not a source of truth for
|
||||||
> brewing techniques, cultural representation, or local-language accuracy. See
|
> 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.
|
> 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)
|
- [How it fits the main app](#how-it-fits-the-main-app)
|
||||||
- [Quick start](#quick-start)
|
- [Quick start](#quick-start)
|
||||||
@@ -176,7 +176,7 @@ output to confirm the fast path was taken.
|
|||||||
### Run the container
|
### Run the container
|
||||||
|
|
||||||
The container always runs the model-backed path; there is no `--mocked`
|
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
|
The entrypoint, `runpod/start.sh`, downloads the GGUF model automatically if it
|
||||||
is not already present at the configured path.
|
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
|
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
|
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
|
#### Environment variables
|
||||||
|
|
||||||
@@ -211,8 +211,9 @@ configurable via environment variable.
|
|||||||
|
|
||||||
Use a GPU pod template. Mount persistent storage for `/workspace/models`,
|
Use a GPU pod template. Mount persistent storage for `/workspace/models`,
|
||||||
`/workspace/output`, and `/workspace/logs`. See
|
`/workspace/output`, and `/workspace/logs`. See
|
||||||
`tooling/pipeline/runpod/pod-template.yaml` for a starter template — set the
|
`tooling/pipeline/runpod/pod-template.yaml` for a starter template; set the
|
||||||
environment variables listed above to match your run.
|
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
|
### Key components
|
||||||
|
|
||||||
- `src/main.cc` — argument parsing and Boost.DI composition root.
|
- `src/main.cc`: argument parsing and Boost.DI composition root.
|
||||||
- `CuratedJsonDataService` — implements `ICuratedDataService`; takes a
|
- `CuratedJsonDataService`: implements `ICuratedDataService`; takes a
|
||||||
`CuratedDataFilePaths` DTO (locations/personas/forenames/surnames paths) in
|
`CuratedDataFilePaths` DTO (locations/personas/forenames/surnames paths) in
|
||||||
its constructor, then parses and validates curated location, persona, and
|
its constructor, then parses and validates curated location, persona, and
|
||||||
forename/surname JSON, memoizing each result after its first load on a given
|
forename/surname JSON, memoizing each result after its first load on a given
|
||||||
instance. `MockCuratedDataService` is the in-memory substitute (4 fixed
|
instance. `MockCuratedDataService` is the in-memory substitute (4 fixed
|
||||||
locations, 3 personas, and name data for `US`/`DE`/`FR`/`BE`) used in
|
locations, 3 personas, and name data for `US`/`DE`/`FR`/`BE`) used in
|
||||||
`--mocked` runs.
|
`--mocked` runs.
|
||||||
- `WikipediaEnrichmentService` — queries Wikipedia extracts, caches results,
|
- `WikipediaEnrichmentService`: queries Wikipedia extracts, caches results,
|
||||||
returns empty context on failure. `MockEnrichmentService` is the no-op
|
returns empty context on failure. `MockEnrichmentService` is the no-op
|
||||||
substitute used in `--mocked` runs.
|
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
|
`GenerateBrewery` and `GenerateUser`, retries malformed responses up to three
|
||||||
times with corrective feedback in the retry prompt. The token budget is fixed
|
times with corrective feedback in the retry prompt. The token budget is fixed
|
||||||
across attempts; it is not raised automatically on truncation.
|
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.
|
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.
|
successful user and brewery into normalized tables.
|
||||||
- Brewery payloads include English and local-language name and description
|
- Brewery payloads include English and local-language name and description
|
||||||
fields. User payloads carry a sampled first/last name and gender, an
|
fields. User payloads carry a sampled first/last name and gender, an
|
||||||
@@ -281,13 +282,13 @@ runs.
|
|||||||
`CuratedJsonDataService` memoizes each of `LoadLocations()`, `LoadPersonas()`,
|
`CuratedJsonDataService` memoizes each of `LoadLocations()`, `LoadPersonas()`,
|
||||||
`LoadForenamesByCountry()`, and `LoadSurnamesByCountry()` independently the
|
`LoadForenamesByCountry()`, and `LoadSurnamesByCountry()` independently the
|
||||||
first time each is called, since `BiergartenPipelineOrchestrator` owns a single
|
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.
|
result instead of re-parsing.
|
||||||
|
|
||||||
`GenerateUsers()` samples a forename/surname pair per city via `SampleName()`,
|
`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
|
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`)
|
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.
|
see ETHICS-AND-KNOWN-ISSUES.md's Names-by-Country Dataset section.
|
||||||
|
|
||||||
### Process flow - activity diagram
|
### 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
|
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.
|
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
|
> **Code style:** The code follows the Google C++ Style Guide, except that
|
||||||
> features modern C++20 throughout. Formatting follows the Google C++ Style
|
> indentation is 3 spaces instead of Google's 2, and uses C++20 throughout.
|
||||||
> Guide, with the exception of indentation width (3 spaces vs. Google's 2).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Tested hardware
|
## 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 |
|
| Model | Gemma 4 E4B |
|
||||||
| Inference | llama.cpp with Metal |
|
| 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 |
|
| Model | Gemma 4 E4B |
|
||||||
| Inference | llama.cpp with CUDA 12.x |
|
| 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
|
- Keep `locations.json` structured enough to support discovery and future
|
||||||
filtering.
|
filtering.
|
||||||
- `personas.json`, `forenames-by-country.json`, and `surnames-by-country.json`
|
- `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.
|
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,
|
- Treat SQLite output as seed material for the app's brewery and user domains,
|
||||||
not production data.
|
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/`.
|
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 export.
|
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`, and `MockCuratedDataService`, the in-memory
|
||||||
`ICuratedDataService` used in `--mocked` runs.
|
`ICuratedDataService` used in `--mocked` runs.
|
||||||
- `src/services/enrichment/wikipedia/` — enrichment service and cache.
|
- `src/services/enrichment/wikipedia/`: enrichment service and cache.
|
||||||
- `src/services/sqlite/` — SQLite export implementation.
|
- `src/services/sqlite/`: SQLite export implementation.
|
||||||
- `src/data_generation/llama/` — local inference, prompt loading, output
|
- `src/data_generation/llama/`: local inference, prompt loading, output
|
||||||
validation.
|
validation.
|
||||||
- `src/data_generation/mock/` — deterministic fallback.
|
- `src/data_generation/mock/`: deterministic fallback.
|
||||||
- `runpod/` — container build and runtime launcher.
|
- `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
|
engineering breakdown of what's needed to reach the architecture in
|
||||||
[`diagrams/planned/`](./diagrams/planned/), see [ROADMAP.md](./ROADMAP.md).
|
[`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
|
- Unit test JSON validation and retry logic against malformed, truncated, and
|
||||||
empty model outputs.
|
empty model outputs.
|
||||||
@@ -481,7 +481,7 @@ enough to exercise search, sort, and category filters.
|
|||||||
### Check-in system
|
### Check-in system
|
||||||
|
|
||||||
Produce timestamped check-in events between users and breweries. Use a J-curve
|
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.
|
appear occasionally. Add bursty behaviour around weekends and travel periods.
|
||||||
|
|
||||||
### Beer ratings
|
### 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
|
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
|
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
|
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
|
editing an existing one in place: DbUp's tracking is per-script, so an edited
|
||||||
an already-applied script means it won't re-run against existing databases.
|
already-applied script never re-runs against existing databases.
|
||||||
|
|
||||||
## Data seeding
|
## 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,
|
pipeline (see [Pipeline README](../pipeline/README.md)): countries,
|
||||||
state/provinces, and cities (created on demand while resolving each brewery's
|
state/provinces, and cities (created on demand while resolving each brewery's
|
||||||
location), user accounts, and brewery posts with locations. It doesn't call
|
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.
|
repository (`Features.PhotoUpload`) but no seed data.
|
||||||
|
|
||||||
## Database error handling
|
## Database error handling
|
||||||
@@ -78,10 +78,10 @@ repository (`Features.PhotoUpload`) but no seed data.
|
|||||||
- On failure, the repository throws a `Domain.Exceptions` type directly
|
- On failure, the repository throws a `Domain.Exceptions` type directly
|
||||||
(`NotFoundException`, `ConflictException`, `ArgumentException`, ...).
|
(`NotFoundException`, `ConflictException`, `ArgumentException`, ...).
|
||||||
`API.Core`'s `GlobalExceptionFilter` maps these to HTTP status codes (404,
|
`API.Core`'s `GlobalExceptionFilter` maps these to HTTP status codes (404,
|
||||||
409, 400, ...); an unmapped `SqlException` (e.g. a genuine connectivity or
|
409, 400, ...); an unmapped `SqlException` (for example, a genuine
|
||||||
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` directly, to detect a duplicate-key race (SQL Server error
|
`SqlException` directly, to detect a duplicate-key race (SQL Server error
|
||||||
2601 or 2627) when two requests try to verify the same account
|
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
|
```bash
|
||||||
# Run all tests
|
# 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
|
# View results
|
||||||
ls -la test-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
|
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`)
|
### 3. Production (`docker-compose.prod.yaml`)
|
||||||
|
|
||||||
**Purpose**: Production-ready deployment
|
**Purpose**: Production-ready deployment
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# Environment variables
|
# Environment variables
|
||||||
|
|
||||||
This document covers the active environment variables used by the current
|
This document covers the environment variables used by the Biergarten stack.
|
||||||
Biergarten stack.
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -19,10 +18,10 @@ Direct environment variable access via `Environment.GetEnvironmentVariable()`.
|
|||||||
|
|
||||||
### Frontend (`web/frontend`)
|
### Frontend (`web/frontend`)
|
||||||
|
|
||||||
The active website reads runtime values from the server environment for its auth
|
The website reads runtime values from the server environment for its auth and
|
||||||
and API integration. In Docker, the `frontend` service is wired into the same
|
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
|
`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
|
### Docker
|
||||||
|
|
||||||
@@ -104,7 +103,8 @@ WEBSITE_BASE_URL=https://thebiergarten.app # Base URL for the website
|
|||||||
**Security Requirements**:
|
**Security Requirements**:
|
||||||
|
|
||||||
- Use at least 32 characters per secret
|
- 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
|
- Generate secrets with a cryptographically secure random function
|
||||||
- Never reuse secrets across token types or environments
|
- Never reuse secrets across token types or environments
|
||||||
- Rotate secrets periodically in production
|
- 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
|
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`)
|
## Frontend variables (`web/frontend`)
|
||||||
|
|
||||||
The active website does not use the old Next.js/Prisma environment model. Its
|
The website's core runtime variables are:
|
||||||
core runtime variables are:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
API_BASE_URL=http://api.core:8080 # Base URL for the .NET API (container DNS name in Docker)
|
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
|
# 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**:
|
**Docker Compose Mapping**:
|
||||||
|
|
||||||
- `web/docker-compose.dev.yaml` → `web/.env.dev`
|
- `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 |
|
| `PORT` | | ✓ | ✓ | No | Frontend server port |
|
||||||
| `CLEAR_DATABASE` | ✓ | | ✓ | No | Dev/test reset flag |
|
| `CLEAR_DATABASE` | ✓ | | ✓ | No | Dev/test reset flag |
|
||||||
| `ASPNETCORE_ENVIRONMENT` | ✓ | | ✓ | Yes | ASP.NET environment |
|
| `ASPNETCORE_ENVIRONMENT` | ✓ | | ✓ | Yes | ASP.NET environment |
|
||||||
| `ASPNETCORE_URLS` | ✓ | | ✓ | Yes | API binding address |
|
| `ASPNETCORE_URLS` | ✓ | | ✓ | Yes\*\* | API binding address |
|
||||||
| `SA_PASSWORD` | | | ✓ | Yes | SQL Server container |
|
| `SA_PASSWORD` | | | ✓ | Yes | SQL Server container |
|
||||||
| `ACCEPT_EULA` | | | ✓ | Yes | SQL Server EULA |
|
| `ACCEPT_EULA` | | | ✓ | Yes | SQL Server EULA |
|
||||||
| `MSSQL_PID` | | | ✓ | No | SQL Server edition |
|
| `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`,
|
\* Either `DB_CONNECTION_STRING` OR the component variables (`DB_SERVER`,
|
||||||
`DB_NAME`, `DB_USER`, `DB_PASSWORD`) must be provided.
|
`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
|
## Validation
|
||||||
|
|
||||||
### Backend validation
|
### Backend validation
|
||||||
@@ -359,8 +364,8 @@ Variables are validated at startup:
|
|||||||
|
|
||||||
### Frontend validation
|
### Frontend validation
|
||||||
|
|
||||||
The active website relies on runtime defaults for local development and the
|
The website relies on runtime defaults for local development and on the server
|
||||||
surrounding server environment in deployed environments.
|
environment when deployed.
|
||||||
|
|
||||||
- `API_BASE_URL` defaults to `http://localhost:8080`
|
- `API_BASE_URL` defaults to `http://localhost:8080`
|
||||||
- `SESSION_SECRET` falls back to a development-only local secret
|
- `SESSION_SECRET` falls back to a development-only local secret
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Getting started
|
# Getting started
|
||||||
|
|
||||||
This guide covers local setup for the current Biergarten stack: the .NET backend
|
This guide covers local setup for the Biergarten stack: the .NET backend in
|
||||||
in `web/backend` and the active React Router frontend in `web/frontend`.
|
`web/backend` and the React Router frontend in `web/frontend`.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ Available endpoints:
|
|||||||
- Mailpit UI: http://localhost:8025
|
- Mailpit UI: http://localhost:8025
|
||||||
- SeaweedFS S3 API: http://localhost:8333
|
- SeaweedFS S3 API: http://localhost:8333
|
||||||
|
|
||||||
### 4. Start the active frontend
|
### 4. Start the frontend
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd web/frontend
|
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
|
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
|
`archive/next-js-web-app/` contains an archived Next.js frontend, kept for
|
||||||
only and is not the active website.
|
reference; it is not part of the running stack.
|
||||||
|
|
||||||
## Next steps
|
## Next steps
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ The project uses a multi-layered testing approach across backend and frontend:
|
|||||||
|
|
||||||
## Running tests with Docker (recommended)
|
## Running tests with Docker (recommended)
|
||||||
|
|
||||||
The easiest way to run all tests is using Docker Compose, which sets up an
|
To run all tests, use Docker Compose, which sets up an isolated test
|
||||||
isolated test environment:
|
environment:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
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 up -d
|
||||||
@@ -43,7 +43,7 @@ This command:
|
|||||||
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 down separately (see below) — `wait` doesn't touch them.
|
torn down separately (see below); `wait` doesn't touch them.
|
||||||
|
|
||||||
### View test results
|
### 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.
|
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.
|
`./test-results/` is uploaded as a build artifact regardless of outcome.
|
||||||
|
|
||||||
Earlier versions of this workflow used
|
`up -d` + `wait` keeps container teardown independent of any single
|
||||||
`up --abort-on-container-exit`, which stops the entire stack the instant
|
container's exit. The one-shot `database.migrations`/`database.seed` jobs
|
||||||
*any* watched container exits. That's wrong here: the one-shot
|
exit 0 partway through the run, and `frontend.tests` (which has no database
|
||||||
`database.migrations`/`database.seed` jobs are expected to exit 0 partway
|
dependency) reliably finishes before `api.specs`/`unit.tests` start; because
|
||||||
through, and even scoping the flag to just the three test services doesn't
|
`wait` blocks on all three test containers, neither early exit ends the run
|
||||||
help, since `frontend.tests` (no database dependency) reliably finishes
|
prematurely.
|
||||||
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.
|
|
||||||
|
|
||||||
Exit codes:
|
Exit codes:
|
||||||
|
|
||||||
@@ -414,13 +411,13 @@ act push \
|
|||||||
`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 that platform for the whole job container, not just that one
|
emulate 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 `Unable to get the
|
||||||
ACTIONS_RUNTIME_TOKEN env variable`. act doesn't provide a real Actions
|
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
|
under act even when everything else passes. It works normally on
|
||||||
GitHub-hosted runners.
|
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
|
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
|
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
|
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.
|
'sa'` errors that look like a test bug but are really a leftover volume.
|
||||||
Clear it before re-running:
|
Clear it before re-running:
|
||||||
|
|
||||||
|
|||||||
@@ -34,8 +34,7 @@ Low-level JWT operations.
|
|||||||
|
|
||||||
#### [ITokenService](../../web/backend/Features/Features.Users/Services/ITokenService.cs)
|
#### [ITokenService](../../web/backend/Features/Features.Users/Services/ITokenService.cs)
|
||||||
|
|
||||||
Both token generation and validation live on the same slice-internal service
|
Token generation and validation live on the same slice-internal service.
|
||||||
(there is no separate validation service/class).
|
|
||||||
|
|
||||||
**Generation methods:**
|
**Generation methods:**
|
||||||
|
|
||||||
@@ -79,9 +78,17 @@ 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 `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
|
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)
|
#### [ResendConfirmationEmailHandler](../../web/backend/Features/Features.Users/Commands/Authentication/ResendConfirmationEmail/ResendConfirmationEmailHandler.cs)
|
||||||
|
|
||||||
**Flow:**
|
**Flow:**
|
||||||
|
|||||||
Reference in New Issue
Block a user