add refinement phases

This commit is contained in:
hamid
2026-07-10 20:59:47 +03:30
parent a87b47bedb
commit 850cdf3414
11 changed files with 1428 additions and 0 deletions
@@ -0,0 +1,171 @@
# Refinement Phase 0 — Local end-to-end bring-up & the integration seam
> **Mission:** make the client and the server actually talk to each other, once, on one machine, in a
> repeatable way — and prove it with one real authenticated round-trip. Today the two projects each run,
> but nothing wires them together: the browser can't call the API (no CORS), the API only starts against a
> remote database, and the frontend calls almost nothing real. This phase removes the three hard
> integration blockers and ends with a logged-in session backed by the real backend.
>
> **Track:** integration (both projects) · **Depends on:** nothing · **Unlocks:** every other refinement phase
> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).**
## 1. Context — where this sits
The 16-phase backend chain (b0b15) and the 16-phase frontend chain (f0f15) are both **complete against
their own specs**. But they were built in **parallel, decoupled** lanes that only ever met through the
`dev/contracts/` documents — they were **never run against each other**. As a result the app "runs" but is
not integrated:
- The frontend is **mock-primary**: 21 of its 22 service domains default to an in-browser mock
(`USE_*_MOCK = true`), so a fresh `npm run dev` calls the backend for **auth only**. Everything you see —
the customer home, search results, bookings — is fake in-memory data.
- The backend has **no CORS**, so even the calls the frontend *does* make (auth) would be blocked by the
browser the moment the two run on different origins.
- The backend only starts against a **committed remote SQL Server** (see the security phase) — there is no
local-database story, so a developer can't stand it up cleanly.
This phase is the foundation: it makes "run both and watch a real request succeed" a 5-minute, documented
procedure. It does **not** flip the frontend mocks (that's [Refinement Phase 4](refinement-phase-4-frontend-de-mock.md))
or build any missing endpoints (that's [Refinement Phase 3](refinement-phase-3-contract-batch.md)) — it
only removes the plumbing blockers and proves the seam works with the one domain that's already real: auth.
**What already exists (do not rebuild):**
- Real, wired phone-OTP auth on both sides — client `services/auth` (`USE_AUTH_MOCK = false`) ↔ server
`AuthController`/`MeController` (`/api/v1/auth/*`, `/api/v1/me`).
- The client fetch layer (`client/src/lib/api/client.ts`) already reads `NEXT_PUBLIC_API_URL`, attaches the
bearer, unwraps the `ApiResult` envelope, and does silent 401 refresh. It is correct — it just has nothing
to talk to.
- On boot the server applies all 17 EF migrations and seeds roles + an admin user + a sandbox gateway
(`Program.cs:99-104`). Migrations are **current**, not behind.
## 2. Required reading (do this first)
- **This directory's [README.md](README.md)** — the full problem inventory and how the 10 refinement phases
fit together. Read it before anything.
- `server/src/API/Baya.Web.Api/Program.cs` — the startup pipeline (note: **no `UseCors`**).
- `server/src/API/Baya.Web.Api/appsettings.json` / `appsettings.Development.json` — the connection strings
(identical; both point at the remote `87.107.152.16`).
- `server/src/API/Baya.Web.Api/Properties/launchSettings.json` — the API binds `https://localhost:5002` only.
- `client/src/lib/api/client.ts`, `client/src/config.ts`, `client/.env.development` — the client's API base
URL wiring (`NEXT_PUBLIC_API_URL = https://localhost:5002`).
- `server/src/Infrastructure/Baya.Infrastructure.CrossCutting/Seams/LoggingSmsSender.cs` — OTP delivery is a
**log statement**; the code you need to log in is printed to the server console, not sent by SMS.
- Root [CLAUDE.md](../../../CLAUDE.md) working agreement #6 (never commit secrets) — the local-DB change here
must not add a new committed secret.
## 3. Scope — build this
### 3.1 CORS on the server (the #1 blocker)
There is **no CORS anywhere** in `server/` (verified: zero matches for `AddCors`/`UseCors`). A browser at
`http://localhost:3000` calling `https://localhost:5002` is blocked by the same-origin policy before the
request even reaches a controller.
- Add a CORS policy via an extension method (per the server's "expose it as an extension method, call it from
`Program.cs`" rule — never inline). Suggested home:
`Baya.WebFramework/ServiceConfiguration/CorsServiceExtension.cs` with `AddCorsPolicies(configuration)`.
- Read the allowed origins from configuration (`Cors:AllowedOrigins` string array), defaulting to
`http://localhost:3000` in Development. **Do not** use `AllowAnyOrigin()` together with credentials.
- The client sends the token as a **bearer header** (not a cookie) on cross-origin calls, so
`AllowCredentials()` is not strictly required — but allow the `Authorization`, `Content-Type`,
`Accept-Language`, and `Idempotency-Key` headers (the client sets all four) and the methods the API uses.
- Register `app.UseCors(...)` in the pipeline **after `UseRouting()` and before `UseRateLimiter()`/
`UseAuthentication()`** so a pre-flight `OPTIONS` is answered before auth/limits run.
- Keep it config-driven so a deployed environment lists its real web origin(s) and Development stays permissive
to localhost only.
### 3.2 A local database story (so the API can start without the remote server)
Today both appsettings files hardcode the remote `87.107.152.16` SQL Server. A developer with no access to
that host cannot boot the API at all.
- Provide a **local** SQL Server for development. Recommended: a `server/docker-compose.yml` with
`mcr.microsoft.com/mssql/server:2022-latest` (Developer edition, a dev-only `SA_PASSWORD`) exposing `1433`.
- Point the **Development** connection string at that local instance **via a non-committed mechanism**
`dotnet user-secrets` (the server project already supports it) or environment variables — and replace the
committed remote string in `appsettings*.json` with a placeholder. (The full credential rotation is
[Refinement Phase 5](refinement-phase-5-security-hygiene.md); here we only need a working, secret-free local
default so the app boots.)
- Document the one-time HTTPS dev-cert trust: `dotnet dev-certs https --trust` — without it the browser (and
`fetch`) rejects `https://localhost:5002` and every call fails with an opaque network error.
- On boot the server already runs `MigrateAsync()` + seeders, so pointing at an empty local DB is enough — it
self-migrates and self-seeds roles/admin/gateway on first run. (Rich demo data is
[Refinement Phase 1](refinement-phase-1-database-and-seed.md).)
### 3.3 A Development-only way to complete OTP login without an SMS gateway
OTP delivery is `LoggingSmsSender` — it **logs** the code (`server/.../Seams/LoggingSmsSender.cs`) instead of
sending it. A tester can already read the 6-digit code from the server console, but make this explicit and
frictionless:
- Document the "read the OTP from the server console" procedure in the client and server run docs.
- Optionally (Development/Testing-gated only) return the code in the `request_otp` response **or** expose a
tiny `GET /api/v1/dev/last_otp/{phone}` behind an `IsDevelopment()` guard, so an automated/e2e flow can
complete login. **This must be impossible to enable outside Development** and is superseded by the real SMS
gateway in [Refinement Phase 8](refinement-phase-8-external-rails.md). Do not weaken the `otp` rate-limit or
the per-phone resend window.
### 3.4 Client env & run docs
- Confirm `client/.env.development` (auto-loaded by `npm run dev`) has `NEXT_PUBLIC_API_URL =
https://localhost:5002`. It already does — verify it matches the server's actual bound URL. If a developer
runs the API on a different port/scheme, they set a `client/.env.local` override.
- Add a top-level **"Run the whole app locally"** section (root `README.md` or a new
`dev/post-phase/refinement/RUNBOOK.md`) that ties the two `npm`/`dotnet` commands, the cert trust, the local
DB, and the OTP-from-logs step into one copy-pasteable procedure.
**(DEFERRED here, done later):** flipping any `USE_*_MOCK` flag (Phase 4), building any missing endpoint
(Phase 3), rotating the leaked credentials (Phase 5), a reverse proxy / production CORS origins (Phase 5/9).
## 4. Mocks & seams in this phase
No new seams. This phase uses the existing `LoggingSmsSender` (OTP in logs) as the interim OTP channel and
touches no business logic. The only "mock" decision is the optional Development-only OTP echo (3.3), which is
a dev affordance, not a seam.
## 5. Critical rules you must not get wrong
- **CORS middleware order matters.** `UseCors` must sit after `UseRouting` and before the rate limiter / auth,
or pre-flight `OPTIONS` requests get rejected (429/401) before the browser ever sends the real request.
- **Do not introduce a new committed secret.** The local DB password and connection string live in
user-secrets/env/compose, never in a committed `appsettings*.json`. Replacing the remote string with a
placeholder is in-scope; committing a new one is not.
- **The Development OTP shortcut is Development-only.** Gate it on `app.Environment.IsDevelopment()` and never
log/return the code in any other environment. It is a bridge until real SMS (Phase 8).
- **Change nothing about the money path, auth crypto, or the envelope shape.** This is plumbing only.
## 6. Definition of Done
On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):
- [ ] `server/`: CORS policy added (config-driven, localhost:3000 in Dev), registered in the correct pipeline
position; `dotnet build Baya.sln` clean, `dotnet test Baya.sln` green.
- [ ] A developer with **no access to the remote server** can boot the API against a local SQL Server using
only documented steps (compose/user-secrets), and the committed appsettings no longer carry a working
remote credential as the default.
- [ ] `dotnet dev-certs https --trust` and the OTP-from-logs step are documented in the runbook.
- [ ] `client/`: `npm run check` green; the client points at the running API; no mock flags changed.
- [ ] The end-to-end proof in §7 passes.
## 7. How to test (what a human can verify after this phase)
1. `docker compose up` (or start local SQL Server) → set the Dev connection string via user-secrets.
2. `cd server && dotnet run --project src/API/Baya.Web.Api/Baya.Web.Api.csproj` → API boots, applies
migrations, seeds, listens on `https://localhost:5002/swagger`.
3. `cd client && npm run dev` → `http://localhost:3000`.
4. Open `http://localhost:3000/fa/login`, enter a phone number, request the OTP.
5. **Read the 6-digit code from the server console** (the `LoggingSmsSender` line), enter it, submit.
6. **Expected:** you land on the customer home; the browser **Network tab shows real calls** to
`https://localhost:5002/api/v1/auth/request_otp`, `/auth/verify_otp`, and `/api/v1/me` all returning
`200` with the `ApiResult` envelope — **and no CORS error in the console**. This is the first real
authenticated round-trip between the two projects.
7. Negative check: temporarily remove the CORS registration and confirm the same flow now fails with a CORS
error in the console — proving the fix is what unblocked it.
## 8. Hand off & document (close the phase)
- Update `server/CLAUDE.md` "Startup wiring" to list the new CORS extension in the pipeline order.
- Write/append the local-run runbook (§3.4).
- Report per the operating rules: what was wired, the exact local-run procedure, and confirm auth is the only
real domain until Phase 4. Save a memory note on the CORS + local-DB + OTP-in-logs bring-up so the next
agent doesn't rediscover the blockers.