cleanup phases 6
This commit is contained in:
@@ -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 (b0–b15) and the 16-phase frontend chain (f0–f15) 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.
|
||||
Reference in New Issue
Block a user