refinement phase 0
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
# Run the whole app locally — Balinyaar bring-up runbook
|
||||
|
||||
The one copy-pasteable procedure for standing up **both** projects on one machine and watching a real,
|
||||
authenticated request cross the wire. Established by **Refinement Phase 0**
|
||||
([refinement-phase-0-bring-up.md](refinement-phase-0-bring-up.md)).
|
||||
|
||||
After this you have: the API on `https://localhost:5002` (against a local SQL Server), the web client on
|
||||
`http://localhost:3000`, and a working phone-OTP login backed by the real backend. **Auth is the only real
|
||||
domain** until [Refinement Phase 4](refinement-phase-4-frontend-de-mock.md) — everything else in the UI is
|
||||
still an in-browser mock.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **.NET 10 SDK** (`dotnet --version`) — preview is fine.
|
||||
- **Node.js 20+** and **npm** (`node --version`).
|
||||
- **Docker** (Desktop or engine) for the local database — or your own SQL Server on `localhost:1433`.
|
||||
|
||||
---
|
||||
|
||||
## One-time setup
|
||||
|
||||
### 1. Trust the ASP.NET Core HTTPS dev certificate
|
||||
|
||||
Without this the browser (and `fetch`) rejects `https://localhost:5002` and every call fails with an opaque
|
||||
network error.
|
||||
|
||||
```bash
|
||||
dotnet dev-certs https --trust
|
||||
```
|
||||
|
||||
Accept the OS prompt. (macOS/Windows trust the cert; on Linux see the .NET docs for the per-distro step.)
|
||||
|
||||
### 2. Start a local SQL Server
|
||||
|
||||
From `server/`:
|
||||
|
||||
```bash
|
||||
cd server
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
This runs SQL Server 2022 (Developer edition) on `localhost:1433` with a **dev-only** SA password
|
||||
(`Balinyaar_Dev1433`, defined in `server/docker-compose.yml` — not a secret, never used in production).
|
||||
Give it ~20–30s on first start (`docker compose ps` shows `healthy`).
|
||||
|
||||
> Already have a SQL Server? Skip this and point the connection string in step 3 at it instead.
|
||||
|
||||
### 3. Point the API at the local database (via user-secrets — never a committed file)
|
||||
|
||||
The committed `appsettings*.json` carry a **placeholder** connection string on purpose. Supply the real
|
||||
local one through `dotnet user-secrets` so no working credential ever lands in git. From the API project:
|
||||
|
||||
```bash
|
||||
cd server/src/API/Baya.Web.Api
|
||||
dotnet user-secrets set "ConnectionStrings:SqlServer" "Server=localhost,1433;Database=Baya;User Id=sa;Password=Balinyaar_Dev1433;TrustServerCertificate=True;Encrypt=False;"
|
||||
```
|
||||
|
||||
The `Password` must match `MSSQL_SA_PASSWORD` in `docker-compose.yml`. User-secrets auto-load only in the
|
||||
Development environment, so this never affects a deployed build.
|
||||
|
||||
> **Env-var alternative** (e.g. for CI/containers): set `ConnectionStrings__SqlServer` (double underscore =
|
||||
> the `:` config separator) instead of using user-secrets.
|
||||
> PowerShell: `$env:ConnectionStrings__SqlServer = "Server=localhost,1433;Database=Baya;User Id=sa;Password=Balinyaar_Dev1433;TrustServerCertificate=True;Encrypt=False;"`
|
||||
> bash: `export ConnectionStrings__SqlServer="Server=localhost,1433;Database=Baya;User Id=sa;Password=Balinyaar_Dev1433;TrustServerCertificate=True;Encrypt=False;"`
|
||||
|
||||
---
|
||||
|
||||
## Run it (two terminals)
|
||||
|
||||
### Terminal 1 — backend (from `server/`)
|
||||
|
||||
```bash
|
||||
dotnet run --project src/API/Baya.Web.Api/Baya.Web.Api.csproj
|
||||
```
|
||||
|
||||
On boot the API applies all EF migrations and seeds roles + an admin user + a sandbox payment gateway
|
||||
against the (empty) local DB, then listens on **`https://localhost:5002`** — Swagger at
|
||||
`https://localhost:5002/swagger`.
|
||||
|
||||
### Terminal 2 — frontend (from `client/`)
|
||||
|
||||
```bash
|
||||
cd client
|
||||
npm install # first time only
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Serves **`http://localhost:3000`**. It reads the API base URL from `client/.env.development`
|
||||
(`NEXT_PUBLIC_API_URL = https://localhost:5002`). If you run the API on a different port/scheme, create
|
||||
`client/.env.local` with your `NEXT_PUBLIC_API_URL=…` (it overrides `.env.development`, is git-ignored).
|
||||
|
||||
---
|
||||
|
||||
## Log in (the real round-trip)
|
||||
|
||||
1. Open **`http://localhost:3000/fa/login`**.
|
||||
2. Enter an Iranian mobile number (e.g. `09120000001`) and request the code.
|
||||
3. Get the 6-digit OTP one of two ways:
|
||||
- **Read the server console** (Terminal 1) — SMS is mocked, so the code is logged:
|
||||
`MOCK SMS — OTP code 123456 for phone ending in 0001`.
|
||||
- **Or hit the Development-only helper** (handy for scripts/e2e):
|
||||
`GET https://localhost:5002/api/v1/dev/last_otp/09120000001` →
|
||||
`{ "data": { "phone": "09120000001", "code": "123456" }, ... }`.
|
||||
This endpoint returns **404 outside Development** and is superseded by real SMS in
|
||||
[Refinement Phase 8](refinement-phase-8-external-rails.md).
|
||||
4. Enter the code and submit → you land on the customer home.
|
||||
5. **Verify in DevTools → Network:** `POST /api/v1/auth/request_otp`, `POST /api/v1/auth/verify_otp`, and
|
||||
`GET /api/v1/me` all return **200** with the `ApiResult` envelope, and there is **no CORS error** in the
|
||||
console. That is the first real authenticated request between the two projects.
|
||||
|
||||
---
|
||||
|
||||
## Good to know
|
||||
|
||||
- **The API speaks HTTP/2** (Kestrel `Protocols: Http2`, for gRPC). Browsers negotiate h2-over-TLS
|
||||
automatically, so `fetch` just works; for `curl` add `--http2`.
|
||||
- **Only `auth` is real by default.** 21 of 22 client service domains default to an in-browser mock
|
||||
(`USE_*_MOCK = true`); the home, search, bookings, etc. are fake in-memory data until Refinement Phase 4.
|
||||
- **The DB self-migrates + self-seeds**, so pointing at an empty local instance is enough. Rich demo data
|
||||
(nurses, variants, search rows) arrives in [Refinement Phase 1](refinement-phase-1-database-and-seed.md).
|
||||
- **Allowed browser origins** are configuration-driven (`Cors:AllowedOrigins`), defaulting to
|
||||
`http://localhost:3000` in Development. A deployed environment lists its real web origin(s).
|
||||
|
||||
## Stopping / resetting
|
||||
|
||||
```bash
|
||||
docker compose down # stop the DB, keep its data
|
||||
docker compose down -v # stop the DB and wipe the volume (fresh migrate + seed next run)
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Fix |
|
||||
| --- | --- |
|
||||
| Browser: `net::ERR_CERT_AUTHORITY_INVALID` on `:5002` | Run `dotnet dev-certs https --trust` (setup step 1). |
|
||||
| API startup: `Login failed for user 'sa'` / connect timeout | DB not up or wrong password — check `docker compose ps` and that the user-secrets password matches `docker-compose.yml`. |
|
||||
| Console: `...has been blocked by CORS policy` | `UseCors` missing/mis-ordered, or the browser origin isn't in `Cors:AllowedOrigins`. It must sit after `UseRouting` and before the rate limiter. |
|
||||
| `dotnet user-secrets` errors with "could not find UserSecretsId" | Run it from `server/src/API/Baya.Web.Api` (the project with `<UserSecretsId>`). |
|
||||
@@ -12,6 +12,27 @@ One block per completed backend phase. Newest at the top. Backend lane writes he
|
||||
- **Notes for frontend:** <anything load-bearing>
|
||||
-->
|
||||
|
||||
## refinement-phase-0 — Local end-to-end bring-up & the integration seam — 2026-07-12
|
||||
- **Shipped (integration/plumbing — no business logic):** **CORS** (`Baya.WebFramework/ServiceConfiguration/
|
||||
CorsServiceExtension.cs` → `AddCorsPolicies`, policy `BalinyaarWebClient` from `Cors:AllowedOrigins`, default
|
||||
`http://localhost:3000`; `app.UseCors` after `UseRouting` / before `UseRateLimiter`; no `AllowCredentials`).
|
||||
**Local DB story** — `server/docker-compose.yml` rewritten to SQL Server 2022 Developer on `localhost:1433`
|
||||
(dev-only SA password); committed `appsettings*.json` connection strings replaced with **non-working
|
||||
placeholders** (real value via `dotnet user-secrets`); added `<UserSecretsId>` to the API csproj.
|
||||
**Development-only OTP helper** `GET /api/v1/dev/last_otp/{phone}` (`DevController` + `DevOtpStore` +
|
||||
`DevCapturingSmsSender`, wired only in Development via `AddDevelopmentOtpCapture`; **404 outside
|
||||
Development**). Wrote `dev/post-phase/refinement/RUNBOOK.md`.
|
||||
- **Contracts:** none produced; swagger snapshot **not** regenerated (only new path is the dev-only helper).
|
||||
- **Mocked:** no new seam; `ISmsSender` (`LoggingSmsSender`) row updated in mocks-registry (interim OTP channel
|
||||
+ the Development-only capture affordance).
|
||||
- **Gate:** build clean (0 new warnings) / tests green (**369**: 4 identity + 248 foundation + 117 API, incl.
|
||||
3 new CORS/dev-otp integration tests + 8 dev-otp store/decorator unit tests).
|
||||
- **Handoff:** backend/handoff/after-refinement-phase-0.md
|
||||
- **Notes for frontend:** the client now really reaches the API cross-origin (CORS unblocked). **No mock flag
|
||||
was flipped — `auth` is still the only real domain** (that's Phase 4). `client/.env.development` already
|
||||
points at `https://localhost:5002`; the API speaks HTTP/2 (browsers negotiate h2-over-TLS automatically).
|
||||
Read the OTP from the server console or `GET /api/v1/dev/last_otp/{phone}` (Development only).
|
||||
|
||||
## backend-phase-15 — Messaging (tickets), partner centers & admin backoffice — 2026-07-10
|
||||
- **Shipped (FINAL backend phase):** new `messaging` schema — `Tickets` (`UNIQUE(reference_code)`, status/
|
||||
category, nullable `booking_id`/`refund_id`), `TicketParticipants` (`UNIQUE(ticket_id, user_id)`, soft-remove
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Handoff — after refinement-phase-0 (Local end-to-end bring-up)
|
||||
|
||||
**Date:** 2026-07-12 · **Track:** integration (both projects) · **Unlocks:** every other refinement phase.
|
||||
|
||||
## What the frontend can now do
|
||||
- **Actually call the backend cross-origin.** The API now has a CORS policy (`BalinyaarWebClient`) allowing
|
||||
`http://localhost:3000` (config `Cors:AllowedOrigins`, default in Dev) with the four headers the client
|
||||
sends (`Authorization`, `Content-Type`, `Accept-Language`, `Idempotency-Key`). A browser at `:3000` calling
|
||||
`https://localhost:5002` is no longer blocked by same-origin policy.
|
||||
- **Stand the stack up in ~5 minutes** via `dev/post-phase/refinement/RUNBOOK.md` (dev-cert trust → local SQL
|
||||
Server via `docker compose` → connection string via `dotnet user-secrets` → `dotnet run` + `npm run dev`).
|
||||
- **Complete a real login end-to-end.** Request an OTP, read the 6-digit code from the **server console**
|
||||
(`MOCK SMS — OTP code … for phone ending in …`) or from the Development-only helper
|
||||
`GET /api/v1/dev/last_otp/{phone}`, verify → real tokens + `/me`.
|
||||
|
||||
## What did NOT change (important)
|
||||
- **No `USE_*_MOCK` flag was flipped.** `auth` is still the only real client domain; the home/search/bookings
|
||||
are still in-browser mocks until [Refinement Phase 4](../../../post-phase/refinement/refinement-phase-4-frontend-de-mock.md).
|
||||
- **No client app code changed** — only verified `client/.env.development` (already `https://localhost:5002`)
|
||||
and added the runbook. If you run the API elsewhere, override with `client/.env.local`.
|
||||
- **Auth crypto, the money path, and the `ApiResult` envelope are untouched.**
|
||||
|
||||
## New endpoint (Development only — not a contract)
|
||||
- `GET /api/v1/dev/last_otp/{phone}` → `{ data: { phone, code } }` when a code was issued, else 404. **404 in
|
||||
every non-Development environment** (the capture isn't even wired there). For manual/e2e login only;
|
||||
superseded by real SMS in [Refinement Phase 8](../../../post-phase/refinement/refinement-phase-8-external-rails.md).
|
||||
Do not build client features on it.
|
||||
|
||||
## Gotchas
|
||||
- The API binds **HTTP/2** (`Kestrel:Protocols = Http2`, for gRPC). Browsers negotiate h2-over-TLS via ALPN
|
||||
automatically, so `fetch` works; `curl` needs `--http2`.
|
||||
- The dev HTTPS cert **must be trusted** (`dotnet dev-certs https --trust`) or `fetch` to `:5002` fails with an
|
||||
opaque network error.
|
||||
- Committed `appsettings*.json` connection strings are **placeholders** — the API will not boot until you set
|
||||
`ConnectionStrings:SqlServer` via user-secrets (or the `ConnectionStrings__SqlServer` env var).
|
||||
@@ -9,7 +9,7 @@ Status legend: 🔴 not built · 🟡 mocked (seam + fake impl in place) · 🟢
|
||||
|
||||
| Seam (interface) | Introduced in | What it fakes | Config keys | Make it real → | Status |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `ISmsSender` | backend-phase-2 | OTP/SMS delivery — `LoggingSmsSender` (`Baya.Infrastructure.CrossCutting/Seams/`) logs the OTP code (phone shown as last-4 only) and returns success; registered singleton in `AddCrossCuttingSeams` | none today; real client will need `Seams:Sms:ApiKey` + `Seams:Sms:SenderLine` (+ gateway base URL) | 1) pick a gateway (Kavenegar/Ghasedak/SMS.ir), add its client package to `Directory.Packages.props`; 2) implement `ISmsSender.SendOtpAsync`/`SendAsync` against it (template/pattern-based OTP send); 3) bind the new `Seams:Sms` options; 4) swap the registration in `AddCrossCuttingSeams` (config-selected) — handlers unchanged; 5) keep the per-phone resend window + `otp` rate-limit policy exactly as-is; test with a real SIM | 🟡 |
|
||||
| `ISmsSender` | backend-phase-2 | OTP/SMS delivery — `LoggingSmsSender` (`Baya.Infrastructure.CrossCutting/Seams/`) logs the OTP code (phone shown as last-4 only) and returns success; registered singleton in `AddCrossCuttingSeams`. **refinement-phase-0:** in **Development only**, `DevCapturingSmsSender` decorates it (via `AddDevelopmentOtpCapture`, called from `Program.cs` inside `IsDevelopment()`) to also capture the code in `DevOtpStore` for the `GET /api/v1/dev/last_otp/{phone}` bring-up helper — not wired / 404 outside Development | none today; real client will need `Seams:Sms:ApiKey` + `Seams:Sms:SenderLine` (+ gateway base URL) | 1) pick a gateway (Kavenegar/Ghasedak/SMS.ir), add its client package to `Directory.Packages.props`; 2) implement `ISmsSender.SendOtpAsync`/`SendAsync` against it (template/pattern-based OTP send); 3) bind the new `Seams:Sms` options; 4) swap the registration in `AddCrossCuttingSeams` (config-selected) — handlers unchanged; 5) keep the per-phone resend window + `otp` rate-limit policy exactly as-is; test with a real SIM | 🟡 |
|
||||
| `IObjectStorage` | backend-phase-0/6 | File storage — local-disk store under a scratch root (`LocalDiskObjectStorage`, `Baya.Infrastructure.CrossCutting/Seams/`) | `Seams:ObjectStorage:RootPath` (default: temp dir) | Point at MinIO/S3/ArvanCloud; presigned upload/download; bucket + creds | 🟡 |
|
||||
| `ICacheService` | backend-phase-0 | Caching — in-memory `IMemoryCache` (`MemoryCacheService`, `Baya.Infrastructure.CrossCutting/Seams/`) | _none_ | Swap to Redis (`StackExchange.Redis`); keep key/TTL scheme | 🟡 |
|
||||
| `IDistributedLock` | backend-phase-10 | Money-path locks — no-op/in-proc | _tbd_ | Redis lock (RedLock); DB constraint remains the backstop | 🔴 |
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# Refinement Phase 0 — Local end-to-end bring-up & the integration seam — Report (2026-07-12)
|
||||
|
||||
## What was built
|
||||
Removed the three hard integration blockers so the client and server actually talk on one machine, and
|
||||
proved it with one real authenticated round-trip. **Plumbing only — no business logic, money path, auth
|
||||
crypto, or envelope shape changed.**
|
||||
|
||||
- **CORS (§3.1).** New `Baya.WebFramework/ServiceConfiguration/CorsServiceExtension.cs` —
|
||||
`AddCorsPolicies(configuration)` builds the named policy `BalinyaarWebClient` from
|
||||
`Cors:AllowedOrigins` (string array), defaulting to `http://localhost:3000` when unset. Allows exactly the
|
||||
four headers the client sends (`Authorization`, `Content-Type`, `Accept-Language`, `Idempotency-Key`) +
|
||||
`AllowAnyMethod()`; **no `AllowCredentials()`** (the client uses a bearer header, not a cookie). Registered
|
||||
in the service chain and `app.UseCors(CorsServiceExtension.PolicyName)` placed **after `UseRouting()` and
|
||||
before `UseRateLimiter()`** so pre-flight `OPTIONS` is answered before the limiter/auth run.
|
||||
- **Local database story (§3.2).** `server/docker-compose.yml` rewritten to a single-purpose SQL Server 2022
|
||||
(Developer edition) on `localhost:1433` with a **dev-only** `MSSQL_SA_PASSWORD` + a named volume +
|
||||
healthcheck. (The old compose referenced an unbuildable `bobby-baya` app image and exposed `1435`.) The
|
||||
committed `appsettings.json` / `appsettings.Development.json` connection strings (`SqlServer` + `logDb`) are
|
||||
now **non-working placeholders** (`Password=SET_VIA_USER_SECRETS_OR_ENV`); the real local value comes from
|
||||
`dotnet user-secrets`. Added `<UserSecretsId>baya-web-api</UserSecretsId>` to the API csproj (it was
|
||||
missing — user-secrets weren't actually wired before).
|
||||
- **Development-only OTP retrieval (§3.3).** `GET /api/v1/dev/last_otp/{phone}` (`DevController`) returns the
|
||||
most recent OTP for a phone so a browser / e2e flow can log in without an SMS gateway. Backed by a new
|
||||
Development-only `DevOtpStore` + `DevCapturingSmsSender` (an `ISmsSender` decorator that captures the code
|
||||
then delegates to the log-only `LoggingSmsSender`), wired **only** in Development by
|
||||
`AddDevelopmentOtpCapture()`. The endpoint **404s in every non-Development environment** and the capture is
|
||||
not even registered there — two independent guarantees it can't leak a code. It does **not** weaken the
|
||||
`otp` rate-limit or the per-phone resend window.
|
||||
- **Client env & runbook (§3.4).** Verified `client/.env.development` already has
|
||||
`NEXT_PUBLIC_API_URL = https://localhost:5002` (matches the API's bound URL) — **no client code changed, no
|
||||
mock flag flipped.** Wrote `dev/post-phase/refinement/RUNBOOK.md` — the copy-pasteable "run the whole app
|
||||
locally" procedure (dev-cert trust, compose, user-secrets, both run commands, OTP-from-logs / dev-endpoint,
|
||||
DevTools verification, troubleshooting).
|
||||
|
||||
## What is now testable (and exactly how)
|
||||
- **Automated (in the suite, +11 tests → 369 total):** 3 new `Baya.Test.Api` integration tests
|
||||
(`CorsAndDevBringUpTests`): a pre-flight `OPTIONS /api/v1/auth/request_otp` from `http://localhost:3000`
|
||||
reflects `Access-Control-Allow-Origin: http://localhost:3000`; the same from a foreign origin gets **no**
|
||||
allow-origin header; `GET /api/v1/dev/last_otp/...` returns **404** in the (non-Development) test host. Plus
|
||||
8 `Baya.Test.Foundation` unit tests (`DevOtpBringUpTests`) covering the store's capture/latest/
|
||||
spelling-insensitive lookup + the decorator's capture-and-still-delegate behaviour.
|
||||
- **Manual (the §7 proof):** follow `RUNBOOK.md` — `docker compose up -d`, set the user-secret, `dotnet run`,
|
||||
`npm run dev`, open `/fa/login`, request an OTP, read the code from the server console (or
|
||||
`GET /api/v1/dev/last_otp/{phone}`), submit → land on the customer home with **200s** on
|
||||
`/auth/request_otp`, `/auth/verify_otp`, `/api/v1/me` and **no CORS error**. Negative check: remove
|
||||
`app.UseCors(...)` → the same flow fails with a CORS error.
|
||||
|
||||
## What is mocked / waiting on a real service
|
||||
- No new seam. The existing **`ISmsSender`** (`LoggingSmsSender`) stays the interim OTP channel (logs the
|
||||
code); its `mocks-registry.md` row is updated to note the Development-only `DevCapturingSmsSender` +
|
||||
`/dev/last_otp` affordance. Real SMS is [Refinement Phase 8](refinement-phase-8-external-rails.md).
|
||||
- The `DevOtpStore` / `DevCapturingSmsSender` / `/dev/last_otp` endpoint are a **Development-only dev
|
||||
affordance, not a seam** (per the phase §4) — superseded by real SMS in Phase 8.
|
||||
|
||||
## Contracts
|
||||
- **None produced.** This phase ships plumbing (CORS middleware) + a Development-only diagnostic endpoint the
|
||||
frontend does not consume as a contract, so no `dev/contracts/domains/*.md` was written and the
|
||||
`swagger.v1.json` snapshot was **not** regenerated (the only new path is the dev-only helper Phase 8
|
||||
removes — regenerating would add churn for a path no client binds to). Auth remains the one real domain
|
||||
the frontend consumes, unchanged.
|
||||
|
||||
## Docs updated
|
||||
- `server/CLAUDE.md` "Startup wiring" — added `AddCorsPolicies(config)` to the registration list, the
|
||||
Development-only `AddDevelopmentOtpCapture()` note, and `CORS` in the pipeline order (after routing, before
|
||||
the rate limiter). Project map — noted the Development-only `Dev` controller.
|
||||
- `dev/post-phase/refinement/RUNBOOK.md` — new local-run runbook.
|
||||
- `dev/shared-working-context/reports/mocks-registry.md` — `ISmsSender` row updated.
|
||||
|
||||
## Follow-ups for later phases
|
||||
- **Phase 1** — local-dev demo seed (nurses/variants/search rows) so discovery/booking aren't empty on the
|
||||
real path.
|
||||
- **Phase 4** — flip the 21 `USE_*_MOCK` flags (this phase changed none).
|
||||
- **Phase 5** — rotate the leaked remote `sa` credentials + the dev-grade `IdentitySettings` keys (still
|
||||
committed as dev placeholders here); set real production `Cors:AllowedOrigins`.
|
||||
- **Phase 8** — real SMS gateway; removes the `/dev/last_otp` helper + the `DevCapturingSmsSender` decorator.
|
||||
- Note for deployed envs: `Cors:AllowedOrigins` must list the real web origin(s); an empty array falls back
|
||||
to the localhost dev origin (safe — a real user's origin won't match, so cross-origin is effectively denied
|
||||
until configured).
|
||||
Reference in New Issue
Block a user