Files
konstruct/.planning/phases/04-rbac/04-02-SUMMARY.md
Adolfo Delorenzo e899b14fa7 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
2026-03-24 17:08:50 -06:00

7.4 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed duration completed
04-rbac 02 auth
nextjs
nextauth
jwt
rbac
typescript
react
tanstack-query
shadcn
base-ui
phase provides
04-rbac-01 JWT auth verify endpoint returning role+tenant_ids, invitation accept endpoint, RBAC guards, portal auth module
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
04-rbac-03
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
created modified
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
packages/portal/lib/auth.ts
packages/portal/proxy.ts
packages/portal/components/nav.tsx
packages/portal/app/(dashboard)/layout.tsx
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
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
RBAC-05
RBAC-04
RBAC-01
5min 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)