From ca639105629ac68c8b5e2e7101eb988e48c53b2c Mon Sep 17 00:00:00 2001 From: lorentz Date: Thu, 16 Jul 2026 14:41:06 -0400 Subject: [PATCH] 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 --- .../tickets/[ticket_id]/campaign/route.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 app/api/phishing/tickets/[ticket_id]/campaign/route.ts diff --git a/app/api/phishing/tickets/[ticket_id]/campaign/route.ts b/app/api/phishing/tickets/[ticket_id]/campaign/route.ts new file mode 100644 index 0000000..1291f3f --- /dev/null +++ b/app/api/phishing/tickets/[ticket_id]/campaign/route.ts @@ -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 } + ); + } +}