Some checks failed
Architecture Governance / architecture (push) Has been cancelled
nginx owns the validated CORS response; the live :445 backend rejects requests when the browser Origin is forwarded.
123 lines
5.0 KiB
Markdown
123 lines
5.0 KiB
Markdown
# Final handoff: tenant API domains
|
|
|
|
This is the required production contract between the shared frontend, nginx,
|
|
and the backend. It supersedes any fixed `api.dexarmarket.ru` or same-origin
|
|
`/backend` routing proposal.
|
|
|
|
## 1. Deterministic hostname rule
|
|
|
|
The frontend uses one API hostname per **base domain**:
|
|
|
|
| Storefront | API origin | Bootstrap |
|
|
|---|---|---|
|
|
| `example.com` | `https://api.example.com` | `https://api.example.com/bootstrap` |
|
|
| `store1.example.com` | `https://api.example.com` | `https://api.example.com/bootstrap` |
|
|
| `www.example.com` | `https://api.example.com` | `https://api.example.com/bootstrap` |
|
|
|
|
Bootstrap, auth, legacy routes, and `/api/...` routes all use this origin.
|
|
Localhost is the only exception and continues through the local `/api` proxy.
|
|
|
|
## 2. Backend changes required
|
|
|
|
For every request received publicly on the shared `api.<base-domain>`:
|
|
|
|
1. Behind the trusted project nginx, use `X-Storefront-Host`. nginx derives it
|
|
from a validated browser `Origin`, sends the same value as upstream `Host`,
|
|
and preserves the shared public API hostname in `X-Forwarded-Host`.
|
|
2. Do not infer a subdomain tenant from the API `Host`: `store1.example.com` and
|
|
`example.com` intentionally share `api.example.com`. Non-browser clients must
|
|
provide tenant context through their authenticated server-to-server contract.
|
|
3. Resolve that normalized storefront hostname through the tenant-domain
|
|
registry. Do not infer a tenant from only the first label.
|
|
4. Reject unknown, disabled, or unverified domains with `403` before reading
|
|
tenant data. Never fall back to the default/Dexar tenant.
|
|
5. Bind the authenticated session to the resolved tenant and reject a mismatch.
|
|
6. Trust `X-Storefront-Host` / `X-Forwarded-*` only from the known nginx proxy;
|
|
direct clients can forge them.
|
|
7. Return JSON for `/bootstrap`, including a tenant identity that matches the
|
|
normalized storefront domain. HTML or a default tenant response is a fault.
|
|
|
|
Pseudo-code:
|
|
|
|
```text
|
|
require request.remoteAddress is trustedProxy
|
|
storefrontHost = lower(stripPort(request.header["X-Storefront-Host"]))
|
|
tenant = registry.findVerifiedDomain(storefrontHost) ?? forbidden()
|
|
request.tenant = tenant
|
|
```
|
|
|
|
## 3. CORS contract
|
|
|
|
For API host `api.<base-domain>`, echo the exact validated storefront origin:
|
|
|
|
```http
|
|
Access-Control-Allow-Origin: https://<base-domain or registered subdomain>
|
|
Access-Control-Allow-Credentials: true
|
|
Vary: Origin
|
|
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
|
|
Access-Control-Allow-Headers: Authorization, Content-Type, AdminWebSessionID, X-Requested-With
|
|
```
|
|
|
|
Answer valid preflight requests with `204`. Do not use `*` together with
|
|
credentials. nginx applies this policy now; the backend should enforce the same
|
|
allowlist when it is reached without that proxy.
|
|
|
|
## 4. nginx and TLS
|
|
|
|
Run the idempotent project script as root:
|
|
|
|
```bash
|
|
scripts/deploy/configure-api-domain.sh \
|
|
--domain gorbushka.market \
|
|
--email ops@example.com \
|
|
--upstream https://127.0.0.1:445
|
|
```
|
|
|
|
It creates the shared `api.gorbushka.market`, issues/renews its certificate,
|
|
configures CORS for `gorbushka.market` and its subdomains, and proxies all paths
|
|
to the backend. A request from `store1.gorbushka.market` reaches upstream with
|
|
`Host` and `X-Storefront-Host` set to `store1.gorbushka.market`, while
|
|
`X-Forwarded-Host` remains `api.gorbushka.market`. nginx terminates CORS and
|
|
strips the browser `Origin` before proxying because the current `:445` service
|
|
rejects direct browser origins; tenant identity is carried by the trusted
|
|
storefront header instead.
|
|
|
|
`store1.example.com` requires no additional API DNS or certificate; it uses the
|
|
same `api.example.com` certificate as the root storefront.
|
|
|
|
## 5. CI/CD contract
|
|
|
|
`deploy.yml` runs the same root-owned configurator before activating a frontend
|
|
release. Required production secrets:
|
|
|
|
| Secret | Example |
|
|
|---|---|
|
|
| `DEPLOY_HOST` | server hostname/IP |
|
|
| `DEPLOY_USER` | `deploy` |
|
|
| `DEPLOY_SSH_KEY` | private deploy key |
|
|
| `DEPLOY_KNOWN_HOSTS` | pinned SSH host-key line |
|
|
| `STOREFRONT_DOMAINS` | `gorbushka.market store1.example.com` |
|
|
| `CERTBOT_EMAIL` | operations email |
|
|
| `BACKEND_UPSTREAM` | `https://127.0.0.1:445` (optional default) |
|
|
|
|
One-time provisioning must first run `server-setup.sh`; it installs the helper
|
|
as root-owned `/usr/local/sbin/marketplaces-configure-api-domain` and grants the
|
|
deploy user permission to run only that validated command plus nginx reload.
|
|
|
|
## 6. Acceptance checks
|
|
|
|
For every storefront domain, all of these must pass:
|
|
|
|
```bash
|
|
curl -fsS https://api.example.com/bootstrap | jq -e 'type == "object"'
|
|
curl -i -X OPTIONS https://api.example.com/bootstrap \
|
|
-H 'Origin: https://example.com' \
|
|
-H 'Access-Control-Request-Method: GET'
|
|
```
|
|
|
|
- Frontend bundle contains no fixed marketplace API hostname.
|
|
- Root and nested storefronts call the same `api.<base-domain>`.
|
|
- Unknown storefront domains return `403`, not the default tenant.
|
|
- `/bootstrap` returns JSON and the correct tenant.
|
|
- API responses never return the Angular `index.html` fallback.
|