feat(19-02): add POST /api/phishing/campaigns/[id]/classify route

- requirePermission('phishing','analyze') early-return (same action as /analyze, Phase 18 D-06)
- UUID_RE guard on campaign id before any DB query (T-19-05)
- 404 when campaign id is well-formed but not found
- delegates to classifyCampaign(id) from lib/services/campaign-classifier.ts (Plan 01), returns flat ClassifyResult payload
This commit is contained in:
lorentz 2026-07-16 08:26:20 -04:00
parent 28f28a87a4
commit 3e8d5b83c9

View file

@ -0,0 +1,51 @@
/**
* POST /api/phishing/campaigns/[id]/classify
*
* On-demand (re-)trigger for campaign classification (D-02). Enforces the
* same phishing/analyze permission gate as
* `/api/phishing/tickets/[ticket_id]/analyze` (Phase 18 D-06 convention
* NOT a new permission), validates the campaign id as a UUID (V5), then
* delegates to `classifyCampaign` (Plan 01) and returns the flat camelCase
* verdict payload.
*/
import { NextRequest, NextResponse } from 'next/server';
import { requirePermission } from '@/lib/auth-utils';
import postgresClient from '@/lib/services/postgres-client';
import { classifyCampaign } from '@/lib/services/campaign-classifier';
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 });
}
const result = await classifyCampaign(id);
return NextResponse.json(result);
} catch (err) {
console.error('[PHISHING-CLASSIFY] Failed to classify campaign', id, err);
return NextResponse.json(
{ error: 'Failed to classify campaign', message: err instanceof Error ? err.message : 'Unknown error' },
{ status: 500 }
);
}
}