f98d20ef0d
Live-stack debugging caught this: Auth.js v5 builds the OAuth redirect_uri from AUTH_URL, NOT from the request Host header, even with AUTH_TRUST_HOST=true. If you visit http://acme.localhost:3000 with AUTH_URL=http://localhost:3000, Keycloak rejects the token exchange because the PKCE cookie was set on acme.localhost but the callback URL Auth.js sent was localhost. Fix in dev: pin AUTH_URL to the subdomain you're testing on. In prod, orca-proxy passes the right host via X-Forwarded-Host and AUTH_URL is set to the apex. Updates .env.example with a long-form note + sets AUTH_URL to the acme tenant so a copy/paste-and-go workflow Just Works. Adds a 'AUTH_URL gotcha' callout to the local-dev section in README. Refs: M5.1
111 lines
5.7 KiB
Markdown
111 lines
5.7 KiB
Markdown
# portal
|
|
|
|
Next.js 16 customer area + backstage.
|
|
|
|
> Part of the **Breakpilot Platform**. For the big picture see [`platform/docs`](https://gitea.meghsakha.com/platform/docs):
|
|
> [Architecture](https://gitea.meghsakha.com/platform/docs/src/branch/main/PLATFORM_ARCHITECTURE.md) ·
|
|
> [Infrastructure](https://gitea.meghsakha.com/platform/docs/src/branch/main/INFRASTRUCTURE.md) ·
|
|
> [Product Integration Spec](https://gitea.meghsakha.com/platform/docs/src/branch/main/PRODUCT_INTEGRATION_SPEC.md) ·
|
|
> [Implementation Plan](https://gitea.meghsakha.com/platform/docs/src/branch/main/IMPLEMENTATION_PLAN.md)
|
|
|
|
## What this is
|
|
|
|
Next.js 16 customer area + backstage. Scaffolded under milestone M5.1. See [`platform/docs`](https://gitea.meghsakha.com/platform/docs) for the full architecture context.
|
|
|
|
**Plane:** Control
|
|
**Owner:** @sharang
|
|
**Status:** pre-alpha
|
|
**Linked milestone:** [M5.1](https://gitea.meghsakha.com/platform/docs/src/branch/main/IMPLEMENTATION_PLAN.md)
|
|
|
|
## Run locally
|
|
|
|
```bash
|
|
# Prerequisites: Node 20+, pnpm 9+, the dev stack running.
|
|
|
|
# 1. Bring up Keycloak + Postgres + Redis (separate clone):
|
|
cd /path/to/platform/orca-platform && make dev-up
|
|
|
|
# 2. Run tenant-registry (separate clone):
|
|
cd /path/to/platform/tenant-registry && make dev
|
|
|
|
# 3. Run this app:
|
|
make install # pnpm install --frozen-lockfile
|
|
make dev # next dev on http://localhost:3000
|
|
|
|
# Or hit a real tenant immediately:
|
|
# open http://acme.localhost:3000 → redirects to Keycloak → back to /acme/dashboard
|
|
```
|
|
|
|
Seed login (from the dev-stack realm): `test@breakpilot.dev` / `test`.
|
|
|
|
> **AUTH_URL gotcha:** Auth.js v5 builds the OAuth `redirect_uri` from `AUTH_URL` — not from the request Host header, even with `AUTH_TRUST_HOST=true`. For multi-tenant dev work, pin `AUTH_URL` to the subdomain you log in on (e.g., `http://acme.localhost:3000`); otherwise Keycloak rejects the token exchange with `invalid_grant: Incorrect redirect_uri`. In prod, orca-proxy passes the right host via `X-Forwarded-Host` and `AUTH_URL` is set to the apex (`https://breakpilot.com`).
|
|
|
|
`make test` / `make lint` / `make typecheck` / `make build` run vitest / eslint / tsc / next build respectively.
|
|
|
|
Env vars live in `.env.example`. Copy to `.env.local` for local overrides (gitignored).
|
|
|
|
## Surface
|
|
|
|
| Route | Renders |
|
|
|---|---|
|
|
| `http://localhost:3000/` | Apex landing — pointer to tenant subdomains |
|
|
| `http://<slug>.localhost:3000/` | Middleware rewrites to `/[slug]/` → redirects to `/[slug]/dashboard` |
|
|
| `http://<slug>.localhost:3000/dashboard` | OIDC-gated dashboard; signed-out users see "Sign in with Keycloak" |
|
|
| `http://backstage.localhost:3000/` | (Skeleton) backstage route — rewritten to `/__backstage__/*` |
|
|
| `/api/auth/[...nextauth]` | Auth.js v5 endpoints (callback, signin, signout, jwt) |
|
|
|
|
## Architecture notes
|
|
|
|
- **Host → slug routing**: `src/middleware.ts` parses `Host` header via `parseHost()` (in `src/lib/host.ts`) and rewrites the request path to `/<slug>/...`. URL bar stays unchanged. Apex hosts and unknown subdomains fall through unmodified.
|
|
- **Tenant context**: `src/app/[slug]/layout.tsx` fetches the tenant from `tenant-registry` (`src/lib/tenant-registry.ts`). 404 → `notFound()`; HTTP errors bubble up.
|
|
- **Auth**: `src/auth.ts` is the Auth.js v5 config — Keycloak provider, tenant-context claims (`tenant_id`, `tenant_slug`, `org_roles`, `products`, `plan`, `tenant_status`) propagated via JWT/session callbacks. Real RBAC enforcement lands in M5.2 / M10.1.
|
|
|
|
## Deployment
|
|
|
|
| Env | URL | How |
|
|
|---|---|---|
|
|
| dev | `http://localhost:3000` | `make dev` |
|
|
| stage | `https://portal.stage.breakpilot.com` | auto on merge to `main` |
|
|
| prod | `https://portal.breakpilot.com` | manual: tag `vX.Y.Z` + sign-off |
|
|
|
|
Rollback: `orca rollout undo portal --env={{env}}`.
|
|
|
|
## Observability
|
|
|
|
- Traces, logs, metrics: [SigNoz](https://signoz.meghsakha.com) — service name `portal`
|
|
- Audit events: Tenant Registry `/audit` (Retraced-shape schema)
|
|
- On-call: `oncall@breakpilot.com` · runbook at `platform/docs/runbooks/portal.md`
|
|
|
|
## Contributing
|
|
|
|
See [`CONTRIBUTING.md`](./CONTRIBUTING.md). TL;DR: branch from main, open a PR, 1 review + green CI, squash-merge.
|
|
|
|
|
|
|
|
## End-to-end tests (M5.3)
|
|
|
|
Playwright config at `playwright.config.ts`. Tests under `tests/e2e/`.
|
|
|
|
```bash
|
|
make e2e-install # one-time: pnpm exec playwright install chromium
|
|
# bring up the dev stack + tenant-registry + portal in three separate terminals,
|
|
# then:
|
|
make e2e # pnpm playwright test
|
|
```
|
|
|
|
Test groups (filter with `--grep`):
|
|
|
|
| File | What it asserts |
|
|
|---|---|
|
|
| `tests/e2e/apex.spec.ts` | Apex landing page renders |
|
|
| `tests/e2e/tenant.spec.ts` | Tenant subdomain serves signed-out dashboard + 404 on unknown slug |
|
|
| `tests/e2e/health.spec.ts` | The whole dev stack is reachable: portal API, tenant-registry, Keycloak |
|
|
|
|
`@needs-stack` in a test title means the dev stack must be running. We don't yet have a full OIDC click-through test — Keycloak in headless mode is flaky, so we assert the gate (Sign-in button visible) rather than completing the login.
|
|
|
|
In CI, the e2e job is gated behind the repo variable `RUN_E2E == 'true'` so it stays off until stage exists. Lint / typecheck / build / vitest still run on every PR.
|
|
|
|
## License
|
|
|
|
Proprietary — all rights reserved. Copyright (c) 2026 Sharang Parnerkar and Benjamin Boenisch. See [`LICENSE`](./LICENSE).
|