|
| 1 | +# (app) Route Group |
| 2 | + |
| 3 | +This is a Next.js [route group](https://nextjs.org/docs/app/building-your-application/routing/route-groups). The parenthesized folder name does not affect the URL structure. Routes here are served at the root (e.g., `/search`, `/chat`, `/settings`). |
| 4 | + |
| 5 | +## Why this route group exists |
| 6 | + |
| 7 | +Routes outside `(app)/` (like `/login`, `/signup`, `/invite`, `/onboard`) are accessible without authentication. Routes inside `(app)/` go through the layout's auth and membership guards before rendering. |
| 8 | + |
| 9 | +## What the layout does |
| 10 | + |
| 11 | +The `layout.tsx` acts as a gate and app shell. It runs the following checks in order, short-circuiting if any condition is met: |
| 12 | + |
| 13 | +1. **Org existence** - Looks up the single-tenant org by `SINGLE_TENANT_ORG_ID`. Returns 404 if missing. |
| 14 | +2. **Authentication** - If the user is not logged in and anonymous access is not enabled, redirects to `/login` (or renders GCP IAP auth if configured). |
| 15 | +3. **Membership** - If the user is logged in but not a member of the org, renders one of: |
| 16 | + - `JoinOrganizationCard` if the org does not require approval |
| 17 | + - `SubmitJoinRequest` / `PendingApprovalCard` if the org requires approval |
| 18 | +4. **Onboarding** - If the org has not completed onboarding, wraps children in `OnboardGuard`. |
| 19 | +5. **SSO account linking** - If required SSO providers are not linked, renders `ConnectAccountsCard`. |
| 20 | +6. **Mobile splash screen** - Shows an unsupported screen on mobile devices (dismissible via cookie). |
| 21 | + |
| 22 | +After all guards pass, the layout wraps children with shared UI: `SyntaxGuideProvider`, `PermissionSyncBanner`, `GitHubStarToast`, and `UpgradeToast`. |
| 23 | + |
| 24 | +## What the layout does NOT do |
| 25 | + |
| 26 | +- **Role-based access control** - The layout does not check `OWNER` vs `MEMBER`. Pages that require a specific role should use `authenticatedPage` with the `minRole` option. |
| 27 | +- **Guarantee a user exists** - When anonymous access is enabled, the user may be undefined. |
| 28 | + |
| 29 | +## Writing pages in (app) |
| 30 | + |
| 31 | +Use the `authenticatedPage` HOC from `@/middleware/authenticatedPage`. It resolves the auth context (`user`, `org`, `role`, `prisma`) and handles redirects on auth failure. This avoids manual org lookups with `SINGLE_TENANT_ORG_ID` — pages inside `(app)/` should not reference that constant directly. |
| 32 | + |
| 33 | +```tsx |
| 34 | +import { authenticatedPage } from "@/middleware/authenticatedPage"; |
| 35 | + |
| 36 | +// Basic authenticated page |
| 37 | +export default authenticatedPage(async ({ prisma }) => { |
| 38 | + const data = await prisma.repo.findMany(); |
| 39 | + return <MyPage data={data} />; |
| 40 | +}); |
| 41 | + |
| 42 | +// Owner-only page |
| 43 | +export default authenticatedPage(async ({ org }) => { |
| 44 | + return <AdminPage orgName={org.name} />; |
| 45 | +}, { minRole: OrgRole.OWNER, redirectTo: '/settings' }); |
| 46 | + |
| 47 | +// Page that allows anonymous access |
| 48 | +export default authenticatedPage(async ({ user, prisma }) => { |
| 49 | + // user may be undefined |
| 50 | + return <PublicPage />; |
| 51 | +}, { allowAnonymous: true }); |
| 52 | +``` |
0 commit comments