Files
baya-monorepo/archive/post-phase/refinement/refinement-phase-0-bring-up.md
T
2026-08-02 18:48:32 +03:30

172 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.