diff --git a/docs/architecture.md b/docs/architecture.md index 6a905811..c1200b45 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 diff --git a/docs/pipeline/ETHICS-AND-KNOWN-ISSUES.md b/docs/pipeline/ETHICS-AND-KNOWN-ISSUES.md index 08d9ca9b..b7096e7b 100644 --- a/docs/pipeline/ETHICS-AND-KNOWN-ISSUES.md +++ b/docs/pipeline/ETHICS-AND-KNOWN-ISSUES.md @@ -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. diff --git a/docs/pipeline/README.md b/docs/pipeline/README.md index 7916007e..626bc46f 100644 --- a/docs/pipeline/README.md +++ b/docs/pipeline/README.md @@ -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 diff --git a/docs/website/database.md b/docs/website/database.md index e98dcf10..b763d868 100644 --- a/docs/website/database.md +++ b/docs/website/database.md @@ -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`. diff --git a/docs/website/docker.md b/docs/website/docker.md index 20b882cc..fbfaf271 100644 --- a/docs/website/docker.md +++ b/docs/website/docker.md @@ -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 diff --git a/docs/website/environment-variables.md b/docs/website/environment-variables.md index 8b1534b1..7c5d74c2 100644 --- a/docs/website/environment-variables.md +++ b/docs/website/environment-variables.md @@ -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 diff --git a/docs/website/getting-started.md b/docs/website/getting-started.md index e0492922..ce01eb1b 100644 --- a/docs/website/getting-started.md +++ b/docs/website/getting-started.md @@ -1,7 +1,7 @@ # Getting started -This guide covers local setup for the current Biergarten stack: the .NET backend -in `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 diff --git a/docs/website/testing.md b/docs/website/testing.md index 9c9167c1..9f936c48 100644 --- a/docs/website/testing.md +++ b/docs/website/testing.md @@ -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: diff --git a/docs/website/token-validation.md b/docs/website/token-validation.md index 7cd3e769..390fba7a 100644 --- a/docs/website/token-validation.md +++ b/docs/website/token-validation.md @@ -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` -4. Marks the account confirmed via `IUserEmailStore.SetEmailConfirmedAsync()` +4. Marks the account confirmed via `IUserEmailStore.SetEmailConfirmedAsync()`, + then persists it via `UserManager.UpdateAsync()` 5. Returns confirmation result +`UserManager` and `IUserEmailStore` 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:**