cf1f67229a
- 2 tasks completed: approve + comment API routes, and ApproveButton/CommentForm/CommentList client components - 3 auto-fixed blocking issues documented (missing dep, missing .env.local, prop signature updates) - all STRIDE threats T-02-15 through T-02-20 mitigated - build passes cleanly
161 lines
8.3 KiB
Markdown
161 lines
8.3 KiB
Markdown
---
|
|
phase: "02-admin-area-interactive-features"
|
|
plan: 04
|
|
subsystem: "client-interactions"
|
|
tags: [api-routes, client-components, approvals, comments, immutability]
|
|
dependency_graph:
|
|
requires: ["02-03"]
|
|
provides: ["client-approval-flow", "client-comment-flow"]
|
|
affects: ["src/app/c/[token]/page.tsx", "src/components/phase-timeline.tsx"]
|
|
tech_stack:
|
|
added: []
|
|
patterns: ["token-in-body auth", "router.refresh() for Server Component revalidation", "polymorphic entity comments"]
|
|
key_files:
|
|
created:
|
|
- src/app/api/client/approve/route.ts
|
|
- src/app/api/client/comment/route.ts
|
|
- src/components/client/ApproveButton.tsx
|
|
- src/components/client/CommentForm.tsx
|
|
- src/components/client/CommentList.tsx
|
|
modified:
|
|
- src/app/c/[token]/page.tsx
|
|
- src/components/client-dashboard.tsx
|
|
- src/components/phase-timeline.tsx
|
|
decisions:
|
|
- "approved_at immutability enforced at API layer: route checks approved_at !== null before UPDATE; no-op 200 response if already approved"
|
|
- "Client components use router.refresh() (not full page reload) to re-fetch Server Component data after mutations"
|
|
- "Comments fetched server-side in page.tsx as a single DB query across all entity IDs, passed down as prop — avoids N+1 per entity"
|
|
- "revalidate set to 0 on client page — approvals and comments must always be fresh, ISR would serve stale state"
|
|
- "PhaseTimeline extended (not replaced) to accept token + comments props — Phase 1 UI preserved, interactive elements additive"
|
|
- "ApproveButton renders on all deliverables regardless of status — shows date badge if approved_at set, Approva button otherwise"
|
|
metrics:
|
|
duration_minutes: 17
|
|
completed_date: "2026-05-15"
|
|
tasks_completed: 2
|
|
tasks_total: 2
|
|
files_created: 5
|
|
files_modified: 3
|
|
---
|
|
|
|
# Phase 02 Plan 04: Client Interactions — Approvals + Comments Summary
|
|
|
|
**One-liner:** Client-facing approval and comment API routes with token validation, ownership verification, approved_at immutability enforcement, and inline ApproveButton/CommentForm/CommentList wired into the Phase 1 dashboard via PhaseTimeline.
|
|
|
|
## Tasks Completed
|
|
|
|
| Task | Name | Commit | Key Files |
|
|
|------|------|--------|-----------|
|
|
| 1 | POST /api/client/approve + POST /api/client/comment | c24bdde | src/app/api/client/approve/route.ts, src/app/api/client/comment/route.ts |
|
|
| 2 | ApproveButton, CommentForm, CommentList + dashboard wiring | dc512ec | src/components/client/*, src/app/c/[token]/page.tsx, src/components/phase-timeline.tsx |
|
|
|
|
## What Was Built
|
|
|
|
### API Routes
|
|
|
|
**POST /api/client/approve** (`src/app/api/client/approve/route.ts`):
|
|
- Reads `{ token, deliverableId }` from request body
|
|
- Validates token → finds clientId via `clients` table
|
|
- Verifies deliverable ownership: `deliverables → tasks → phases.client_id = clientId` (innerJoin chain prevents cross-client approval — T-02-16)
|
|
- Checks `approved_at !== null` — if already set, returns 200 no-op (T-02-17 immutability)
|
|
- Sets `status = 'approved'` and `approved_at = new Date()` atomically
|
|
- Returns 404 on invalid token or missing deliverable; 500 on unexpected error
|
|
|
|
**POST /api/client/comment** (`src/app/api/client/comment/route.ts`):
|
|
- Reads `{ token, entity_type, entity_id, body }` from request body
|
|
- Zod schema validates: entity_type enum, body min 1 / max 2000 chars (T-02-20 DoS mitigation)
|
|
- Validates token → finds clientId
|
|
- For tasks: verifies task belongs to a phase owned by clientId
|
|
- For deliverables: verifies deliverable belongs to a task in a phase owned by clientId (T-02-18)
|
|
- Inserts comment with `author: 'client'`
|
|
- Returns 201 on success; 400 on validation failure; 404 on invalid token/entity
|
|
|
|
### Client Components
|
|
|
|
**ApproveButton** (`src/components/client/ApproveButton.tsx`):
|
|
- `'use client'` directive
|
|
- If `approvedAt !== null`: renders immutable green badge "Approvato il [localeDateString it-IT]"
|
|
- Otherwise: renders "Approva" button; on click POSTs to `/api/client/approve`, calls `router.refresh()` on success
|
|
- Loading state disables button; error message shown below on failure
|
|
|
|
**CommentForm** (`src/components/client/CommentForm.tsx`):
|
|
- `'use client'` directive
|
|
- Textarea + submit button; disabled when body is empty or loading
|
|
- POSTs to `/api/client/comment` with `{ token, entity_type, entity_id, body }`
|
|
- On success: clears textarea, calls `router.refresh()` to reload comments from server
|
|
|
|
**CommentList** (`src/components/client/CommentList.tsx`):
|
|
- Pure presentational Server Component (no `'use client'`)
|
|
- Renders nothing when empty
|
|
- Admin comments: right-aligned, dark bubble, labelled "iamcavalli"
|
|
- Client comments: left-aligned, gray bubble, labelled "Tu"
|
|
|
|
### Dashboard Wiring
|
|
|
|
**page.tsx** — extended to:
|
|
- Collect all task IDs and deliverable IDs from the ClientView
|
|
- Run single `db.select().from(comments).where(inArray(comments.entity_id, allEntityIds))` query
|
|
- Pass `token` and `allComments` to `<ClientDashboard>`
|
|
- Changed `revalidate` from 60 (ISR) to 0 (always fresh)
|
|
|
|
**client-dashboard.tsx** — updated `ClientDashboardProps` to include `token: string` and `comments: Comment[]`; passes both to `<PhaseTimeline>`
|
|
|
|
**phase-timeline.tsx** — extended `PhaseTimelineProps` with `token` and `comments`; added `commentsFor()` helper; renders within each task: ApproveButton on each deliverable, CommentList + CommentForm below each deliverable and below each task
|
|
|
|
## Deviations from Plan
|
|
|
|
### Auto-fixed Issues
|
|
|
|
**1. [Rule 3 - Blocking] Missing @radix-ui/react-tabs dependency caused build failure**
|
|
- **Found during:** Task 1 build verification
|
|
- **Issue:** `tabs.tsx` component (from Plan 02-03) imported `@radix-ui/react-tabs` which was listed in `package.json` but not installed in the worktree's node_modules
|
|
- **Fix:** Ran `npm install` in the main repo directory to install all declared dependencies
|
|
- **Files modified:** None (package install only)
|
|
- **Commit:** Resolved before Task 1 commit; no separate commit needed
|
|
|
|
**2. [Rule 3 - Blocking] Missing .env.local in worktree caused build page-data collection error**
|
|
- **Found during:** Task 1 build verification (second attempt)
|
|
- **Issue:** `DATABASE_URL env var is required` error during page data collection; .env.local exists in main repo but not copied to worktree
|
|
- **Fix:** Copied `.env.local` from main repo to worktree root (file is gitignored)
|
|
- **Files modified:** `.env.local` (worktree only, not committed)
|
|
- **Commit:** Not committed (gitignored)
|
|
|
|
**3. [Rule 2 - Missing prop type] ClientDashboard and PhaseTimeline needed prop signature updates**
|
|
- **Found during:** Task 2 — IDE diagnostic after updating page.tsx
|
|
- **Issue:** `ClientDashboard` and `PhaseTimeline` had no `token` or `comments` props in their interfaces — TypeScript error TS2322
|
|
- **Fix:** Updated `ClientDashboardProps` and `PhaseTimelineProps` to include `token: string` and `comments: Comment[]`; updated function signatures and render logic accordingly
|
|
- **Files modified:** `src/components/client-dashboard.tsx`, `src/components/phase-timeline.tsx`
|
|
- **Commit:** dc512ec (included in Task 2 commit)
|
|
|
|
## Known Stubs
|
|
|
|
None — all components are fully wired to live API routes and server-fetched data.
|
|
|
|
## Threat Surface Scan
|
|
|
|
All threat mitigations from the plan's `<threat_model>` are implemented:
|
|
|
|
| Threat ID | Status | Implementation |
|
|
|-----------|--------|---------------|
|
|
| T-02-15 | Mitigated | Token validated via DB lookup before any mutation in both routes |
|
|
| T-02-16 | Mitigated | innerJoin chain (deliverable → task → phase → client_id) prevents cross-client approval |
|
|
| T-02-17 | Mitigated | `approved_at !== null` check before UPDATE; no-op 200 if already approved |
|
|
| T-02-18 | Mitigated | Entity ownership verified via phase → client_id chain before comment insert |
|
|
| T-02-19 | Accepted | Comments scoped to entity_ids from validated client's view; server-side filtered |
|
|
| T-02-20 | Mitigated | Zod schema enforces `max(2000)` on comment body; returns 400 if exceeded |
|
|
|
|
No new threat surface introduced beyond what is documented in the plan.
|
|
|
|
## Self-Check: PASSED
|
|
|
|
Files exist:
|
|
- src/app/api/client/approve/route.ts: FOUND
|
|
- src/app/api/client/comment/route.ts: FOUND
|
|
- src/components/client/ApproveButton.tsx: FOUND
|
|
- src/components/client/CommentForm.tsx: FOUND
|
|
- src/components/client/CommentList.tsx: FOUND
|
|
|
|
Commits exist:
|
|
- c24bdde: FOUND (Task 1)
|
|
- dc512ec: FOUND (Task 2)
|
|
|
|
Build: PASSED (npm run build — no TypeScript errors, all routes listed in output) |