feat: scope workspace routes by organization slug

This commit is contained in:
2026-07-02 20:11:57 -07:00
parent fae97a7046
commit 3ab0e3e034
48 changed files with 565 additions and 96 deletions

View File

@@ -216,6 +216,17 @@ surface.
implemented, show an honest not-connected state instead of fake provider accounts.
- The sidebar nav selected state belongs in a small client component using `usePathname()`;
keep the rest of `AppShell` server-rendered.
- Authenticated workspace links should preserve the active organization slug by using
`modules/shared/lib/workspace-routing.ts`. Sidebar links, breadcrumbs, permission
redirects, table search/pagination paths, login redirects, and organization switch
redirects should call `getWorkspaceHref(activeSlug, workspacePath)` instead of hard-coding
`/app/...` paths.
- The legacy unscoped `/app/...` paths remain valid fallback paths for accounts without an
active organization. When a session has an active organization, redirect into
`/app/{organizationSlug}/...` so operators can see which workspace they are using.
- Top-level workspace route names such as `dashboard`, `settings`, `elders`, and `beds` are
reserved path segments, not tenant identifiers. Keep the frontend reserved list in
`workspace-routing.ts` aligned with backend organization slug validation.
### Business Form Defaults Contract

View File

@@ -155,6 +155,71 @@ app/(app)/
└── page.tsx -> modules/orders/ (detail view)
```
## Scenario: Organization-Scoped Workspace Routes
### 1. Scope / Trigger
- Trigger: adding or changing protected app workspace routes, app-shell navigation, breadcrumbs, login redirects, organization switching, or settings pagination links.
- The current workspace URL must include the active organization slug when an organization is selected.
### 2. Signatures
- Canonical workspace route: `/app/[organizationSlug]/<workspacePath>`.
- Legacy-compatible route: `/app/<workspacePath>` may remain available while old links are migrated.
- Shared helpers live in `modules/shared/lib/workspace-routing.ts`:
- `getWorkspaceHref(organizationSlug: string | undefined, path?: string): string`
- `getWorkspacePathFromPathname(pathname: string): string`
- `getWorkspaceSlugFromPathname(pathname: string): string | undefined`
- `isWorkspacePathActive(pathname: string, itemPath: string): boolean`
### 3. Contracts
- `navGroups` stores workspace-local paths such as `/dashboard`, `/settings/users`, not full `/app/...` hrefs.
- `AppShell` passes the current `organization.slug` into navigation and logo links.
- `AppSidebarNav`, `AppBreadcrumbs`, settings search forms, and pagination must generate hrefs with `getWorkspaceHref(...)`.
- The `[organizationSlug]` layout must reject slug/session mismatches by redirecting to the active session organization workspace.
- Organization slugs cannot use reserved first-level workspace section keys such as `dashboard`, `settings`, `elders`, or `beds`.
### 4. Validation & Error Matrix
- Missing active organization slug -> helpers fall back to legacy `/app/<workspacePath>`.
- URL slug differs from `getCurrentAuthContext().organization.slug` -> redirect to `getWorkspaceHref(activeSlug, "/dashboard")`.
- New or updated organization slug equals a reserved workspace key -> API returns validation failure.
- A page-level redirect inside protected routes -> use `getWorkspaceHref(context.organization?.slug, targetPath)`.
### 5. Good/Base/Bad Cases
- Good: organization switch calls `POST /api/auth/organization`, then navigates to the same workspace path under the returned active organization slug.
- Base: legacy `/app/dashboard` remains renderable for compatibility, but new app-shell links point to `/app/{slug}/dashboard`.
- Bad: hard-code `/app/settings/users` in table forms or breadcrumbs; it drops users out of the organization-scoped workspace.
- Bad: infer tenant from `localStorage`; active organization remains server-session state.
### 6. Tests Required
- `pnpm lint`
- `pnpm type-check`
- `pnpm build`
- Browser assertions:
- login/setup lands on `/app/{activeOrg.slug}/dashboard`
- sidebar links preserve the active organization slug
- settings search and pagination preserve the active organization slug
- switching organization moves the URL to the new slug while preserving the workspace path
- mismatched `/app/{wrongSlug}/...` redirects to the active organization workspace
### 7. Wrong vs Correct
#### Wrong
```tsx
<Link href="/app/settings/users"></Link>
```
#### Correct
```tsx
<Link href={getWorkspaceHref(organization.slug, "/settings/users")}></Link>
```
## Import Path Aliases
Configure in `tsconfig.json`: