feat(260717-v6c): add POST /api/phishing/campaigns/[id]/mark-accidental-report route
Mirrors mark-false-positive route exactly (requirePermission phishing/approve, UUID validation, optional reason body, campaign-existence check, 409/400/500 error mapping) but calls markCampaignAccidentalReport and returns the richer result including notePosted/noteError.
This commit is contained in:
parent
aea4fd2f0c
commit
97804f2e5b
1 changed files with 83 additions and 0 deletions
|
|
@ -0,0 +1,83 @@
|
||||||
|
/**
|
||||||
|
* POST /api/phishing/campaigns/[id]/mark-accidental-report
|
||||||
|
*
|
||||||
|
* Marks a campaign as an accidental report — an employee flagged a
|
||||||
|
* legitimate email by mistake. Gated by phishing/approve (same elevated
|
||||||
|
* tier as mark-false-positive; no separate action key). Validates the
|
||||||
|
* campaign id as a UUID, optionally accepts a JSON body with a `reason`
|
||||||
|
* string, and delegates to `markCampaignAccidentalReport`, which guards
|
||||||
|
* against marking a campaign that already has approved/completed
|
||||||
|
* remediation (RemediationConflictError -> 409). Unlike mark-false-positive,
|
||||||
|
* this also posts a customer-facing "reviewed, no action needed" note to
|
||||||
|
* every reporting employee's ticket — the response includes
|
||||||
|
* notePosted/noteError so the caller can distinguish full success from
|
||||||
|
* status-changed-but-note-failed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { requirePermission } from '@/lib/auth-utils';
|
||||||
|
import postgresClient from '@/lib/services/postgres-client';
|
||||||
|
import {
|
||||||
|
markCampaignAccidentalReport,
|
||||||
|
RemediationValidationError,
|
||||||
|
RemediationConflictError,
|
||||||
|
} from '@/lib/services/remediation-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 { session, error } = await requirePermission('phishing', 'approve');
|
||||||
|
if (error) return error;
|
||||||
|
|
||||||
|
const { id } = await params;
|
||||||
|
// 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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Body is optional — tolerate an empty/absent body (default reason undefined).
|
||||||
|
let reason: string | undefined;
|
||||||
|
const rawBody = await request.text();
|
||||||
|
if (rawBody.trim().length > 0) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(rawBody) as { reason?: unknown };
|
||||||
|
reason = typeof parsed.reason === 'string' ? parsed.reason : undefined;
|
||||||
|
} catch {
|
||||||
|
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const actor = (session?.user as { email?: string } | undefined)?.email ?? null;
|
||||||
|
|
||||||
|
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 markCampaignAccidentalReport(id, actor, reason);
|
||||||
|
return NextResponse.json(result);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof RemediationConflictError) {
|
||||||
|
return NextResponse.json({ error: err.message }, { status: 409 });
|
||||||
|
}
|
||||||
|
if (err instanceof RemediationValidationError) {
|
||||||
|
return NextResponse.json({ error: err.message }, { status: 400 });
|
||||||
|
}
|
||||||
|
console.error('[PHISHING-MARK-ACCIDENTAL] Failed to mark campaign as accidental report', id, err);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: 'Failed to mark campaign as accidental report',
|
||||||
|
message: err instanceof Error ? err.message : 'Unknown error',
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue