docs(01): create phase plan
Phase 01: Foundation - 5 plan(s) in 4 wave(s) - 2 parallel (wave 2: auth + tenant provisioning), 3 sequential - Ready for execution Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -30,14 +30,14 @@ Decimal phases appear between their surrounding integers in numeric order.
|
||||
3. A user with the Technician role cannot access billing or subscriber management routes — a 403 is returned at the API layer, not just hidden in the UI
|
||||
4. A new ISP tenant can be created; a query issued by Tenant A returns zero rows from Tenant B's data — verified by automated test
|
||||
5. Platform super-admin can log in on a separate auth context and view all tenants without being scoped to any single tenant
|
||||
**Plans**: TBD
|
||||
**Plans**: 5 plans
|
||||
|
||||
Plans:
|
||||
- [ ] 01-01: Docker environment, PostgreSQL with RLS, Redis, project scaffolding (Next.js 15 + TypeScript + Prisma)
|
||||
- [ ] 01-02: Auth module — Auth.js credential login, JWT with tenant_id + role, session persistence, logout
|
||||
- [ ] 01-03: Tenant provisioning — signup flow, tenant resolver, Prisma middleware tenant injection, PostgreSQL RLS policies
|
||||
- [ ] 01-04: RBAC with CASL — permission matrix for all five roles, API-layer enforcement, unauthorized access tests
|
||||
- [ ] 01-05: Super-admin auth context — separate login, tenant management UI (create, suspend, view), unit test harness for core business logic
|
||||
- [ ] 01-01-PLAN.md — Docker environment, PostgreSQL with RLS, Redis, project scaffolding (Next.js 15 + TypeScript + Prisma)
|
||||
- [ ] 01-02-PLAN.md — Auth module: NextAuth credentials login, JWT with tenant_id + roles, session persistence, logout
|
||||
- [ ] 01-03-PLAN.md — Tenant provisioning: signup flow, Prisma tenant middleware, PostgreSQL RLS policies, isolation tests
|
||||
- [ ] 01-04-PLAN.md — RBAC with CASL: permission matrix for all five roles, API-layer enforcement, unauthorized access tests
|
||||
- [ ] 01-05-PLAN.md — Super-admin panel: tenant management UI (list, suspend, activate), comprehensive test harness
|
||||
|
||||
---
|
||||
|
||||
@@ -132,7 +132,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5
|
||||
|
||||
| Phase | Plans Complete | Status | Completed |
|
||||
|-------|----------------|--------|-----------|
|
||||
| 1. Foundation | 0/5 | Not started | - |
|
||||
| 1. Foundation | 0/5 | Planned | - |
|
||||
| 2. Subscriber and Billing Core | 0/5 | Not started | - |
|
||||
| 3. Operational Modules | 0/5 | Not started | - |
|
||||
| 4. Inventory, Expenses, and Financial Reports | 0/5 | Not started | - |
|
||||
|
||||
218
.planning/phases/01-foundation/01-01-PLAN.md
Normal file
218
.planning/phases/01-foundation/01-01-PLAN.md
Normal file
@@ -0,0 +1,218 @@
|
||||
---
|
||||
phase: 01-foundation
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- docker-compose.yml
|
||||
- Dockerfile
|
||||
- .dockerignore
|
||||
- .env.example
|
||||
- .env
|
||||
- package.json
|
||||
- tsconfig.json
|
||||
- next.config.ts
|
||||
- tailwind.config.ts
|
||||
- postcss.config.mjs
|
||||
- prisma/schema.prisma
|
||||
- src/app/layout.tsx
|
||||
- src/app/page.tsx
|
||||
- src/app/globals.css
|
||||
- src/lib/prisma.ts
|
||||
- vitest.config.ts
|
||||
- src/lib/__tests__/setup.test.ts
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Running docker compose up starts PostgreSQL 16, Redis 7, and the Next.js dev server with zero manual steps"
|
||||
- "Prisma can connect to the Dockerized PostgreSQL and run migrations"
|
||||
- "vitest runs and passes at least one smoke test"
|
||||
- "The Next.js app renders at localhost:3000"
|
||||
artifacts:
|
||||
- path: "docker-compose.yml"
|
||||
provides: "PostgreSQL, Redis, and app service definitions"
|
||||
contains: "postgres"
|
||||
- path: "prisma/schema.prisma"
|
||||
provides: "Base Tenant and User models with RLS-ready tenant_id"
|
||||
contains: "model Tenant"
|
||||
- path: "src/lib/prisma.ts"
|
||||
provides: "Singleton Prisma client"
|
||||
exports: ["prisma"]
|
||||
- path: "vitest.config.ts"
|
||||
provides: "Test runner configuration"
|
||||
contains: "vitest"
|
||||
key_links:
|
||||
- from: "docker-compose.yml"
|
||||
to: "prisma/schema.prisma"
|
||||
via: "DATABASE_URL env var"
|
||||
pattern: "DATABASE_URL"
|
||||
- from: "src/lib/prisma.ts"
|
||||
to: "prisma/schema.prisma"
|
||||
via: "PrismaClient import"
|
||||
pattern: "PrismaClient"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Scaffold the entire NetForge project from scratch: Next.js 15 with App Router, TypeScript, Tailwind CSS, Prisma ORM with PostgreSQL, Redis, Docker Compose for local dev, and Vitest for testing.
|
||||
|
||||
Purpose: Every subsequent plan depends on this foundation existing. Nothing can be built without the project scaffold, database connection, and dev environment.
|
||||
Output: A running dockerized development environment with base schema and test infrastructure.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@C:\Users\KevinAsprec\.claude/get-shit-done/workflows/execute-plan.md
|
||||
@C:\Users\KevinAsprec\.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/01-foundation/01-CONTEXT.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create Next.js project with Docker Compose dev environment</name>
|
||||
<files>
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
.env.example
|
||||
.env
|
||||
package.json
|
||||
tsconfig.json
|
||||
next.config.ts
|
||||
tailwind.config.ts
|
||||
postcss.config.mjs
|
||||
src/app/layout.tsx
|
||||
src/app/page.tsx
|
||||
src/app/globals.css
|
||||
</files>
|
||||
<action>
|
||||
Initialize a Next.js 15 project with App Router, TypeScript, and Tailwind CSS using `npx create-next-app@latest . --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"` (answer yes to all prompts or use flags to avoid interactive mode).
|
||||
|
||||
Create `docker-compose.yml` with three services:
|
||||
1. **db**: PostgreSQL 16 image, port 5432, volume for data persistence, POSTGRES_USER=netforge, POSTGRES_PASSWORD=netforge_dev, POSTGRES_DB=netforge_dev
|
||||
2. **redis**: Redis 7 image, port 6379
|
||||
3. **app**: Builds from Dockerfile, port 3000, depends_on db and redis, mounts source code as volume for hot reload, environment variables from .env
|
||||
|
||||
Create a `Dockerfile` for development (Node 20 alpine, installs deps, runs `npm run dev`).
|
||||
|
||||
Create `.env.example` and `.env` with:
|
||||
- DATABASE_URL=postgresql://netforge:netforge_dev@db:5432/netforge_dev
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- NEXTAUTH_SECRET=dev-secret-change-in-production
|
||||
- NEXTAUTH_URL=http://localhost:3000
|
||||
|
||||
Create `.dockerignore` excluding node_modules, .next, .git.
|
||||
|
||||
Update `src/app/page.tsx` to render a simple "NetForge" heading so we can verify the app loads.
|
||||
|
||||
IMPORTANT: Also create a local DATABASE_URL variant in .env for running Prisma commands from the host (outside Docker):
|
||||
- DATABASE_URL_LOCAL=postgresql://netforge:netforge_dev@localhost:5432/netforge_dev
|
||||
Add a note in .env.example explaining when to use each.
|
||||
</action>
|
||||
<verify>
|
||||
Run `docker compose up -d` and verify all three services start. Run `docker compose ps` to confirm all services are healthy/running. Visit http://localhost:3000 and confirm the page loads.
|
||||
</verify>
|
||||
<done>All three Docker services (db, redis, app) start successfully. Next.js app serves at localhost:3000.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Prisma schema with base models and Vitest setup</name>
|
||||
<files>
|
||||
prisma/schema.prisma
|
||||
src/lib/prisma.ts
|
||||
vitest.config.ts
|
||||
src/lib/__tests__/setup.test.ts
|
||||
package.json
|
||||
</files>
|
||||
<action>
|
||||
Install Prisma: `npm install prisma @prisma/client` and `npm install -D vitest @vitejs/plugin-react`.
|
||||
|
||||
Create `prisma/schema.prisma` with PostgreSQL provider and the following base models:
|
||||
|
||||
**Tenant** model:
|
||||
- id: String @id @default(uuid())
|
||||
- name: String (business name)
|
||||
- slug: String @unique (URL-friendly identifier, auto-generated from name)
|
||||
- ownerEmail: String
|
||||
- status: TenantStatus enum (ACTIVE, SUSPENDED, PENDING_SUSPENSION)
|
||||
- suspendedAt: DateTime? (when suspension was triggered)
|
||||
- gracePeriodEndsAt: DateTime? (when service actually stops — 7 day grace period)
|
||||
- createdAt: DateTime @default(now())
|
||||
- updatedAt: DateTime @updatedAt
|
||||
- users: User[]
|
||||
|
||||
**User** model:
|
||||
- id: String @id @default(uuid())
|
||||
- email: String
|
||||
- passwordHash: String
|
||||
- firstName: String
|
||||
- lastName: String
|
||||
- tenantId: String? (nullable for super-admins who are not tenant-scoped)
|
||||
- tenant: Tenant? @relation
|
||||
- roles: Role[] (array of enum values — multi-role support)
|
||||
- isActive: Boolean @default(true)
|
||||
- isSuperAdmin: Boolean @default(false)
|
||||
- createdAt: DateTime @default(now())
|
||||
- updatedAt: DateTime @updatedAt
|
||||
- @@unique([email, tenantId]) — email unique within tenant
|
||||
- @@index([tenantId])
|
||||
|
||||
**Role** enum: ADMIN, OFFICE_STAFF, COLLECTOR, TECHNICIAN, CLIENT
|
||||
|
||||
**TenantStatus** enum: ACTIVE, SUSPENDED, PENDING_SUSPENSION
|
||||
|
||||
IMPORTANT: Add a comment block at the top of schema.prisma noting that tenant_id is included on all tenant-scoped models for RLS. Future models (Subscriber, Invoice, etc.) MUST include tenantId.
|
||||
|
||||
Create `src/lib/prisma.ts` — singleton PrismaClient pattern (check for existing instance on globalThis in dev to avoid hot-reload connection exhaustion).
|
||||
|
||||
Create `vitest.config.ts` with:
|
||||
- resolve alias for @/* pointing to src/*
|
||||
- test.globals: true
|
||||
- test.environment: 'node'
|
||||
|
||||
Create `src/lib/__tests__/setup.test.ts` — a smoke test that imports prisma client and asserts it's defined (does NOT need DB connection, just verifies the import chain works).
|
||||
|
||||
Run `npx prisma generate` to generate the Prisma client.
|
||||
Run `npx prisma db push` to sync the schema to the Dockerized PostgreSQL (use DATABASE_URL_LOCAL if running from host, or exec into app container).
|
||||
|
||||
Add to package.json scripts:
|
||||
- "test": "vitest run"
|
||||
- "test:watch": "vitest"
|
||||
- "db:push": "prisma db push"
|
||||
- "db:generate": "prisma generate"
|
||||
- "db:studio": "prisma studio"
|
||||
</action>
|
||||
<verify>
|
||||
Run `npx vitest run` — the smoke test passes. Run `npx prisma db push` — schema syncs without errors. Run `npx prisma studio` briefly to confirm Tenant and User tables exist in the database.
|
||||
</verify>
|
||||
<done>Prisma schema with Tenant and User models is synced to PostgreSQL. Vitest runs and passes the smoke test. PrismaClient singleton is importable from @/lib/prisma.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. `docker compose up -d` starts all services without errors
|
||||
2. `docker compose ps` shows db, redis, and app running
|
||||
3. http://localhost:3000 renders the Next.js app
|
||||
4. `npx prisma db push` succeeds (schema matches DB)
|
||||
5. `npx vitest run` passes all tests
|
||||
6. `npx prisma studio` shows Tenant and User tables
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Docker Compose starts PostgreSQL 16, Redis 7, and Next.js 15 dev server with one command
|
||||
- Prisma schema has Tenant and User models with RLS-ready tenantId field
|
||||
- Vitest is configured and runs at least one passing test
|
||||
- All environment variables are documented in .env.example
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-foundation/01-01-SUMMARY.md`
|
||||
</output>
|
||||
197
.planning/phases/01-foundation/01-02-PLAN.md
Normal file
197
.planning/phases/01-foundation/01-02-PLAN.md
Normal file
@@ -0,0 +1,197 @@
|
||||
---
|
||||
phase: 01-foundation
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["01-01"]
|
||||
files_modified:
|
||||
- package.json
|
||||
- src/lib/auth.ts
|
||||
- src/lib/auth-options.ts
|
||||
- src/app/api/auth/[...nextauth]/route.ts
|
||||
- src/app/(auth)/login/page.tsx
|
||||
- src/app/(auth)/layout.tsx
|
||||
- src/middleware.ts
|
||||
- src/types/next-auth.d.ts
|
||||
- src/lib/__tests__/auth.test.ts
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "A user can log in with email and password and receive a valid session"
|
||||
- "A logged-in user stays logged in after browser refresh"
|
||||
- "Logging out ends the session and redirects to login"
|
||||
- "The JWT contains tenantId and roles for downstream use"
|
||||
- "Unauthenticated users are redirected to the login page"
|
||||
artifacts:
|
||||
- path: "src/lib/auth-options.ts"
|
||||
provides: "NextAuth configuration with credentials provider"
|
||||
exports: ["authOptions"]
|
||||
- path: "src/app/api/auth/[...nextauth]/route.ts"
|
||||
provides: "NextAuth API route handler"
|
||||
exports: ["GET", "POST"]
|
||||
- path: "src/app/(auth)/login/page.tsx"
|
||||
provides: "Login form UI"
|
||||
min_lines: 40
|
||||
- path: "src/middleware.ts"
|
||||
provides: "Route protection middleware"
|
||||
contains: "middleware"
|
||||
- path: "src/types/next-auth.d.ts"
|
||||
provides: "Extended session/JWT types with tenantId and roles"
|
||||
contains: "tenantId"
|
||||
key_links:
|
||||
- from: "src/lib/auth-options.ts"
|
||||
to: "prisma.user"
|
||||
via: "credential validation query"
|
||||
pattern: "prisma\\.user\\.findFirst"
|
||||
- from: "src/lib/auth-options.ts"
|
||||
to: "src/types/next-auth.d.ts"
|
||||
via: "JWT callback populates tenantId and roles"
|
||||
pattern: "token\\.tenantId"
|
||||
- from: "src/middleware.ts"
|
||||
to: "src/lib/auth-options.ts"
|
||||
via: "getToken checks authentication"
|
||||
pattern: "getToken|withAuth"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Implement email/password authentication using NextAuth.js (Auth.js) with the credentials provider, JWT sessions carrying tenantId and roles, login/logout UI, and route protection middleware.
|
||||
|
||||
Purpose: Every protected feature in the app depends on knowing WHO the user is and WHICH tenant they belong to. Auth is the gateway.
|
||||
Output: Working login/logout flow with JWT-based sessions that persist across refresh.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@C:\Users\KevinAsprec\.claude/get-shit-done/workflows/execute-plan.md
|
||||
@C:\Users\KevinAsprec\.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/phases/01-foundation/01-CONTEXT.md
|
||||
@.planning/phases/01-foundation/01-01-SUMMARY.md
|
||||
@prisma/schema.prisma
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: NextAuth.js configuration with credentials provider and JWT</name>
|
||||
<files>
|
||||
package.json
|
||||
src/lib/auth-options.ts
|
||||
src/lib/auth.ts
|
||||
src/app/api/auth/[...nextauth]/route.ts
|
||||
src/types/next-auth.d.ts
|
||||
src/middleware.ts
|
||||
</files>
|
||||
<action>
|
||||
Install dependencies: `npm install next-auth@4 bcryptjs` and `npm install -D @types/bcryptjs`.
|
||||
|
||||
Use NextAuth.js v4 (not v5 — v5/Auth.js is still beta-ish for credentials provider with custom JWT). This is a deliberate choice for stability.
|
||||
|
||||
Create `src/types/next-auth.d.ts` to extend NextAuth types:
|
||||
- Session.user includes: id, email, tenantId (string | null), roles (Role[]), isSuperAdmin (boolean), firstName, lastName
|
||||
- JWT includes same fields
|
||||
|
||||
Create `src/lib/auth-options.ts` exporting `authOptions: NextAuthOptions`:
|
||||
- CredentialsProvider with email and password fields
|
||||
- In authorize(): look up user by email using prisma.user.findFirst where email matches AND (tenantId matches an ACTIVE tenant OR user.isSuperAdmin is true). Use bcryptjs.compare for password verification.
|
||||
- If user found and password valid, return user object with id, email, tenantId, roles, isSuperAdmin, firstName, lastName
|
||||
- If not found or password invalid, return null (NextAuth shows error)
|
||||
- JWT callback: persist tenantId, roles, isSuperAdmin, firstName, lastName into token
|
||||
- Session callback: expose tenantId, roles, isSuperAdmin, firstName, lastName on session.user
|
||||
- Session strategy: "jwt"
|
||||
- JWT maxAge: 24 hours (session duration)
|
||||
- Pages: signIn: "/login"
|
||||
|
||||
Create `src/lib/auth.ts` exporting a helper `getServerSession()` wrapper and a `getCurrentUser()` helper that returns the typed session user or null.
|
||||
|
||||
Create `src/app/api/auth/[...nextauth]/route.ts` — standard NextAuth route handler exporting GET and POST.
|
||||
|
||||
Create `src/middleware.ts`:
|
||||
- Use NextAuth's `withAuth` or `getToken` to check for valid JWT
|
||||
- Protect all routes EXCEPT: /login, /signup, /api/auth/*, /_next/*, /favicon.ico, static assets
|
||||
- If no valid token, redirect to /login
|
||||
- If valid token, allow request to continue
|
||||
</action>
|
||||
<verify>
|
||||
Build check: `npx next build` completes without type errors. The auth route is accessible at /api/auth/providers (returns JSON with credentials provider listed).
|
||||
</verify>
|
||||
<done>NextAuth configured with credentials provider. JWT contains tenantId and roles. Middleware redirects unauthenticated users to /login. TypeScript types are extended for session.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Login page UI, logout flow, and seed script for testing</name>
|
||||
<files>
|
||||
src/app/(auth)/login/page.tsx
|
||||
src/app/(auth)/layout.tsx
|
||||
prisma/seed.ts
|
||||
package.json
|
||||
src/lib/__tests__/auth.test.ts
|
||||
</files>
|
||||
<action>
|
||||
Create `src/app/(auth)/layout.tsx` — a centered layout for auth pages (no sidebar/nav, just centered card on neutral background). Use Tailwind.
|
||||
|
||||
Create `src/app/(auth)/login/page.tsx`:
|
||||
- Client component ("use client")
|
||||
- Form with email and password fields, submit button
|
||||
- Uses `signIn("credentials", { email, password, redirect: false })` from next-auth/react
|
||||
- On success: redirect to /dashboard (or / for now)
|
||||
- On error: show error message below form (red text, "Invalid email or password")
|
||||
- Loading state on submit button
|
||||
- Clean, minimal design with Tailwind — no heavy UI library needed
|
||||
- Page title: "Sign in to NetForge"
|
||||
- Include a "Sign up" link pointing to /signup (page will be created in plan 01-03)
|
||||
|
||||
Add logout functionality:
|
||||
- Create a simple header component at `src/components/layout/header.tsx` that shows the user's name and a "Sign out" button
|
||||
- Sign out button calls `signOut({ callbackUrl: "/login" })` from next-auth/react
|
||||
- Include this header in the main app layout (not the auth layout)
|
||||
- Create `src/app/(dashboard)/layout.tsx` as the authenticated layout that includes the header
|
||||
|
||||
Create `prisma/seed.ts`:
|
||||
- Creates a test tenant: "Demo ISP", status ACTIVE
|
||||
- Creates an admin user: admin@demo.com / password "admin123" (bcrypt hashed), role ADMIN, linked to demo tenant
|
||||
- Creates a super-admin user: superadmin@netforge.com / password "super123" (bcrypt hashed), isSuperAdmin true, no tenantId
|
||||
- Uses upsert to be idempotent
|
||||
- Add "prisma.seed" config to package.json pointing to "ts-node prisma/seed.ts" (or use tsx: `npx tsx prisma/seed.ts`)
|
||||
- Add script: "db:seed": "npx tsx prisma/seed.ts"
|
||||
|
||||
Create `src/lib/__tests__/auth.test.ts`:
|
||||
- Test that authOptions has credentials provider configured
|
||||
- Test that JWT callback adds tenantId and roles to token (mock the callback)
|
||||
- Test that session callback exposes tenantId and roles on session
|
||||
- These are unit tests of the auth config, NOT integration tests requiring a running server
|
||||
|
||||
Run the seed script to populate test data.
|
||||
</action>
|
||||
<verify>
|
||||
Run `npm run db:seed` — creates test users without errors. Run `npx vitest run` — auth tests pass. Start the app, navigate to /login, log in with admin@demo.com / admin123 — redirects to dashboard. Click sign out — redirects to /login. Refresh after login — session persists.
|
||||
</verify>
|
||||
<done>Login page renders with email/password form. Seed data creates demo tenant + admin + super-admin. Login with valid credentials works and session persists across refresh. Logout redirects to login. Auth unit tests pass.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. /login page renders a clean login form
|
||||
2. Login with admin@demo.com / admin123 succeeds and redirects to authenticated area
|
||||
3. Session persists after browser refresh (no re-login required)
|
||||
4. Sign out button ends session and redirects to /login
|
||||
5. Visiting a protected route while logged out redirects to /login
|
||||
6. `npx vitest run` passes all auth tests
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Email/password authentication works end-to-end (AUTH-01)
|
||||
- JWT contains tenantId and roles for downstream RBAC and tenant scoping
|
||||
- Session persists across browser refresh (AUTH-04)
|
||||
- Unauthenticated users cannot access protected routes
|
||||
- Seed data provides test accounts for development
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-foundation/01-02-SUMMARY.md`
|
||||
</output>
|
||||
206
.planning/phases/01-foundation/01-03-PLAN.md
Normal file
206
.planning/phases/01-foundation/01-03-PLAN.md
Normal file
@@ -0,0 +1,206 @@
|
||||
---
|
||||
phase: 01-foundation
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["01-01"]
|
||||
files_modified:
|
||||
- src/app/(auth)/signup/page.tsx
|
||||
- src/app/api/tenants/signup/route.ts
|
||||
- src/lib/tenant.ts
|
||||
- prisma/schema.prisma
|
||||
- prisma/migrations/
|
||||
- src/lib/prisma-tenant.ts
|
||||
- src/lib/__tests__/tenant-isolation.test.ts
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "A new ISP can sign up and get their own isolated tenant space"
|
||||
- "A query by Tenant A returns zero rows from Tenant B's data"
|
||||
- "Every database query for tenant-scoped data automatically includes the tenant filter"
|
||||
- "PostgreSQL RLS policies enforce isolation even if application code is bypassed"
|
||||
artifacts:
|
||||
- path: "src/app/api/tenants/signup/route.ts"
|
||||
provides: "Tenant signup API endpoint"
|
||||
exports: ["POST"]
|
||||
- path: "src/app/(auth)/signup/page.tsx"
|
||||
provides: "Tenant signup form UI"
|
||||
min_lines: 50
|
||||
- path: "src/lib/prisma-tenant.ts"
|
||||
provides: "Tenant-scoped Prisma client with automatic tenant filtering"
|
||||
exports: ["createTenantPrisma", "withTenantContext"]
|
||||
- path: "src/lib/tenant.ts"
|
||||
provides: "Tenant creation and management service"
|
||||
exports: ["createTenant"]
|
||||
- path: "src/lib/__tests__/tenant-isolation.test.ts"
|
||||
provides: "Cross-tenant data leakage tests"
|
||||
min_lines: 30
|
||||
key_links:
|
||||
- from: "src/app/api/tenants/signup/route.ts"
|
||||
to: "src/lib/tenant.ts"
|
||||
via: "createTenant function call"
|
||||
pattern: "createTenant"
|
||||
- from: "src/lib/prisma-tenant.ts"
|
||||
to: "prisma/schema.prisma"
|
||||
via: "Prisma middleware injects tenantId filter"
|
||||
pattern: "tenantId"
|
||||
- from: "src/lib/__tests__/tenant-isolation.test.ts"
|
||||
to: "src/lib/prisma-tenant.ts"
|
||||
via: "Tests verify tenant scoping"
|
||||
pattern: "tenantId.*Tenant"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Implement tenant provisioning: signup flow that creates a new ISP tenant with its admin user, Prisma middleware that automatically injects tenant filtering on all queries, and PostgreSQL Row-Level Security policies as a defense-in-depth layer. Prove zero cross-tenant data leakage with automated tests.
|
||||
|
||||
Purpose: Multi-tenancy is the architectural foundation. Every feature built on top must be tenant-isolated. Getting this wrong means data leaks between ISPs.
|
||||
Output: Working tenant signup, automatic tenant scoping on all queries, RLS policies, and isolation tests.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@C:\Users\KevinAsprec\.claude/get-shit-done/workflows/execute-plan.md
|
||||
@C:\Users\KevinAsprec\.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/phases/01-foundation/01-CONTEXT.md
|
||||
@.planning/phases/01-foundation/01-01-SUMMARY.md
|
||||
@prisma/schema.prisma
|
||||
@src/lib/prisma.ts
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Tenant signup API, tenant service, and signup UI</name>
|
||||
<files>
|
||||
src/lib/tenant.ts
|
||||
src/app/api/tenants/signup/route.ts
|
||||
src/app/(auth)/signup/page.tsx
|
||||
prisma/schema.prisma
|
||||
</files>
|
||||
<action>
|
||||
Create `src/lib/tenant.ts` with a `createTenant()` function:
|
||||
- Accepts: businessName, ownerFirstName, ownerLastName, ownerEmail, password, businessAddress, contactPhone
|
||||
- Validates: email not already used, businessName not empty, password min 8 chars
|
||||
- In a Prisma transaction ($transaction):
|
||||
1. Create Tenant record (name=businessName, slug=slugify(businessName), status=ACTIVE, ownerEmail)
|
||||
2. Create User record (email=ownerEmail, passwordHash=bcrypt(password, 12), firstName, lastName, tenantId=new tenant id, roles=[ADMIN], isActive=true)
|
||||
- Returns: { tenant, user } (without passwordHash)
|
||||
- Throws descriptive errors for validation failures
|
||||
|
||||
Add to prisma/schema.prisma if not present:
|
||||
- businessAddress: String? on Tenant
|
||||
- contactPhone: String? on Tenant
|
||||
|
||||
Run `npx prisma db push` after schema changes.
|
||||
|
||||
Create `src/app/api/tenants/signup/route.ts`:
|
||||
- POST handler accepting JSON body with all signup fields
|
||||
- Calls createTenant()
|
||||
- Returns 201 with { tenant: { id, name, slug }, user: { id, email } }
|
||||
- Returns 400 for validation errors with { error: string }
|
||||
- Returns 409 if email already exists
|
||||
|
||||
Create `src/app/(auth)/signup/page.tsx`:
|
||||
- Client component ("use client")
|
||||
- Form fields: Business Name, First Name, Last Name, Email, Password, Confirm Password, Business Address (optional), Contact Phone (optional)
|
||||
- Client-side validation: passwords match, email format, required fields
|
||||
- On submit: POST to /api/tenants/signup
|
||||
- On success: redirect to /login with a success message (use query param ?registered=true, login page shows "Account created, please sign in")
|
||||
- On error: show error message
|
||||
- Link to /login: "Already have an account? Sign in"
|
||||
- Clean Tailwind design matching the login page style
|
||||
</action>
|
||||
<verify>
|
||||
Start the app. Navigate to /signup. Fill out the form and submit. Verify the API returns 201. Check the database has the new tenant and user. Navigate to /login and sign in with the new credentials.
|
||||
</verify>
|
||||
<done>New ISP can sign up via /signup form, which creates tenant + admin user in a transaction. User can immediately log in after signup.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Prisma tenant middleware, PostgreSQL RLS, and isolation tests</name>
|
||||
<files>
|
||||
src/lib/prisma-tenant.ts
|
||||
prisma/migrations/
|
||||
src/lib/__tests__/tenant-isolation.test.ts
|
||||
</files>
|
||||
<action>
|
||||
Create `src/lib/prisma-tenant.ts`:
|
||||
- Export a `withTenantContext(tenantId: string)` function that returns a Prisma client extended with middleware
|
||||
- The middleware intercepts all find/findMany/create/update/delete operations on tenant-scoped models
|
||||
- For reads (findMany, findFirst, findUnique): automatically inject `where: { tenantId }` filter
|
||||
- For creates: automatically set `tenantId` on the data
|
||||
- For updates/deletes: automatically add `tenantId` to the where clause
|
||||
- Maintain a list of tenant-scoped models (for now: User — more will be added in later phases). Models NOT in this list (like Tenant itself) are not filtered.
|
||||
- Export `TENANT_SCOPED_MODELS` constant array so it can be extended as new models are added
|
||||
|
||||
IMPORTANT: Use Prisma's `$extends` with query extensions (not the deprecated middleware API). This is the modern approach:
|
||||
```typescript
|
||||
prisma.$extends({
|
||||
query: {
|
||||
user: {
|
||||
async findMany({ args, query }) {
|
||||
args.where = { ...args.where, tenantId };
|
||||
return query(args);
|
||||
},
|
||||
// ... same for findFirst, create, update, delete, etc.
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Create PostgreSQL RLS policies as a defense-in-depth layer. Create a raw SQL migration:
|
||||
- `npx prisma migrate dev --name add-rls-policies --create-only` to create empty migration
|
||||
- Add SQL to enable RLS on the User table (and any future tenant-scoped tables):
|
||||
```sql
|
||||
ALTER TABLE "User" ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY tenant_isolation_user ON "User"
|
||||
USING ("tenantId" = current_setting('app.current_tenant_id', true)::text)
|
||||
WITH CHECK ("tenantId" = current_setting('app.current_tenant_id', true)::text);
|
||||
```
|
||||
- Note: The Prisma client connects as the DB owner so RLS is bypassed by default. Add a comment explaining that RLS is defense-in-depth — the Prisma middleware is the primary enforcement, RLS catches bugs.
|
||||
- For RLS to actually enforce, queries would need to SET app.current_tenant_id before each request. Add a helper function `setTenantRLS(prisma, tenantId)` that runs `SET LOCAL app.current_tenant_id = 'tenantId'` inside a transaction.
|
||||
|
||||
Create `src/lib/__tests__/tenant-isolation.test.ts`:
|
||||
- These tests require a real database connection (mark with a describe block or test tag)
|
||||
- Setup: Create two tenants (Tenant A, Tenant B) with one user each
|
||||
- Test 1: Query users with Tenant A context — returns only Tenant A's user, zero rows from Tenant B
|
||||
- Test 2: Query users with Tenant B context — returns only Tenant B's user, zero rows from Tenant A
|
||||
- Test 3: Creating a user with Tenant A context automatically sets tenantId to Tenant A
|
||||
- Test 4: Attempting to read Tenant B's user by ID with Tenant A context returns null (not found)
|
||||
- Teardown: Clean up test data after tests
|
||||
|
||||
Add a vitest setup file or test helper that provides database connection for integration tests. Use a test-specific database or transaction rollback pattern to keep tests isolated.
|
||||
</action>
|
||||
<verify>
|
||||
Run `npx prisma migrate dev` — migration applies successfully. Run `npx vitest run` — all tenant isolation tests pass. Specifically verify Test 1 and Test 2 return exactly the right number of rows (1 each, 0 cross-tenant).
|
||||
</verify>
|
||||
<done>Prisma tenant middleware automatically filters all queries by tenantId. PostgreSQL RLS policies exist as defense-in-depth. Automated tests prove zero cross-tenant data leakage (TENANT-01).</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. /signup form creates a new tenant and admin user
|
||||
2. New user can log in immediately after signup
|
||||
3. Prisma middleware automatically scopes queries to the current tenant
|
||||
4. Tenant isolation tests pass — zero cross-tenant data leakage
|
||||
5. RLS policies are applied to the User table in PostgreSQL
|
||||
6. `npx vitest run` passes all tests including isolation tests
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- New ISP can sign up and onboard (TENANT-02)
|
||||
- Data is fully isolated per tenant at application layer via Prisma middleware
|
||||
- PostgreSQL RLS policies provide defense-in-depth isolation (TENANT-01)
|
||||
- Automated test proves cross-tenant query returns zero rows
|
||||
- One email per tenant constraint is enforced
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-foundation/01-03-SUMMARY.md`
|
||||
</output>
|
||||
250
.planning/phases/01-foundation/01-04-PLAN.md
Normal file
250
.planning/phases/01-foundation/01-04-PLAN.md
Normal file
@@ -0,0 +1,250 @@
|
||||
---
|
||||
phase: 01-foundation
|
||||
plan: 04
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: ["01-02", "01-03"]
|
||||
files_modified:
|
||||
- package.json
|
||||
- src/lib/casl/ability.ts
|
||||
- src/lib/casl/permissions.ts
|
||||
- src/lib/casl/types.ts
|
||||
- src/lib/middleware/authorize.ts
|
||||
- src/app/api/test-rbac/route.ts
|
||||
- src/lib/__tests__/rbac.test.ts
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "A Technician cannot access billing or subscriber management routes and receives a 403"
|
||||
- "A Collector can view subscribers but cannot modify subscribers outside their zone"
|
||||
- "A Client can only see their own account data"
|
||||
- "An Admin has full access within their tenant"
|
||||
- "A user with multiple roles gets the union of all permissions"
|
||||
artifacts:
|
||||
- path: "src/lib/casl/ability.ts"
|
||||
provides: "CASL ability factory that builds permissions from user roles"
|
||||
exports: ["defineAbilityFor"]
|
||||
- path: "src/lib/casl/permissions.ts"
|
||||
provides: "Permission matrix for all five roles"
|
||||
contains: "ADMIN"
|
||||
- path: "src/lib/middleware/authorize.ts"
|
||||
provides: "API route authorization wrapper"
|
||||
exports: ["authorize", "withPermission"]
|
||||
- path: "src/lib/__tests__/rbac.test.ts"
|
||||
provides: "RBAC permission tests for all roles"
|
||||
min_lines: 80
|
||||
key_links:
|
||||
- from: "src/lib/casl/ability.ts"
|
||||
to: "src/lib/casl/permissions.ts"
|
||||
via: "Reads role permission definitions"
|
||||
pattern: "defineAbilityFor"
|
||||
- from: "src/lib/middleware/authorize.ts"
|
||||
to: "src/lib/casl/ability.ts"
|
||||
via: "Builds ability from session user and checks permission"
|
||||
pattern: "defineAbilityFor.*can\\("
|
||||
- from: "src/lib/middleware/authorize.ts"
|
||||
to: "src/lib/auth.ts"
|
||||
via: "Gets current user session for authorization"
|
||||
pattern: "getCurrentUser|getServerSession"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Implement role-based access control using CASL.js with a permission matrix for all five roles (Admin, Office Staff, Collector, Technician, Client), an API-layer authorization middleware that returns 403 for unauthorized access, and comprehensive tests proving each role's boundaries.
|
||||
|
||||
Purpose: Without RBAC, any authenticated user can access everything. This plan enforces the principle of least privilege — a Technician sees only their jobs, a Client sees only their account.
|
||||
Output: Working CASL permission system, API authorization middleware, and tests for all role boundaries.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@C:\Users\KevinAsprec\.claude/get-shit-done/workflows/execute-plan.md
|
||||
@C:\Users\KevinAsprec\.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/phases/01-foundation/01-CONTEXT.md
|
||||
@.planning/phases/01-foundation/01-02-SUMMARY.md
|
||||
@.planning/phases/01-foundation/01-03-SUMMARY.md
|
||||
@src/lib/auth-options.ts
|
||||
@src/types/next-auth.d.ts
|
||||
@prisma/schema.prisma
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: CASL permission definitions and ability factory</name>
|
||||
<files>
|
||||
package.json
|
||||
src/lib/casl/types.ts
|
||||
src/lib/casl/permissions.ts
|
||||
src/lib/casl/ability.ts
|
||||
</files>
|
||||
<action>
|
||||
Install CASL: `npm install @casl/ability`.
|
||||
|
||||
Create `src/lib/casl/types.ts`:
|
||||
- Define AppAbility type using CASL's PureAbility
|
||||
- Define subjects: "Tenant", "User", "Subscriber", "Invoice", "Payment", "Ticket", "JobOrder", "Inventory", "Expense", "Account", "Report", "all"
|
||||
- Define actions: "create", "read", "update", "delete", "manage" (manage = all actions)
|
||||
- Export AppAbility and AppSubjects types
|
||||
- Note: Most subjects (Subscriber, Invoice, etc.) don't have models yet — we're defining the permission structure now so it's ready when those models are created in later phases
|
||||
|
||||
Create `src/lib/casl/permissions.ts`:
|
||||
- Export a function `definePermissionsFor(role: Role, userId: string, tenantId: string)` that returns an array of CASL permission rules
|
||||
- Permission matrix based on CONTEXT.md decisions:
|
||||
|
||||
**ADMIN**: can("manage", "all") — full access within tenant
|
||||
|
||||
**OFFICE_STAFF**:
|
||||
- can("manage", "User") — create/manage users, assign roles
|
||||
- can("manage", "Subscriber") — full subscriber management
|
||||
- can("manage", "Invoice") — billing management
|
||||
- can("manage", "Payment") — record payments
|
||||
- can("manage", "Ticket") — ticketing
|
||||
- can("manage", "JobOrder") — job order management
|
||||
- can("read", "Report") — view financial reports
|
||||
- can("read", "Account") — view accounting (not modify)
|
||||
- cannot("create", "Account") — cannot modify Chart of Accounts
|
||||
- cannot("update", "Account")
|
||||
- cannot("delete", "Account")
|
||||
|
||||
**COLLECTOR**:
|
||||
- can("read", "Subscriber") — can view all subscribers (context for work)
|
||||
- can("create", "Payment") — can record payments (zone filtering done at data layer, not CASL)
|
||||
- can("read", "Payment") — view payment history
|
||||
- cannot("manage", "Invoice") — no billing access
|
||||
- cannot("manage", "User") — no user management
|
||||
- cannot("manage", "Report") — no report access
|
||||
|
||||
**TECHNICIAN**:
|
||||
- can("read", "JobOrder", { assignedToId: userId }) — only their assigned jobs
|
||||
- can("update", "JobOrder", { assignedToId: userId }) — update their own jobs
|
||||
- can("read", "Subscriber") — limited: only contact info for assigned job subscribers (enforced at data layer)
|
||||
- can("read", "Inventory", { assignedToId: userId }) — only inventory checked out to them
|
||||
- cannot("manage", "Invoice")
|
||||
- cannot("manage", "Payment")
|
||||
- cannot("manage", "Subscriber") — no subscriber management
|
||||
- cannot("manage", "Report")
|
||||
|
||||
**CLIENT**:
|
||||
- can("read", "Invoice", { subscriberId: userId }) — only their own invoices
|
||||
- can("read", "Payment", { subscriberId: userId }) — only their own payments
|
||||
- can("read", "Subscriber", { id: userId }) — only their own profile
|
||||
- can("create", "Ticket") — submit tickets
|
||||
- can("read", "Ticket", { submittedById: userId }) — only their own tickets
|
||||
- cannot("manage", "User")
|
||||
- cannot("manage", "Report")
|
||||
|
||||
Create `src/lib/casl/ability.ts`:
|
||||
- Export `defineAbilityFor(user: { id: string, roles: Role[], tenantId: string | null, isSuperAdmin: boolean })`
|
||||
- If isSuperAdmin: can("manage", "all") — unrestricted
|
||||
- Otherwise: iterate over user.roles, call definePermissionsFor for each role, merge all rules (union of permissions for multi-role users)
|
||||
- Return the built CASL Ability instance
|
||||
- Export the Ability type for use in components/routes
|
||||
</action>
|
||||
<verify>
|
||||
TypeScript compiles without errors. Import defineAbilityFor and verify it returns an Ability instance for each role.
|
||||
</verify>
|
||||
<done>CASL permission matrix defined for all 5 roles plus super-admin. Multi-role users get union of permissions. Ability factory builds correct permissions from user session data.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: API authorization middleware and RBAC tests</name>
|
||||
<files>
|
||||
src/lib/middleware/authorize.ts
|
||||
src/lib/__tests__/rbac.test.ts
|
||||
</files>
|
||||
<action>
|
||||
Create `src/lib/middleware/authorize.ts`:
|
||||
- Export `withPermission(action: string, subject: string)` — a higher-order function that wraps a Next.js API route handler
|
||||
- Flow:
|
||||
1. Get current user session via getCurrentUser() from auth.ts
|
||||
2. If no session: return 401 { error: "Unauthorized" }
|
||||
3. Build CASL ability using defineAbilityFor(session.user)
|
||||
4. Check ability.can(action, subject)
|
||||
5. If cannot: return 403 { error: "Forbidden" }
|
||||
6. If can: call the wrapped handler, passing the ability and user in context
|
||||
- Export `authorize(handler, action, subject)` as an alternative API for convenience
|
||||
- The wrapped handler receives (req, { user, ability }) so handlers can do fine-grained checks internally
|
||||
|
||||
Example usage (add as JSDoc comment):
|
||||
```typescript
|
||||
// In route.ts:
|
||||
export const GET = withPermission("read", "Subscriber")(async (req, { user, ability }) => {
|
||||
// ability is available for fine-grained checks within the handler
|
||||
const subscribers = await getSubscribers(user.tenantId);
|
||||
return NextResponse.json(subscribers);
|
||||
});
|
||||
```
|
||||
|
||||
Create `src/lib/__tests__/rbac.test.ts` — comprehensive unit tests:
|
||||
|
||||
**Admin tests:**
|
||||
- Admin can manage Subscribers (returns true)
|
||||
- Admin can manage Users (returns true)
|
||||
- Admin can manage Reports (returns true)
|
||||
|
||||
**Office Staff tests:**
|
||||
- Office Staff can manage Subscribers (true)
|
||||
- Office Staff can read Reports (true)
|
||||
- Office Staff cannot create Account (false — cannot modify COA)
|
||||
- Office Staff cannot delete Account (false)
|
||||
|
||||
**Collector tests:**
|
||||
- Collector can read Subscribers (true)
|
||||
- Collector can create Payment (true)
|
||||
- Collector cannot manage Invoice (false)
|
||||
- Collector cannot manage User (false)
|
||||
- Collector cannot read Reports (false)
|
||||
|
||||
**Technician tests:**
|
||||
- Technician cannot manage Invoice (false)
|
||||
- Technician cannot manage Payment (false)
|
||||
- Technician cannot manage Subscriber (false)
|
||||
- Technician cannot read Reports (false)
|
||||
|
||||
**Client tests:**
|
||||
- Client can create Ticket (true)
|
||||
- Client cannot manage User (false)
|
||||
- Client cannot manage Subscriber (false — not even their own, they can only read)
|
||||
- Client cannot read Reports (false)
|
||||
|
||||
**Multi-role tests:**
|
||||
- User with [TECHNICIAN, COLLECTOR] roles can read Subscribers (from Collector) AND update JobOrder (from Technician)
|
||||
- Union of permissions is additive
|
||||
|
||||
**Super-admin tests:**
|
||||
- Super-admin can manage all (true for any subject)
|
||||
|
||||
All tests use defineAbilityFor directly — no HTTP requests needed. These are pure unit tests.
|
||||
</action>
|
||||
<verify>
|
||||
Run `npx vitest run` — all RBAC tests pass. Verify that at minimum: Technician cannot access billing (critical requirement from phase success criteria).
|
||||
</verify>
|
||||
<done>API authorization middleware returns 403 for unauthorized access. All 5 roles have correct permission boundaries verified by unit tests. Multi-role union works. Super-admin has full access (AUTH-02, AUTH-03).</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. `npx vitest run` passes all RBAC tests
|
||||
2. Admin has full access, Office Staff can't modify COA, Collector can't manage billing, Technician can't see billing/subscriber management, Client can only see own data
|
||||
3. Multi-role user gets union of permissions
|
||||
4. Super-admin bypasses all permission checks
|
||||
5. withPermission middleware returns 401 for unauthenticated, 403 for unauthorized
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- All 5 roles have correctly scoped permissions (AUTH-02, AUTH-03)
|
||||
- API-layer enforcement returns 403 (not just UI hiding) for unauthorized access
|
||||
- Technician cannot access billing or subscriber management routes
|
||||
- Multi-role users get union of all assigned role permissions
|
||||
- Comprehensive unit tests validate every role boundary
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-foundation/01-04-SUMMARY.md`
|
||||
</output>
|
||||
222
.planning/phases/01-foundation/01-05-PLAN.md
Normal file
222
.planning/phases/01-foundation/01-05-PLAN.md
Normal file
@@ -0,0 +1,222 @@
|
||||
---
|
||||
phase: 01-foundation
|
||||
plan: 05
|
||||
type: execute
|
||||
wave: 4
|
||||
depends_on: ["01-04"]
|
||||
files_modified:
|
||||
- src/app/(super-admin)/layout.tsx
|
||||
- src/app/(super-admin)/admin/page.tsx
|
||||
- src/app/(super-admin)/admin/tenants/page.tsx
|
||||
- src/app/api/admin/tenants/route.ts
|
||||
- src/app/api/admin/tenants/[id]/route.ts
|
||||
- src/app/api/admin/tenants/[id]/suspend/route.ts
|
||||
- src/lib/middleware/super-admin.ts
|
||||
- prisma/seed.ts
|
||||
- src/lib/__tests__/super-admin.test.ts
|
||||
- src/lib/__tests__/tenant-isolation.test.ts
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Super-admin can log in and view all tenants without being scoped to any single tenant"
|
||||
- "Super-admin can see tenant status, creation date, and subscriber count for each tenant"
|
||||
- "Super-admin can suspend a tenant with a grace period"
|
||||
- "Super-admin cannot access tenant-scoped data (no impersonation)"
|
||||
- "Non-super-admin users cannot access the admin panel"
|
||||
- "Test harness validates core isolation and auth behaviors"
|
||||
artifacts:
|
||||
- path: "src/app/(super-admin)/admin/tenants/page.tsx"
|
||||
provides: "Tenant management list UI"
|
||||
min_lines: 40
|
||||
- path: "src/app/api/admin/tenants/route.ts"
|
||||
provides: "List all tenants API"
|
||||
exports: ["GET"]
|
||||
- path: "src/app/api/admin/tenants/[id]/suspend/route.ts"
|
||||
provides: "Tenant suspension API"
|
||||
exports: ["POST"]
|
||||
- path: "src/lib/middleware/super-admin.ts"
|
||||
provides: "Super-admin route guard"
|
||||
exports: ["withSuperAdmin"]
|
||||
- path: "src/lib/__tests__/super-admin.test.ts"
|
||||
provides: "Super-admin access and tenant management tests"
|
||||
min_lines: 40
|
||||
key_links:
|
||||
- from: "src/app/api/admin/tenants/route.ts"
|
||||
to: "src/lib/middleware/super-admin.ts"
|
||||
via: "withSuperAdmin guard on route"
|
||||
pattern: "withSuperAdmin"
|
||||
- from: "src/app/(super-admin)/admin/tenants/page.tsx"
|
||||
to: "src/app/api/admin/tenants/route.ts"
|
||||
via: "fetch tenant list"
|
||||
pattern: "fetch.*api/admin/tenants"
|
||||
- from: "src/lib/middleware/super-admin.ts"
|
||||
to: "src/lib/auth.ts"
|
||||
via: "Checks isSuperAdmin from session"
|
||||
pattern: "isSuperAdmin"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Implement the super-admin panel: a separate auth context for platform-level management, tenant listing with status/metrics, tenant suspension with grace period, and the consolidated test harness proving all Phase 1 behaviors work together.
|
||||
|
||||
Purpose: The platform owner needs to manage ISP tenants (view, suspend, monitor) without being scoped to any single tenant. This closes the super-admin requirement and validates the entire foundation with comprehensive tests.
|
||||
Output: Working super-admin panel with tenant management, plus full test suite covering auth, isolation, and RBAC.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@C:\Users\KevinAsprec\.claude/get-shit-done/workflows/execute-plan.md
|
||||
@C:\Users\KevinAsprec\.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/phases/01-foundation/01-CONTEXT.md
|
||||
@.planning/phases/01-foundation/01-02-SUMMARY.md
|
||||
@.planning/phases/01-foundation/01-03-SUMMARY.md
|
||||
@.planning/phases/01-foundation/01-04-SUMMARY.md
|
||||
@src/lib/auth-options.ts
|
||||
@src/lib/casl/ability.ts
|
||||
@src/lib/prisma-tenant.ts
|
||||
@prisma/schema.prisma
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Super-admin API routes and middleware guard</name>
|
||||
<files>
|
||||
src/lib/middleware/super-admin.ts
|
||||
src/app/api/admin/tenants/route.ts
|
||||
src/app/api/admin/tenants/[id]/route.ts
|
||||
src/app/api/admin/tenants/[id]/suspend/route.ts
|
||||
prisma/seed.ts
|
||||
</files>
|
||||
<action>
|
||||
Create `src/lib/middleware/super-admin.ts`:
|
||||
- Export `withSuperAdmin(handler)` — wraps an API route handler
|
||||
- Gets current user session via getCurrentUser()
|
||||
- If no session: return 401
|
||||
- If session.user.isSuperAdmin is not true: return 403 { error: "Super-admin access required" }
|
||||
- If authorized: call handler with user context
|
||||
|
||||
Create `src/app/api/admin/tenants/route.ts`:
|
||||
- GET handler wrapped with withSuperAdmin
|
||||
- Queries ALL tenants (no tenant scoping — super-admin sees everything)
|
||||
- Returns array of: { id, name, slug, status, ownerEmail, createdAt, userCount, subscriberCount (0 for now — no Subscriber model yet) }
|
||||
- userCount: count of users per tenant via Prisma _count
|
||||
- Sort by createdAt descending
|
||||
|
||||
Create `src/app/api/admin/tenants/[id]/route.ts`:
|
||||
- GET handler wrapped with withSuperAdmin
|
||||
- Returns single tenant detail: all fields plus users list (id, email, firstName, lastName, roles, isActive)
|
||||
- Return 404 if tenant not found
|
||||
|
||||
Create `src/app/api/admin/tenants/[id]/suspend/route.ts`:
|
||||
- POST handler wrapped with withSuperAdmin
|
||||
- Accepts JSON body: { action: "suspend" | "activate" }
|
||||
- For suspend:
|
||||
- Set status to PENDING_SUSPENSION
|
||||
- Set suspendedAt to now()
|
||||
- Set gracePeriodEndsAt to 7 days from now
|
||||
- Return { message: "Tenant suspension initiated. Grace period ends on {date}." }
|
||||
- For activate:
|
||||
- Set status to ACTIVE
|
||||
- Clear suspendedAt and gracePeriodEndsAt
|
||||
- Return { message: "Tenant activated." }
|
||||
- Return 404 if tenant not found
|
||||
|
||||
Update `prisma/seed.ts`:
|
||||
- Add a second demo tenant: "Test ISP 2" with a different admin user (admin2@demo.com) for testing cross-tenant isolation
|
||||
- Keep existing demo tenant and super-admin user
|
||||
</action>
|
||||
<verify>
|
||||
Run `npm run db:seed` — seed runs cleanly with both tenants. Start the app. Use curl or an API client to:
|
||||
1. GET /api/admin/tenants without auth — expect 401
|
||||
2. Log in as super-admin, GET /api/admin/tenants — expect 200 with both tenants
|
||||
3. POST /api/admin/tenants/{id}/suspend with { action: "suspend" } — expect tenant status changes to PENDING_SUSPENSION
|
||||
</verify>
|
||||
<done>Super-admin API routes list all tenants and can suspend/activate them. withSuperAdmin middleware blocks non-super-admin access with 403. Grace period suspension works.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Super-admin UI panel and comprehensive test harness</name>
|
||||
<files>
|
||||
src/app/(super-admin)/layout.tsx
|
||||
src/app/(super-admin)/admin/page.tsx
|
||||
src/app/(super-admin)/admin/tenants/page.tsx
|
||||
src/middleware.ts
|
||||
src/lib/__tests__/super-admin.test.ts
|
||||
src/lib/__tests__/tenant-isolation.test.ts
|
||||
</files>
|
||||
<action>
|
||||
Create `src/app/(super-admin)/layout.tsx`:
|
||||
- Server component that checks session
|
||||
- If user is not super-admin, redirect to /login or show 403 page
|
||||
- Simple layout with sidebar navigation: "Dashboard", "Tenants"
|
||||
- Header shows "NetForge Admin" and the super-admin's name
|
||||
- Sign out button
|
||||
|
||||
Create `src/app/(super-admin)/admin/page.tsx`:
|
||||
- Simple admin dashboard showing: total tenants, active tenants, suspended tenants
|
||||
- Fetches stats from /api/admin/tenants
|
||||
|
||||
Create `src/app/(super-admin)/admin/tenants/page.tsx`:
|
||||
- Table listing all tenants: Name, Status (badge with color: green=Active, yellow=Pending Suspension, red=Suspended), Owner Email, Users, Created Date
|
||||
- Each row has actions: View details, Suspend/Activate toggle button
|
||||
- Suspend button triggers confirmation dialog then calls the suspend API
|
||||
- Activate button calls the suspend API with action: "activate"
|
||||
- Clicking tenant name navigates to a detail view (or opens a modal showing users)
|
||||
- Use Tailwind for styling — clean data table with hover states
|
||||
|
||||
Update `src/middleware.ts`:
|
||||
- Add /admin/* routes to be protected
|
||||
- These routes should check for super-admin status specifically (or rely on the layout redirect + API guards)
|
||||
- Public routes remain: /login, /signup, /api/auth/*
|
||||
|
||||
Create `src/lib/__tests__/super-admin.test.ts`:
|
||||
- Test: super-admin user can access admin routes (withSuperAdmin allows)
|
||||
- Test: regular admin user cannot access admin routes (withSuperAdmin returns 403)
|
||||
- Test: tenant suspension sets correct status and grace period dates
|
||||
- Test: tenant activation clears suspension fields
|
||||
|
||||
Enhance `src/lib/__tests__/tenant-isolation.test.ts` (if needed):
|
||||
- Ensure the existing cross-tenant isolation test still passes with the two-tenant seed data
|
||||
- Add test: super-admin query for tenants returns ALL tenants (not scoped)
|
||||
- Add test: regular user query does NOT return other tenants' data
|
||||
|
||||
Run full test suite: `npx vitest run` — all tests must pass.
|
||||
</action>
|
||||
<verify>
|
||||
Run `npx vitest run` — all tests pass (auth, RBAC, tenant isolation, super-admin). Start the app, log in as superadmin@netforge.com, navigate to /admin/tenants — see both demo tenants listed. Suspend a tenant — status badge updates. Activate it — status reverts. Log in as admin@demo.com — navigating to /admin returns forbidden.
|
||||
</verify>
|
||||
<done>Super-admin can log in and manage all tenants via /admin panel (TENANT-03). Non-super-admin users are blocked from admin routes. Full test harness covers auth, RBAC, tenant isolation, and super-admin access. Phase 1 success criteria are met.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. Super-admin logs in and sees tenant list at /admin/tenants
|
||||
2. Tenant table shows status, owner, user count, creation date
|
||||
3. Suspend action sets PENDING_SUSPENSION with 7-day grace period
|
||||
4. Activate action restores ACTIVE status
|
||||
5. Regular admin user gets 403 when accessing /admin routes
|
||||
6. `npx vitest run` passes ALL tests across the entire phase:
|
||||
- Auth config tests
|
||||
- RBAC permission tests for all 5 roles
|
||||
- Tenant isolation tests (zero cross-tenant leakage)
|
||||
- Super-admin access tests
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Super-admin can log in and view all tenants (TENANT-03)
|
||||
- Super-admin sees tenant status, user count, creation date per tenant
|
||||
- Tenant suspension with grace period works (not immediate lockout)
|
||||
- Non-super-admin blocked from admin panel
|
||||
- Full test suite validates Phase 1 foundation (INFRA-02 partial — unit tests for auth/RBAC/isolation)
|
||||
- All Phase 1 requirements met: TENANT-01, TENANT-02, TENANT-03, AUTH-01, AUTH-02, AUTH-03, AUTH-04, INFRA-01, INFRA-02
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-foundation/01-05-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user