docs(18-03): complete campaign read API plan
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
959907d63b
commit
86f5166a86
1 changed files with 111 additions and 0 deletions
|
|
@ -0,0 +1,111 @@
|
|||
---
|
||||
phase: 18-campaign-grouping-phishing-analysis-api
|
||||
plan: 03
|
||||
subsystem: api
|
||||
tags: [postgres, phishing, campaigns, better-auth, next-js-routes]
|
||||
|
||||
# Dependency graph
|
||||
requires:
|
||||
- phase: 18-campaign-grouping-phishing-analysis-api (Plan 01)
|
||||
provides: "phishing permission resource in lib/permissions.ts (requirePermission('phishing','read') type-checks and enforces role grants)"
|
||||
provides:
|
||||
- "GET /api/phishing/campaigns — paginated camelCase campaign list (CAMP-03)"
|
||||
- "GET /api/phishing/campaigns/{id} — nested camelCase campaign detail with reports/messages/indicators/classifications (CAMP-03)"
|
||||
affects: [19-classification, 20-remediation, 21-autotask-triage-note]
|
||||
|
||||
# Tech tracking
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Bulk-fetch nested detail via ANY($1::uuid[]) keyed by parent id array, assembled with plain .map() (device-link-conflicts pattern) — no ORM, no ad-hoc joins across 4 levels"
|
||||
- "Parameterized status/filter values via $n placeholder index, never string-interpolated into SQL text"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- app/api/phishing/campaigns/route.ts
|
||||
- app/api/phishing/campaigns/[id]/route.ts
|
||||
modified: []
|
||||
|
||||
key-decisions:
|
||||
- "messages.subject is derived at query time via headers->>'subject' JSONB extraction since messages has no subject column (per migrations/097 schema + 18-PATTERNS.md note)"
|
||||
- "requesterEmail on each report is derived via a LEFT JOIN reports.requester_contact_id -> contacts.email_address, since campaigns/reports have no recipients column"
|
||||
- "classifications are queried and included in the detail response shape even though the classifications table is expected empty this phase (Phase 19 stub) — per CAMP-03's requirement to expose the shape now"
|
||||
- "Child bulk-fetch queries (messages, indicators) are skipped entirely when the parent id array is empty, avoiding an unnecessary ANY($1::uuid[]) call with an empty array"
|
||||
|
||||
patterns-established:
|
||||
- "Both list and detail phishing routes gate with a first-line requirePermission('phishing','read') call-and-early-return (D-06), matching every other route in this phase and the wider codebase convention"
|
||||
|
||||
requirements-completed: [CAMP-03, ACCESS-01]
|
||||
|
||||
# Metrics
|
||||
duration: 20min
|
||||
completed: 2026-07-15
|
||||
---
|
||||
|
||||
# Phase 18 Plan 03: Campaign Read API (List + Detail) Summary
|
||||
|
||||
**Two new GET routes — `/api/phishing/campaigns` (paginated camelCase list) and `/api/phishing/campaigns/{id}` (nested detail with linked reports/messages/indicators/classifications) — both gated by `requirePermission('phishing','read')`.**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** ~20 min
|
||||
- **Started:** 2026-07-15T23:15Z (approx)
|
||||
- **Completed:** 2026-07-15T23:31Z
|
||||
- **Tasks:** 2/2 completed
|
||||
- **Files modified:** 2 (both created)
|
||||
|
||||
## Accomplishments
|
||||
- `app/api/phishing/campaigns/route.ts` — paginated `GET` returning `{ items, total, limit, offset }` in camelCase (id, campaignKey, groupMethod, firstSeenAt, lastSeenAt, reportCount, status, createdAt). `limit` clamped to `[1,200]` (default 50), `offset` clamped to `>= 0` (default 0), optional `status` filter bound via a `$n` placeholder (never string-interpolated).
|
||||
- `app/api/phishing/campaigns/[id]/route.ts` — `GET` returning one campaign with nested `reports[]` (+ `requesterEmail` via `contacts` join), `messages[]` (+ `subject` via `headers->>'subject'`), `indicators[]`, and `classifications[]`, all in camelCase. UUID-validates the path param (400 on malformed id before any query), 404 when the campaign is absent. Children are bulk-fetched via `= ANY($1::uuid[])` keyed by the parent id array (device-link-conflicts pattern), with an empty-array short-circuit so no query runs when there are zero reports/messages.
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task was committed atomically:
|
||||
|
||||
1. **Task 1: GET /api/phishing/campaigns paginated list** - `c852cfe` (feat)
|
||||
2. **Task 2: GET /api/phishing/campaigns/[id] nested detail** - `959907d` (feat)
|
||||
|
||||
**Plan metadata:** committed alongside this SUMMARY (final commit in this plan's history)
|
||||
|
||||
## Files Created/Modified
|
||||
- `app/api/phishing/campaigns/route.ts` - `GET` handler: requirePermission gate, clamp limit/offset, optional parameterized status filter, COUNT(*) total, camelCase `.map()`
|
||||
- `app/api/phishing/campaigns/[id]/route.ts` - `GET` handler: requirePermission gate, UUID regex validation (400), parent campaign lookup (404 if absent), bulk-fetch reports (+ contacts join for requesterEmail) / messages (+ headers->>'subject') / indicators / classifications, camelCase nested assembly
|
||||
|
||||
## Decisions Made
|
||||
- Followed 18-PATTERNS.md section 4/5 code shapes essentially verbatim — no deviation in query structure or response shape from the pattern map's "Exact shape to replicate" blocks.
|
||||
- Confirmed `messages` has no `subject` column (checked `migrations/097_phishing_triage_schema.sql` directly) before writing the query, per the plan's explicit warning that a naive `SELECT subject` would fail — used `headers->>'subject' AS subject` instead.
|
||||
- Both routes wrap all Postgres work in try/catch returning 500 `{ error, message }` on unexpected failure, matching `device-link-conflicts/route.ts`'s error-handling convention (not shown in the pattern map's illustrative snippets, but required by CLAUDE.md's API route conventions and consistent with Task 1's stated acceptance criteria).
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
**1. [Manual verification not performed against a live server]**
|
||||
- **Found during:** Task 1 and Task 2 verification step
|
||||
- **Issue:** Both tasks' `<verify>` blocks specify a `<human-check>` step (curl against `localhost:3100` with an admin session cookie to confirm 200/401/400/404 responses). The dev server currently listening on port 3100 in this sandbox is a long-running Next.js process bound to the main repository checkout (`next-server`, running since before this session started), not this git worktree — it does not reflect the two new route files created here. Restarting or repointing that shared server risked disrupting other concurrent worktree agents in the same wave.
|
||||
- **Resolution:** Performed the automated verification (`npx tsc --noEmit --pretty`, exit 0, both files) and re-derived every acceptance criterion via targeted `grep` (requirePermission call sites, `$n` placeholder, `Math.min` clamp, `ANY($1::uuid[])` bulk-fetch, UUID regex, `requester_contact_id` join, `headers->>'subject'`) — all confirmed present. The live curl-based 200/401/400/404 check is deferred to the orchestrator's post-wave verification pass (or a future manual QA pass), since it requires a server instance actually running this worktree's code plus a real campaign UUID and an authenticated session cookie, neither of which are safely producible from this isolated worktree without touching shared infrastructure.
|
||||
- **Files modified:** none (documentation-only deviation, no code change)
|
||||
- **Impact:** Both routes are believed correct per code review against the pattern map and the type checker; the manual HTTP-level check is the one open item, not a code defect.
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 1 (verification method substitution, no code impact)
|
||||
**Impact on plan:** No scope creep, no code changes beyond the plan's spec. The only deviation is in how the manual verification step was satisfied (static analysis + grep instead of live curl), due to the shared-server constraint of the worktree execution model.
|
||||
|
||||
## Issues Encountered
|
||||
None beyond the verification-method note above.
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None - no external service configuration required.
|
||||
|
||||
## Next Phase Readiness
|
||||
- Both campaign read routes are live and ready for Phase 19 (classification) and Phase 20 (remediation/approval) to build write-path routes alongside them under the same `/api/phishing/*` namespace and `requirePermission('phishing', ...)` convention.
|
||||
- The detail route's `classifications[]` array is wired to query the `classifications` table now — Phase 19 can start writing rows to it with zero changes needed on this route.
|
||||
- Recommended follow-up (not blocking): once a real campaign exists in a running environment, a human or the phase verifier should do the one live curl pass (200/401/400/404) that this plan's `<human-check>` originally called for.
|
||||
|
||||
---
|
||||
*Phase: 18-campaign-grouping-phishing-analysis-api*
|
||||
*Completed: 2026-07-15*
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
Both created files confirmed present on disk; both task commit hashes (`c852cfe`, `959907d`) confirmed present in git log.
|
||||
Loading…
Add table
Add a link
Reference in a new issue