From e899b14fa73ed96277a050e74dcbd3d443706772 Mon Sep 17 00:00:00 2001 From: Adolfo Delorenzo Date: Tue, 24 Mar 2026 17:08:50 -0600 Subject: [PATCH] docs(04-rbac-02): complete portal RBAC integration plan - 04-02-SUMMARY.md: Auth.js JWT + role nav + tenant switcher + impersonation banner + user pages - STATE.md: advanced to plan 3, metrics recorded, base-ui decisions added - ROADMAP.md: phase 4 updated to 2/3 plans complete - REQUIREMENTS.md: RBAC-05 marked complete --- .planning/REQUIREMENTS.md | 4 +- .planning/ROADMAP.md | 2 +- .planning/STATE.md | 13 +- .planning/phases/04-rbac/04-02-SUMMARY.md | 151 ++++++++++++++++++++++ 4 files changed, 162 insertions(+), 8 deletions(-) create mode 100644 .planning/phases/04-rbac/04-02-SUMMARY.md diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 0eadd0e..4425678 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -53,7 +53,7 @@ Requirements for beta-ready release. Each maps to roadmap phases. - [x] **RBAC-02**: Customer admin role scoped to a single tenant with full control over agents, channels, billing, API keys, and user management - [x] **RBAC-03**: Customer operator role scoped to a single tenant with read-only access to agents, conversations, and usage dashboards - [x] **RBAC-04**: Customer admin can invite users (admin or operator) by email — invitee receives activation link to set password and enable access -- [ ] **RBAC-05**: Portal navigation, pages, and UI elements adapt based on user role (platform admin sees tenant picker, customer admin sees their tenant, operator sees read-only views) +- [x] **RBAC-05**: Portal navigation, pages, and UI elements adapt based on user role (platform admin sees tenant picker, customer admin sees their tenant, operator sees read-only views) - [x] **RBAC-06**: API endpoints enforce role-based authorization — unauthorized actions return 403 Forbidden, not just hidden UI ## v2 Requirements @@ -133,7 +133,7 @@ Which phases cover which requirements. Updated during roadmap creation. | RBAC-02 | Phase 4 | Complete | | RBAC-03 | Phase 4 | Complete | | RBAC-04 | Phase 4 | Complete | -| RBAC-05 | Phase 4 | Pending | +| RBAC-05 | Phase 4 | Complete | | RBAC-06 | Phase 4 | Complete | **Coverage:** diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 87524b1..066465b 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -103,7 +103,7 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 | 1. Foundation | 4/4 | Complete | 2026-03-23 | | 2. Agent Features | 6/6 | Complete | 2026-03-24 | | 3. Operator Experience | 5/5 | Complete | 2026-03-24 | -| 4. RBAC | 1/3 | In Progress| | +| 4. RBAC | 2/3 | In Progress| | --- diff --git a/.planning/STATE.md b/.planning/STATE.md index aad3dcd..168add5 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,14 +3,14 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone status: completed -stopped_at: Completed 04-rbac-01-PLAN.md -last_updated: "2026-03-24T19:57:06.246Z" +stopped_at: Completed 04-rbac-02-PLAN.md +last_updated: "2026-03-24T23:08:36.666Z" last_activity: 2026-03-23 — Completed 03-02 onboarding wizard, Slack OAuth, BYO API keys progress: total_phases: 4 completed_phases: 3 total_plans: 18 - completed_plans: 16 + completed_plans: 17 percent: 100 --- @@ -68,6 +68,7 @@ Progress: [██████████] 100% | Phase 03-operator-experience P04 | 10min | 2 tasks | 8 files | | Phase 03-operator-experience P05 | 2min | 2 tasks | 6 files | | Phase 04-rbac P01 | 8min | 3 tasks | 14 files | +| Phase 04-rbac P02 | 5min | 3 tasks | 10 files | ## Accumulated Context @@ -141,6 +142,8 @@ Recent decisions affecting current work: - [Phase 04-rbac]: SHA-256 hash of raw invite token stored in DB — token_to_hash enables O(1) lookup without exposing token - [Phase 04-rbac]: platform_admin bypasses tenant membership check entirely (no DB query) for simpler, faster guard logic - [Phase 04-rbac]: Celery invite email task dispatched via lazy local import in invitations.py to avoid shared->orchestrator circular dep +- [Phase 04-rbac]: base-ui DialogTrigger uses render prop not asChild — fixes TypeScript error in portal components +- [Phase 04-rbac]: base-ui Select onValueChange typed as (string | null) — filter state setters use ?? '' to coerce null ### Roadmap Evolution @@ -156,6 +159,6 @@ None — all phases complete. ## Session Continuity -Last session: 2026-03-24T19:57:06.244Z -Stopped at: Completed 04-rbac-01-PLAN.md +Last session: 2026-03-24T23:08:36.663Z +Stopped at: Completed 04-rbac-02-PLAN.md Resume file: None diff --git a/.planning/phases/04-rbac/04-02-SUMMARY.md b/.planning/phases/04-rbac/04-02-SUMMARY.md new file mode 100644 index 0000000..e0e4138 --- /dev/null +++ b/.planning/phases/04-rbac/04-02-SUMMARY.md @@ -0,0 +1,151 @@ +--- +phase: 04-rbac +plan: "02" +subsystem: auth +tags: [nextjs, nextauth, jwt, rbac, typescript, react, tanstack-query, shadcn, base-ui] + +requires: + - phase: 04-rbac-01 + provides: JWT auth verify endpoint returning role+tenant_ids, invitation accept endpoint, RBAC guards, portal auth module + +provides: + - Auth.js JWT carries role + tenant_ids + active_tenant_id (replaces is_admin) + - Proxy (proxy.ts) enforces role-based redirects — operators silently redirected from restricted paths + - Invite acceptance page at /invite/[token] (outside dashboard layout, no auth required) + - Role-filtered sidebar nav — restricted items hidden not disabled + - Tenant switcher updates JWT active_tenant_id without page reload + - Impersonation banner with exit button when platform admin views as tenant + - Per-tenant user management page with invite dialog and resend capability + - Platform admin global user management page with cross-tenant table and filters + +affects: [04-rbac-03] + +tech-stack: + added: [] + patterns: + - "base-ui DialogTrigger uses render prop not asChild" + - "base-ui Select onValueChange receives string | null (not string)" + - "Controller from react-hook-form wraps base-ui Select for form integration" + - "zod v4 z.enum() does not accept required_error param" + - "Next.js 15 params is a Promise — unwrap with use() in client components" + +key-files: + created: + - packages/portal/lib/auth-types.ts + - packages/portal/components/tenant-switcher.tsx + - packages/portal/components/impersonation-banner.tsx + - packages/portal/app/invite/[token]/page.tsx + - packages/portal/app/(dashboard)/users/page.tsx + - packages/portal/app/(dashboard)/admin/users/page.tsx + modified: + - packages/portal/lib/auth.ts + - packages/portal/proxy.ts + - packages/portal/components/nav.tsx + - packages/portal/app/(dashboard)/layout.tsx + +key-decisions: + - "base-ui DialogTrigger uses render prop pattern, not asChild — fixes TS error 'asChild does not exist'" + - "base-ui Select onValueChange typed as (string | null) — filter handlers use ?? '' to coerce null" + - "Invite page placed at app/invite/[token]/page.tsx (outside (dashboard) group) so unauthenticated users can access it" + - "zod v4 enum validation drops required_error from params — just z.enum([...]) with no options object" + +patterns-established: + - "Role-based nav filtering: navItems carry allowedRoles array, filtered at render time via useSession" + - "Tenant switcher calls Auth.js update() which triggers jwt callback with trigger=update" + - "Impersonation stored as impersonating_tenant_id in JWT — cleared via update({impersonating_tenant_id: null})" + - "Page-level TanStack Query hooks: useQuery/useMutation defined inline above the page component" + +requirements-completed: + - RBAC-05 + - RBAC-04 + - RBAC-01 + +duration: 5min +completed: 2026-03-24 +--- + +# Phase 4 Plan 02: Portal RBAC Integration Summary + +**Role-based portal with Auth.js JWT carrying role+tenants, operator path restrictions, tenant switcher, impersonation banner, invite acceptance page, and user management pages** + +## Performance + +- **Duration:** ~5 min +- **Started:** 2026-03-24T23:02:53Z +- **Completed:** 2026-03-24T23:07:17Z +- **Tasks:** 3 +- **Files modified:** 9 (4 modified, 5 created from scratch + 3 new pages) + +## Accomplishments + +- JWT now carries role + tenant_ids + active_tenant_id; proxy enforces role-based redirects silently +- Tenant switcher updates active_tenant_id in JWT without page reload, invalidates TanStack Query cache +- Three new pages: invite acceptance (public), per-tenant users, platform admin global users + +## Task Commits + +1. **Tasks 1+2: Auth.js JWT + nav + tenant switcher + impersonation banner** - `fcb1166` (feat) +2. **Task 3: Invite acceptance page + user management pages** - `744cf79` (feat) + +## Files Created/Modified + +- `packages/portal/lib/auth-types.ts` - Module augmentation: role, tenant_ids, active_tenant_id in JWT/Session/User types +- `packages/portal/lib/auth.ts` - JWT callbacks carry role+tenants, trigger=update for tenant switch and impersonation clear +- `packages/portal/proxy.ts` - /invite public, customer_operator restricted from billing/users/admin, role-based landing page +- `packages/portal/components/nav.tsx` - Role-filtered nav with Users and Platform admin items +- `packages/portal/components/tenant-switcher.tsx` - Multi-tenant dropdown, Auth.js update() + queryClient invalidation +- `packages/portal/components/impersonation-banner.tsx` - Fixed amber banner with exit button +- `packages/portal/app/(dashboard)/layout.tsx` - Integrates ImpersonationBanner and TenantSwitcher +- `packages/portal/app/invite/[token]/page.tsx` - Password form, POST /api/portal/invitations/accept, redirect to /login +- `packages/portal/app/(dashboard)/users/page.tsx` - Tenant user list, invite dialog, resend pending invitations +- `packages/portal/app/(dashboard)/admin/users/page.tsx` - Cross-tenant user table, tenant+role filters, invite with tenant selector + +## Decisions Made + +- `base-ui DialogTrigger` uses `render` prop not `asChild` — the shadcn components are base-ui based, not Radix +- `base-ui Select onValueChange` typed as `(string | null)` — filter state setters use `?? ""` to coerce null +- `zod v4` `z.enum()` does not accept `required_error` option — removed from both user pages +- Next.js 15 `params` is a Promise in page components — unwrap with `use(params)` per decision from Phase 3 + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] Fixed base-ui component API incompatibilities** +- **Found during:** Task 3 (TypeScript compilation) +- **Issue:** Code written using Radix/shadcn API conventions (`asChild`, `onValueChange: (string) => void`, `required_error` in zod enum). The portal uses base-ui primitives which have different APIs. +- **Fix:** Replaced `DialogTrigger asChild` with `DialogTrigger render={