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
This commit is contained in:
@@ -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:**
|
||||
|
||||
@@ -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| |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
151
.planning/phases/04-rbac/04-02-SUMMARY.md
Normal file
151
.planning/phases/04-rbac/04-02-SUMMARY.md
Normal file
@@ -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={<Button>}`. Wrapped `Select` with `Controller` from react-hook-form. Changed filter `onValueChange` handlers to use `?? ""`. Removed `required_error` from `z.enum()` calls.
|
||||
- **Files modified:** `app/(dashboard)/users/page.tsx`, `app/(dashboard)/admin/users/page.tsx`
|
||||
- **Verification:** `npx tsc --noEmit` passes with zero errors
|
||||
- **Committed in:** `744cf79` (Task 3 commit)
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 1 auto-fixed (Rule 1 - Bug)
|
||||
**Impact on plan:** Auto-fix necessary for TypeScript compilation. No scope creep — same functionality, correct component APIs.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
- Tasks 1 and 2 files (`auth.ts`, `auth-types.ts`, `proxy.ts`, `nav.tsx`, `tenant-switcher.tsx`, `impersonation-banner.tsx`, `layout.tsx`) were already fully implemented from Plan 01 execution. Verified TypeScript clean and committed the staged changes.
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None - no external service configuration required.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
- Portal RBAC is fully wired: JWT contains role+tenants, proxy enforces access, nav filters by role
|
||||
- User management pages exist and are wired to expected API endpoints
|
||||
- Plan 03 needs to implement the backend API endpoints: `GET /api/portal/tenants/{id}/users`, `GET /api/portal/admin/users`, `POST /api/portal/invitations/{id}/resend`
|
||||
- Invite acceptance page wired to `POST /api/portal/invitations/accept` (implemented in Plan 01)
|
||||
|
||||
---
|
||||
*Phase: 04-rbac*
|
||||
*Completed: 2026-03-24*
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- All 9 portal files confirmed present on disk
|
||||
- Commit fcb1166 (Tasks 1+2) confirmed in git log
|
||||
- Commit 744cf79 (Task 3) confirmed in git log
|
||||
- TypeScript compiles clean (zero errors)
|
||||
Reference in New Issue
Block a user