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

@@ -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`: