ac22ccef9b
Lands the minimum surface so a developer can: cd platform/orca-platform && make dev-up cd platform/tenant-registry && make dev cd platform/portal && make install && make dev open http://acme.localhost:3000 and complete a real OIDC sign-in against the breakpilot-dev realm. Layout: src/middleware.ts host→slug URL rewrite; backstage carve-out src/auth.ts Auth.js v5 Keycloak provider; passes tenant_id/slug/org_roles/products/plan/status claims through to the session src/app/api/auth/[...nextauth]/ Auth.js handlers (GET, POST) src/app/layout.tsx root html shell src/app/page.tsx apex landing src/app/[slug]/layout.tsx fetches tenant via lib/tenant-registry src/app/[slug]/page.tsx redirect to /dashboard src/app/[slug]/dashboard/page.tsx signed-out → Sign in with Keycloak signed-in → welcome + Sign out src/lib/host.ts testable host parser (apex/tenant/backstage) src/lib/tenant-registry.ts fetch client for the Go service Tooling: vitest 13 tests, 100% coverage of src/lib/ Next.js 15 build compiles all routes; output: standalone ESLint flat config next/core-web-vitals + next/typescript Real RBAC enforcement, the rest of the customer-area surfaces, and the backstage shell land per the M5.2 / M10.1 schedule. This is just enough to be the first thing a developer codes in. Refs: M5.1 (skeleton)
42 lines
1.4 KiB
TypeScript
42 lines
1.4 KiB
TypeScript
// Host → tenant slug parser for the portal middleware.
|
|
//
|
|
// In dev we serve at <slug>.localhost:3000 (e.g. acme.localhost:3000). In
|
|
// prod we serve at <slug>.breakpilot.com. Backstage lives at the apex —
|
|
// no subdomain — and resolves to a fixed `__backstage__` slug.
|
|
|
|
export type HostMatch =
|
|
| { kind: "tenant"; slug: string }
|
|
| { kind: "backstage" }
|
|
| { kind: "apex" }
|
|
| { kind: "unknown" };
|
|
|
|
// Longest-first so `stage.breakpilot.com` is matched before `breakpilot.com`.
|
|
const APEX_HOSTS = ["stage.breakpilot.com", "breakpilot.com", "localhost"];
|
|
const APEX_SET = new Set(APEX_HOSTS);
|
|
|
|
export function parseHost(host: string | null | undefined): HostMatch {
|
|
if (!host) return { kind: "unknown" };
|
|
|
|
const hostNoPort = host.split(":")[0].toLowerCase();
|
|
|
|
if (APEX_SET.has(hostNoPort)) return { kind: "apex" };
|
|
|
|
// Strip the known apex suffix to extract the subdomain.
|
|
for (const apex of APEX_HOSTS) {
|
|
const suffix = `.${apex}`;
|
|
if (hostNoPort.endsWith(suffix)) {
|
|
const sub = hostNoPort.slice(0, -suffix.length);
|
|
if (!sub) return { kind: "apex" };
|
|
// Backstage is reserved.
|
|
if (sub === "backstage") return { kind: "backstage" };
|
|
// Slugs are [a-z0-9-]{2,40} per the tenant-registry schema check.
|
|
if (/^[a-z0-9-]{2,40}$/.test(sub)) {
|
|
return { kind: "tenant", slug: sub };
|
|
}
|
|
return { kind: "unknown" };
|
|
}
|
|
}
|
|
|
|
return { kind: "unknown" };
|
|
}
|