feat(21-02): add POST /api/phishing/campaigns/[id]/triage-note route
Structural twin of the classify route: requirePermission('phishing',
'analyze') gate, UUID guard, campaign-exists 404 check, delegates to
generateAndPostTriageNote and returns its result verbatim (note text +
per-ticket posted/error status, D-06). No audit-event write — deferred
per 21-CONTEXT.md.
This commit is contained in:
parent
2d410f8d15
commit
e3a9cb5191
1 changed files with 59 additions and 0 deletions
59
app/api/phishing/campaigns/[id]/triage-note/route.ts
Normal file
59
app/api/phishing/campaigns/[id]/triage-note/route.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* POST /api/phishing/campaigns/[id]/triage-note
|
||||
*
|
||||
* On-demand (re-)triggerable (D-03 — no dedupe/skip tracking) triage-note
|
||||
* generation + post for a campaign. Structural twin of
|
||||
* `app/api/phishing/campaigns/[id]/classify/route.ts` — enforces the same
|
||||
* `phishing:analyze` permission tier (informational action, not a
|
||||
* state-changing security decision like approve/remediate — CONTEXT.md
|
||||
* Claude's Discretion), validates the campaign id as a UUID (V5), 404s an
|
||||
* unknown campaign, then delegates to `generateAndPostTriageNote` and
|
||||
* returns its result verbatim (D-06 — note text + per-ticket status list,
|
||||
* never reshaped). No audit-event write here (CONTEXT.md: sent-note history
|
||||
* is a deferred idea, out of scope for this phase).
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requirePermission } from '@/lib/auth-utils';
|
||||
import postgresClient from '@/lib/services/postgres-client';
|
||||
import { generateAndPostTriageNote } from '@/lib/services/triage-note-service';
|
||||
|
||||
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { error } = await requirePermission('phishing', 'analyze');
|
||||
if (error) return error;
|
||||
|
||||
const { id } = await params;
|
||||
// V5: validate UUID shape before querying — a malformed id would otherwise
|
||||
// surface as an unhandled Postgres error -> uncaught 500.
|
||||
if (!UUID_RE.test(id)) {
|
||||
return NextResponse.json({ error: 'Invalid campaign id' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const campaignRes = await postgresClient.query<{ id: string }>(
|
||||
`SELECT id FROM campaigns WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
if (!campaignRes.rows[0]) {
|
||||
return NextResponse.json({ error: 'Campaign not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Outer catch below only fires for whole-request failures (e.g. DB
|
||||
// unreachable) — per-ticket Autotask write failures are already captured
|
||||
// inside the service and returned in this 200 body (D-05).
|
||||
const result = await generateAndPostTriageNote(id);
|
||||
|
||||
return NextResponse.json(result);
|
||||
} catch (err) {
|
||||
console.error('[PHISHING-TRIAGE-NOTE] Failed to generate triage note', id, err);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to generate triage note', message: err instanceof Error ? err.message : 'Unknown error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue