feat: scope workspace routes by organization slug
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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`:
|
||||
|
||||
Reference in New Issue
Block a user