feat(proxy): plain-HTTP LAN entry for waiter (:80) and manager (:8081), no certs required

Phones can now open the waiter app at http://<LAN IP> with no domain, DNS
record or certificate (works around DNS-rebinding failures, KI-001). The
manager gets http://<LAN IP>:8081, with http://<LAN IP>/manager redirecting
there. waiter.*/manager.* hostnames on :80 still redirect to https, so
legacy domain sites behave as before.

- Both plain-HTTP servers are LAN-only (allow RFC1918/loopback/ULA/link-local,
  deny all -> 403), so a router port-forward can't expose an unencrypted POS
- nginx-proxy/nginx.conf and the install.sh heredoc are now byte-identical
  (one canonical config, routing map in its header)
- install.sh generates a 10-year self-signed cert when certs/ is empty (nginx
  won't start its TLS listeners without one), detects HOST_IP via
  'ip route get', prompts for it on fresh installs and backfills it into an
  existing .env, always starts the stack, prints the LAN URLs
- docker-compose publishes 8081; .env.example documents HOST_IP
- pack README: ports/request path updated, CS-5 byte-identical check

Verified: nginx -t; install.sh in Debian (fresh / upgrade without HOST_IP /
re-run - no duplicate HOST_IP, cert SAN includes HOST_IP, key 600); full
stack from freshly built images: every entry point returns the expected
200/301/302, and removing the gateway's range from the allow list yields 403
on :80 and :8081.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 11:50:29 +03:00
co-authored by Claude Opus 5.5
parent 18012c2c95
commit 9fcb4df30e
5 changed files with 330 additions and 54 deletions
+10 -6
View File
@@ -13,13 +13,15 @@ This folder is its **own git repo**, nested inside the `xenia-pos` parent repo.
| Service | Stack | Dev port | Prod exposure | Image |
|---|---|---|---|---|
| `local_backend` | FastAPI + SQLAlchemy + SQLite (`/app/data/pos.db`) | 8000 | only through proxy / inner nginx | `pos-backend` |
| `waiter_pwa` | React + Vite + vite-plugin-pwa, axios, zustand, react-query, Dexie (IndexedDB) | 5173 | `https://<host>` (443) | `pos-waiter` |
| `manager_dashboard` | React + Vite | 5174 | `https://<host>:4443` | `pos-manager` |
| `proxy` | nginx:alpine, TLS termination | — | 80 → 443 redirect, 443, 4443 | stock |
| `waiter_pwa` | React + Vite + vite-plugin-pwa, axios, zustand, react-query, Dexie (IndexedDB) | 5173 | **`http://<LAN IP>`** (80, LAN only) · `https://waiter.<domain>` / `https://<IP>` (443) | `pos-waiter` |
| `manager_dashboard` | React + Vite | 5174 | **`http://<LAN IP>:8081`** (LAN only; `http://<IP>/manager` redirects there) · `https://manager.<domain>` (443) · `https://<IP>:4443` | `pos-manager` |
| `proxy` | nginx:alpine | — | 80, 443, 4443, 8081. See the header of `nginx-proxy/nginx.conf` for the full routing map | stock |
### Request path in production
Phones normally use **plain HTTP by LAN IP** (`http://<HOST_IP>` → proxy:80 default_server). Only private source IPs are allowed; everything else gets 403.
Legacy domain sites use `https://waiter.<domain>` (proxy:443), and `waiter.*`/`manager.*` on port 80 still redirect to https.
```
phone ──https──▶ proxy:443 ──http──▶ waiter_pwa nginx:80 ──┬─ / → static SPA
phone ──http(s)──▶ proxy:80/443 ──http──▶ waiter_pwa nginx:80 ──┬─ / → static SPA
├─ /api/ → backend:8000
├─ /api/ws/ → backend:8000 (WebSocket upgrade)
└─ /static/ → backend:8000/static/
@@ -64,8 +66,10 @@ Waiters walk in and out of WiFi range and phones sleep. Any new waiter flow must
**CS-4. Money and prices are snapshotted.**
Prices, costs and discounts are copied onto order items and logs when the action happens (snapshot pattern, `PriceEventLog`). Reports read the snapshots, never the live catalogue.
**CS-5. The proxy config lives in two places.**
Edit `nginx-proxy/nginx.conf` **and** the heredoc in `install.sh` together (global Rule 9).
**CS-5. The proxy config lives in two places and must stay byte-identical.**
`install.sh` writes the heredoc on client sites, and `nginx-proxy/nginx.conf` is the repo copy. Edit both together (global Rule 9) and check with:
`sed -n "/<< 'EOF'/,/^EOF/p" install.sh | sed '1d;$d' | diff - nginx-proxy/nginx.conf`
The plain-HTTP servers (80 default_server, 8081) must keep their LAN-only `allow`/`deny` block.
**CS-6. Permissions are checked on the backend.**
Hiding a button in the UI is not access control. Every new endpoint declares its role or `perm_*` requirement through `routers/deps.py`.