feat(22-02): add ticket->campaign resolver route

- New GET /api/phishing/tickets/{ticket_id}/campaign wraps
  resolveTicketToCampaign() from plan 22-01
- requirePermission('phishing','read') gate, Number.isFinite param
  validation, D-07: found:false at 200 (not 404) for untriaged tickets
This commit is contained in:
lorentz 2026-07-16 14:41:06 -04:00
parent 75cd454ff8
commit ca63910562

View file

@ -0,0 +1,42 @@
/**
* GET /api/phishing/tickets/{ticket_id}/campaign
*
* Thin resolver route: given an Autotask ticket id, reports back whether a
* phishing report exists for it and (if grouped) which campaign it belongs
* to. Pure lookup no side effects, so this is a GET, unlike the sibling
* POST /analyze route. See lib/services/phishing-ticket-resolver.ts for the
* actual query.
*
* D-07: a ticket with no matching report returns `{ found: false }` at HTTP
* 200, NOT a 404 the review page distinguishes "valid ticket, not triaged
* yet" from a hard error via the `found` boolean.
*/
import { NextRequest, NextResponse } from 'next/server';
import { requirePermission } from '@/lib/auth-utils';
import { resolveTicketToCampaign } from '@/lib/services/phishing-ticket-resolver';
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ ticket_id: string }> }
) {
const { error } = await requirePermission('phishing', 'read');
if (error) return error;
const { ticket_id } = await params;
const ticketId = Number(ticket_id);
if (!Number.isFinite(ticketId)) {
return NextResponse.json({ error: 'Invalid ticket_id' }, { status: 400 });
}
try {
const resolution = await resolveTicketToCampaign(ticketId);
return NextResponse.json(resolution);
} catch (err) {
console.error('[PHISHING-TICKET-CAMPAIGN] Failed to resolve ticket->campaign', ticketId, err);
return NextResponse.json(
{ error: 'Failed to resolve ticket', message: err instanceof Error ? err.message : 'Unknown error' },
{ status: 500 }
);
}
}