History

Move web routes under /:org and API routes under /api/:org

The active org moves off the WorkOS session cookie and onto the URL — the cookie still proves login identity, but membership is verified per request against the URL-resolved org. This unblocks "in same browser tab, switch orgs by changing the URL" without a full session refresh. Web side: - All protected routes move to `apps/cloud/src/routes/$org/...`. The bracket-escape `[-]` convention keeps `[-].billing` and `[-].settings` as literal `-` URL segments under `/$org/-/...`. - A new `OrgRouteProvider` resolves the URL handle via `auth.organizations` and exposes `{ orgId, orgName, orgHandle }` to descendants. Unknown handles redirect to the first membership. - `/` index redirects to the first membership's `/$handle`. AuthGate routes `organizations.length === 0` to onboarding. - The org switcher is now a `<Link>` to `/$org`; `switchOrganization`, `organizationsAtom`, and the related session-refresh path are gone. - Cloud's NavItem wrapper goes away — sidebar links use typed `<Link>` directly with a `navItemClassName(active)` helper. API side: - Cloud's protected API mounts under `/api/:org/...` via a `HttpRouter.prefixed("/api/:org")` layer. - `ExecutionStackMiddleware` reads the URL `:org` segment, looks up the org row by handle, and verifies membership against WorkOS — independent of `session.organizationId`. - The frontend executor client sets its base URL to `${origin}/api/${orgHandle}` once the org layout resolves. - `/auth/me` returns `organizations: Array<{id, handle, name}>` instead of a single org. `createOrganization` and `acceptInvitation` no longer refresh the session — the URL is the source of truth, the client just navigates to the new handle. Test harness: - The harness mirrors the prefix layer, lazily seeds an organizations row whose handle equals the requested orgId, and rewrites outgoing request URLs to include `/api/${orgId}`. The `x-test-org-id` header is gone; `x-test-user-id` remains for tests that need a non-default user.

Rhys Sullivan committed 6 months ago

Commit b7508931 parent 0a99d1723 files · +792 −510Browse files

Showing the changes to apps/cloud/src/api.request-scope.node.test.ts (1 of 23 files). Show all

Unified · Split

apps/cloud/src/api.request-scope.node.test.ts+6−5
@@ -183,11 +183,12 @@ describe("makeApiLive (prod handler factory) request scoping", () => { · 182 unmodified lines
183183 }).handler;
184184
185185 // Hit a protected route. ExecutionStackMiddleware short-circuits with
186− // 403 (no session cookie) but not before `requestScopedMiddleware`
187− // has built the per-request layer. We don't care about the response —
188− // only that the layer was built once per request.
189− await handler(new Request("http://test.local/scope"));
190− await handler(new Request("http://test.local/scope"));
186+ // 401/404 (no session cookie / unknown org) but not before
187+ // `requestScopedMiddleware` has built the per-request layer. We don't
188+ // care about the response — only that the layer was built once per
189+ // request. The protected API mounts under `/api/:org/...`.
190+ await handler(new Request("http://test.local/api/test_org/scope"));
191+ await handler(new Request("http://test.local/api/test_org/scope"));
191192
192193 expect(counts.acquires).toBe(2);
193194 expect(counts.releases).toBe(2);