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:
parent
28f28a87a4
commit
3e8d5b83c9
1 changed files with 51 additions and 0 deletions
51
app/api/phishing/campaigns/[id]/classify/route.ts
Normal file
51
app/api/phishing/campaigns/[id]/classify/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue