feat(20-02): add approve and remediate routes for phishing campaigns
- POST /approve: phishing:approve gated, validates actions array (D-03), delegates to approveRemediationActions with actor from session - POST /remediate: phishing:remediate gated, delegates to remediateApprovedActions (idempotent completion, REMED-03/04) - Both UUID-guard the campaign id and map RemediationValidationError->400, RemediationConflictError->409
This commit is contained in:
parent
80e7129740
commit
65c4253f98
2 changed files with 139 additions and 0 deletions
76
app/api/phishing/campaigns/[id]/approve/route.ts
Normal file
76
app/api/phishing/campaigns/[id]/approve/route.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* POST /api/phishing/campaigns/[id]/approve
|
||||
*
|
||||
* Operator approval of one or more recommended remediation actions (D-03).
|
||||
* Gated by phishing/approve (D-02 — super-admin + admin only). Validates the
|
||||
* campaign id as a UUID (V5), parses the request body's `actions` array, and
|
||||
* delegates to `approveRemediationActions` (Plan 01), which validates each
|
||||
* action against the campaign's latest classification's recommended_actions
|
||||
* before materializing any rows.
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requirePermission } from '@/lib/auth-utils';
|
||||
import postgresClient from '@/lib/services/postgres-client';
|
||||
import {
|
||||
approveRemediationActions,
|
||||
RemediationValidationError,
|
||||
RemediationConflictError,
|
||||
type ApproveActionInput,
|
||||
} 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;
|
||||
// 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 });
|
||||
}
|
||||
|
||||
let body: { actions?: unknown };
|
||||
try {
|
||||
body = (await request.json()) as typeof body;
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!Array.isArray(body.actions) || body.actions.length === 0) {
|
||||
return NextResponse.json({ error: '`actions` must be a non-empty array' }, { status: 400 });
|
||||
}
|
||||
const actions = body.actions as ApproveActionInput[];
|
||||
|
||||
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 approveRemediationActions(id, actions, actor);
|
||||
return NextResponse.json(result);
|
||||
} catch (err) {
|
||||
if (err instanceof RemediationValidationError) {
|
||||
return NextResponse.json({ error: err.message }, { status: 400 });
|
||||
}
|
||||
if (err instanceof RemediationConflictError) {
|
||||
return NextResponse.json({ error: err.message }, { status: 409 });
|
||||
}
|
||||
console.error('[PHISHING-APPROVE] Failed to approve remediation actions', id, err);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to approve remediation actions', message: err instanceof Error ? err.message : 'Unknown error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
63
app/api/phishing/campaigns/[id]/remediate/route.ts
Normal file
63
app/api/phishing/campaigns/[id]/remediate/route.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* POST /api/phishing/campaigns/[id]/remediate
|
||||
*
|
||||
* Executes (simulated, D-01) remediation for whatever was previously approved
|
||||
* on this campaign — no request body needed, the action set was fixed at
|
||||
* approval time. Gated by phishing/remediate (D-02 — super-admin + admin
|
||||
* only). Validates the campaign id as a UUID (V5), then delegates to
|
||||
* `remediateApprovedActions` (Plan 01), which is idempotent (REMED-04) and
|
||||
* throws explicitly when nothing is approved (REMED-03).
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requirePermission } from '@/lib/auth-utils';
|
||||
import postgresClient from '@/lib/services/postgres-client';
|
||||
import {
|
||||
remediateApprovedActions,
|
||||
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', 'remediate');
|
||||
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 });
|
||||
}
|
||||
|
||||
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 remediateApprovedActions(id, actor);
|
||||
return NextResponse.json(result);
|
||||
} catch (err) {
|
||||
if (err instanceof RemediationValidationError) {
|
||||
return NextResponse.json({ error: err.message }, { status: 400 });
|
||||
}
|
||||
if (err instanceof RemediationConflictError) {
|
||||
return NextResponse.json({ error: err.message }, { status: 409 });
|
||||
}
|
||||
console.error('[PHISHING-REMEDIATE] Failed to remediate approved actions', id, err);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to remediate approved actions', message: err instanceof Error ? err.message : 'Unknown error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue