256 lines
15 KiB
Markdown
256 lines
15 KiB
Markdown
# 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 a database
|
||
|
||
**`dotnet user-secrets` is no longer used** — the `<UserSecretsId>` was removed from
|
||
`Baya.Web.Api.csproj`, so that store isn't read at all. A leftover `secrets.json` on your machine is inert
|
||
and can be deleted. All configuration lives in
|
||
[`appsettings.Development.json`](../../../server/src/API/Baya.Web.Api/appsettings.Development.json), which
|
||
already points at the **shared remote database** the deployed demo also uses — so a fresh clone boots with
|
||
no configuration step at all.
|
||
|
||
To work against the throwaway local container from step 2 instead, edit `ConnectionStrings:SqlServer` in
|
||
that file (the `Password` must match `MSSQL_SA_PASSWORD` in `server/docker-compose.yml`):
|
||
|
||
```jsonc
|
||
"SqlServer": "Server=localhost,1433;Database=Baya;User Id=sa;Password=Balinyaar_Dev1433;TrustServerCertificate=True;Encrypt=False;"
|
||
```
|
||
|
||
> **Env-var alternative** (CI/containers, or to avoid a local edit showing up in `git status`): set
|
||
> `ConnectionStrings__SqlServer` (double underscore = the `:` config separator) — it overrides the file.
|
||
> 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;"`
|
||
|
||
> Deploying rather than developing? See [DEPLOY.md](../../../DEPLOY.md).
|
||
|
||
---
|
||
|
||
## 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 against the (empty) local DB, then listens on
|
||
**`https://localhost:5002`** — Swagger at `https://localhost:5002/swagger`. In **Development** it also seeds a
|
||
sandbox payment gateway and the demo world (below). It does **not** seed the old `admin`/`qw123321` account
|
||
anymore (refinement-phase-5); a break-glass admin is created only if you set `Seed:AdminUsername` /
|
||
`Seed:AdminPassword` (see below), and the day-to-day admin path is the phone-OTP demo admins.
|
||
|
||
In **Development** it additionally runs the **demo-world seeder** (Refinement Phase 1): verified/unverified
|
||
demo nurses with priced variants + Tehran coverage (and therefore real `nurse_search_index` rows), plus demo
|
||
customers with patients and addresses. The console logs `Demo world seeded: 3 nurse(s), 2 customer(s)…` (or
|
||
`already seeded — no-op` on a subsequent run). It is **idempotent** and **never runs outside Development**.
|
||
|
||
On top of that, the **lifecycle seeder** (`DemoLifecycleSeeder`) populates every flow's mid-state so manual
|
||
testing never starts from an empty world: booking requests in every status, 8 bookings across every reachable
|
||
state (incl. a 5-session package mid-engagement and a cancelled-and-refunded one), the balanced ledger, three
|
||
refunds (card-succeeded / BNPL-processing / post-payout clawback), a **paid** and a **draft** payout batch,
|
||
reviews in all moderation states, tickets, notifications, patient care records, a merchant-of-record partner
|
||
center, and a mid-pipeline verification case. The console logs `Demo lifecycle seeded: …` (or `already
|
||
seeded — no-op`). The full scenario map + per-flow walkthroughs live in
|
||
[`dev/post-phase/manual-testing-plan.md`](../manual-testing-plan.md).
|
||
|
||
### 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).
|
||
|
||
---
|
||
|
||
## Demo accounts (Development seed)
|
||
|
||
The demo seeder creates these loginable accounts (phone-OTP; any 6-digit code you read from the console/dev
|
||
endpoint works). Use them to see the real path populated:
|
||
|
||
| Phone | Role | Who | State |
|
||
| --- | --- | --- | --- |
|
||
| `09120000001` | nurse | زهرا عزیزی (female) | **verified**, 3 variants, whole-city + 2 districts |
|
||
| `09120000002` | nurse | علی کریمی (male) | **verified**, 2 variants, 3 districts |
|
||
| `09120000003` | nurse | مریم احمدی (female) | **unverified** — not discoverable in search |
|
||
| `09120000010` | customer | سارا محمدی (female) | 2 patients, 1 Tehran address |
|
||
| `09120000011` | customer | رضا حسینی (male) | 1 patient, 1 Tehran address |
|
||
| `09120000020` | admin (`super_admin`) | نگار مدیری (female) | full backoffice — **lands on `/admin`**, sees every console incl. RBAC |
|
||
| `09120000021` | admin (`finance`) | کامران مالی (male) | scoped backoffice — lands on `/admin`, sidebar shows only the money consoles (`useAdminCapabilities` gating) |
|
||
| `09120000030` | partner-center owner | بهنام رستگار (male) | owns مرکز پرستاری آرامش (merchant-of-record, sponsors علی کریمی). No admin/nurse role — log in, then navigate to `/partner` manually (REQ-038: no `/me` partner signal yet) |
|
||
|
||
> The old username+password `admin`/`qw123321` account is **no longer auto-seeded** (refinement-phase-5 —
|
||
> no committed credential). To bootstrap a break-glass username+password admin, add both keys to
|
||
> `appsettings.Development.json` before boot, then log in via the API (not the web UI, which is phone-OTP only):
|
||
> ```jsonc
|
||
> "Seed": { "AdminUsername": "admin", "AdminPassword": "<a-strong-password>" }
|
||
> ```
|
||
|
||
The **phone-OTP admins** (`09120000020` / `09120000021`, refinement-phase-2) are how you reach the `/admin`
|
||
console through the same web login flow as everyone else — admin sub-roles are server-granted, never
|
||
self-selectable via `me/select_role`. Log in with either phone exactly like a nurse/customer; role
|
||
hydration routes you to `/admin`. To reach the **nurse** app, log in as a verified nurse phone
|
||
(`09120000001`); a fresh customer can also become a nurse in-app (SelectRole → `me/select_role`) and is
|
||
then routed to `/nurse` after the next `/me`.
|
||
|
||
Prove search works without the frontend: open Swagger →
|
||
`GET /api/v1/search/nurses?service_category_id=1&city_id=101` returns the two verified nurses' variants;
|
||
`service_category_id=3` (only the unverified nurse) returns an empty page.
|
||
|
||
## Log in (the real round-trip)
|
||
|
||
1. Open **`http://localhost:3000/fa/login`**.
|
||
2. Enter an Iranian mobile number (e.g. the verified nurse `09120000001`, or any demo phone above) 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 → role hydration routes you to the app for your role: a customer to the family
|
||
home (`/`), a nurse to `/nurse`, an admin to `/admin` (refinement-phase-2).
|
||
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.
|
||
|
||
### OTP over Telegram (optional — instead of reading the log)
|
||
|
||
For manual testing you can have the code arrive **on your phone in Telegram** rather than in the server
|
||
console. A standalone dev-only relay ([`telegram-otp-bot/`](../../../telegram-otp-bot/README.md)) forwards it;
|
||
the API talks to it through the normal `ISmsSender` seam. **Development only** — the relay *broadcasts* every
|
||
code to every configured chat id, so it is a test-group convenience, not an SMS gateway.
|
||
|
||
1. **Start the relay** (see its README for creating the bot with @BotFather and discovering chat ids —
|
||
each recipient must press **Start** in Telegram first, then `GET /chat_ids`):
|
||
```bash
|
||
cd telegram-otp-bot && npm start # no npm install — zero dependencies
|
||
```
|
||
`api.telegram.org` is filtered in Iran, so set `TELEGRAM_PROXY_URL` in its `.env` to your VPN/proxy
|
||
client (`http://127.0.0.1:10809`, `socks5://…`, or the proxy container on a VPS). The boot banner prints
|
||
the bot's `@username` — that line appearing means the token *and* the proxy work.
|
||
2. **Share the secret with the API** — the same value on both sides: the relay's `.env` `API_KEY` and
|
||
`Seams:Sms:Telegram:ApiKey` in `appsettings.Development.json`. The appsettings side is already filled
|
||
in; copy that value into your local `telegram-otp-bot/.env`. Do **not** use the one in `.env.example` —
|
||
it is published in git, so `TelegramSmsSender` rejects it with
|
||
`Seams:Sms:Telegram:ApiKey is not configured (unset, or still the published example key)`.
|
||
3. **Flip the provider** in `server/src/API/Baya.Web.Api/appsettings.Development.json`:
|
||
```jsonc
|
||
"Seams": { "Sms": { "Provider": "telegram" } } // committed default is "mock"
|
||
```
|
||
4. **Log in** as usual — the 6-digit code arrives in Telegram. The `dev/last_otp` helper keeps working
|
||
alongside it (`telegram` is the one non-mock provider that leaves the capture bridge on), so scripts and
|
||
e2e tests are unaffected. Set the provider back to `mock` to return to reading the console.
|
||
|
||
If the relay is down or reaches nobody it answers `502` and **login fails loudly** (`request_otp` returns an
|
||
error) rather than pretending an undelivered code was sent.
|
||
|
||
---
|
||
|
||
## Good to know
|
||
|
||
- **The API speaks HTTP/1.1 and HTTP/2** (Kestrel `Protocols: Http1AndHttp2`, refinement-phase-5 — the
|
||
previous HTTP/2-only default broke non-TLS HTTP/1.1 hops). Over TLS the client negotiates h2 via ALPN, so
|
||
gRPC and `fetch` both work; plain-HTTP hops fall back to HTTP/1.1.
|
||
- **Secrets fail fast.** If `ConnectionStrings` is blank or still the base file's
|
||
`SET_VIA_USER_SECRETS_OR_ENV` placeholder, the API refuses to start with
|
||
`Refusing to start: required secret configuration is missing…`. Production/Staging must additionally supply
|
||
real `IdentitySettings` JWE keys and `Seams:FieldEncryption` keys; Development uses the dev-only ones in
|
||
`appsettings.Development.json`.
|
||
- **Never change `Seams:FieldEncryption:Key`/`:HashKey`.** They decrypt every PII column in the shared
|
||
database and derive `users.PhoneHash`, which every login looks up. Changing either locks everyone out.
|
||
- **Enable the secret-scan pre-commit hook** once per clone so a stray credential can't be committed:
|
||
`git config core.hooksPath .githooks` (see [`.githooks/README.md`](../../../.githooks/README.md)).
|
||
- **Behind a reverse proxy**, list its address in `ForwardedHeaders:KnownProxies` (or a CIDR in
|
||
`:KnownNetworks`) so the rate limiter partitions on the real client IP, not the proxy's.
|
||
- **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 — including the
|
||
Development demo world (nurses, variants, search rows, customers) from
|
||
[Refinement Phase 1](refinement-phase-1-database-and-seed.md).
|
||
- **Explicit migration path (deploy / migrations-off-boot).** Boot-time `MigrateAsync` is the dev convenience;
|
||
to apply migrations without booting the app (the direction [Refinement Phase 7](refinement-phase-7-unattended-ops.md)
|
||
will formalise), run from `server/`:
|
||
```bash
|
||
dotnet ef database update --project src/Infrastructure/Baya.Infrastructure.Persistence --startup-project src/API/Baya.Web.Api
|
||
```
|
||
(`dotnet tool install --global dotnet-ef` if the `ef` command is missing. It reads the same connection
|
||
string, so set the user-secret / env var first.) This applies **schema only** — the reference + demo seeds
|
||
run on the next app boot.
|
||
- **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 + reference + demo seed next run)
|
||
```
|
||
|
||
Re-running `dotnet run` against an **existing** seeded DB does not duplicate anything — both the reference and
|
||
the demo seeders are idempotent (the demo seeder guards each persona on its phone number). To get a clean demo
|
||
world, wipe the volume (`docker compose down -v`) and boot again.
|
||
|
||
## Troubleshooting
|
||
|
||
| Symptom | Fix |
|
||
| --- | --- |
|
||
| Browser: `net::ERR_CERT_AUTHORITY_INVALID` on `:5002` | Run `dotnet dev-certs https --trust` (setup step 1). |
|
||
| API startup: `Refusing to start: required secret configuration is missing…` | `ConnectionStrings:SqlServer` is blank or still a placeholder. Do setup step 3. |
|
||
| API startup: `Login failed for user 'sa'` / connect timeout | DB not up or wrong password — check `docker compose ps` and that the password in `appsettings.Development.json` 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" | Expected — user-secrets was removed. Edit `appsettings.Development.json` instead. |
|