tenantpilot/specs/001-global-policy-search/plan.md
2025-12-05 22:06:22 +01:00

248 lines
6.4 KiB
Markdown

# Implementation Plan: Global Policy Search
**Feature**: 001-global-policy-search
**Branch**: `001-global-policy-search`
**Version**: 1.0.0
**Created**: 2025-12-05
---
## Technical Context
| Aspect | Decision | Notes |
|--------|----------|-------|
| Database ORM | Drizzle ORM | Existing in project |
| Search Method | PostgreSQL `ilike` | Case-insensitive substring search |
| Auth Provider | Azure AD (NextAuth) | Extract `tenantId` from `tid` claim |
| UI Components | Shadcn UI | Input, Table, Card |
| Data Fetching | Server Actions | Constitution: Server-First |
| API Security | X-API-SECRET header | For n8n ingestion |
---
## Constitution Check
| Principle | Compliant | Implementation |
|-----------|-----------|----------------|
| Server-First | ✅ | Server Actions for search, API route for ingestion |
| TypeScript Strict | ✅ | Full types for schema, validators, actions |
| Drizzle ORM | ✅ | `policySettings` table with proper indexes |
| Shadcn UI | ✅ | Input, Table components for search UI |
| Azure AD Multi-Tenancy | ✅ | tenantId from session, all queries filtered |
---
## Gate Evaluation
### Gate 1: Scope Check
- [x] Feature spec exists: `specs/001-global-policy-search/spec.md`
- [x] User stories defined: 3 stories (Search, Tenant Isolation, Ingestion API)
- [x] Requirements numbered: F-001 through F-009
- **Result**: PASS
### Gate 2: Constitution Compliance
- [x] No client-side data fetching for primary flows
- [x] All types explicitly defined (no `any`)
- [x] Database via Drizzle ORM only
- [x] UI components from Shadcn library
- [x] Tenant isolation enforced
- **Result**: PASS
### Gate 3: Security Review
- [x] Authentication required for search (getUserAuth)
- [x] Tenant isolation in all queries (tenantId filter)
- [x] API secret for ingestion endpoint (X-API-SECRET)
- [x] Input validation with Zod schemas
- **Result**: PASS
---
## Phase 0: Research (Complete)
See: `specs/001-global-policy-search/research.md`
Resolved:
- Drizzle `ilike` for case-insensitive search
- Server Actions pattern for search
- API_SECRET header authentication
- Azure AD tenantId extraction from session
- Upsert with `onConflictDoUpdate`
---
## Phase 1: Design (Complete)
### Artifacts Generated
| Artifact | Path | Status |
|----------|------|--------|
| Data Model | `specs/001-global-policy-search/data-model.md` | ✅ |
| API Contract | `specs/001-global-policy-search/contracts/policy-settings-api.yaml` | ✅ |
| Server Actions Contract | `specs/001-global-policy-search/contracts/server-actions.md` | ✅ |
| Quickstart Guide | `specs/001-global-policy-search/quickstart.md` | ✅ |
---
## Phase 2: Implementation Tasks
### Task 1: Database Schema
**File**: `lib/db/schema/policySettings.ts`
**Priority**: P1
**Estimate**: 30 min
- Create `policySettings` pgTable
- Add indexes for tenant filtering and search
- Export types: `PolicySetting`, `NewPolicySetting`
- Run `npm run db:push`
### Task 2: Zod Validators
**File**: `lib/validators/policySettings.ts`
**Priority**: P1
**Estimate**: 15 min
- `policySettingSchema` for single setting
- `bulkPolicySettingsSchema` for API input
- Export input types
### Task 3: Extend NextAuth Session
**File**: `lib/auth/utils.ts`
**Priority**: P1
**Estimate**: 20 min
- Add `jwt` callback to extract `tid` claim
- Add `session` callback to include `tenantId`
- Update TypeScript types for extended session
### Task 4: Server Actions
**File**: `lib/actions/policySettings.ts`
**Priority**: P1
**Estimate**: 45 min
- `searchPolicySettings(searchTerm)` with ilike query
- `getPolicySettingById(id)` with tenant check
- `getRecentPolicySettings(limit)` sorted by lastSyncedAt
- Full tenant isolation in all queries
### Task 5: Ingestion API Route
**File**: `app/api/policy-settings/route.ts`
**Priority**: P2
**Estimate**: 30 min
- POST handler with X-API-SECRET validation
- Bulk upsert with `onConflictDoUpdate`
- DELETE handler for tenant cleanup
- Proper error responses (400, 401, 500)
### Task 6: Environment Variable
**File**: `.env` (local), `lib/env.mjs`
**Priority**: P2
**Estimate**: 10 min
- Add `POLICY_API_SECRET` to .env
- Add optional validation in env.mjs
### Task 7: Search Page UI
**File**: `app/(app)/search/page.tsx`
**Priority**: P1
**Estimate**: 45 min
- Search input with debounce
- Results table with columns
- Loading and empty states
- Error handling
### Task 8: Search Components
**Files**: `components/search/*.tsx`
**Priority**: P1
**Estimate**: 30 min
- `SearchInput.tsx` - Input with search icon
- `ResultsTable.tsx` - Table with policy data
- `EmptyState.tsx` - No results message
### Task 9: Navigation Update
**File**: `config/nav.ts`
**Priority**: P1
**Estimate**: 5 min
- Add search link to sidebar
- Icon: Search or MagnifyingGlass
---
## Dependencies (Implementation Order)
```
Task 1 (Schema)
Task 2 (Validators)
Task 3 (Auth) ──────┬──→ Task 4 (Server Actions)
└──→ Task 5 (API Route)
Task 6 (Env Var)
Task 4 (Server Actions)
Task 7 (Search Page) ←── Task 8 (Components)
Task 9 (Navigation)
```
---
## Acceptance Criteria
From spec requirements:
- [ ] F-001: Full-text search across settings (ilike on name/value)
- [ ] F-002: Display policy name, type, setting name/value
- [ ] F-003: Debounced instant search (300ms)
- [ ] F-004: User only sees own tenant's data
- [ ] F-005: No tenant impersonation possible
- [ ] F-006: API_SECRET header required for ingestion
- [ ] F-007: Bulk upsert endpoint operational
- [ ] F-008: Zod validation on all API inputs
- [ ] F-009: Upsert logic prevents duplicates
---
## Test Plan
### Manual Testing
1. **Search Flow**
- Login with Azure AD
- Navigate to /search
- Enter search term
- Verify results from own tenant only
2. **Ingestion API**
- POST without secret → 401
- POST with wrong secret → 401
- POST with correct secret → 200
- Verify data appears in search
3. **Tenant Isolation**
- Ingest data for tenant A
- Login as tenant B user
- Search should return empty
---
## Environment Variables Required
| Variable | Purpose | Required In |
|----------|---------|-------------|
| `POLICY_API_SECRET` | n8n ingestion auth | Production, Development |
---
## Rollback Plan
If issues arise:
1. Revert branch to previous commit
2. Run `npm run db:push` to restore schema
3. No data migration needed (new table)