- RMM Overshell (migration 077): admin page, dispatch UI, executor/worker, target resolver, script registry (AD/DHCP/DNS/event-log/services/software/network/loglift) - LogLift evidence pipeline (migration 078): upload webhook, B2 storage client, receiver/matcher, EventLogCollector PowerShell script - IT Glue audit + write-back (migrations 075, 076): asset-audit runner, ticket xrefs, applications/configurations browse pages + apply/revert/audit endpoints - Link-aware analyzer bundles (migration 073) + provider toggle (migration 074): link-discovery service, OpenRouter LLM provider, related-tickets/itglue-suggestion panels, analyze-bundle endpoint - Endpoint data model + device-link reconciliation (migrations 079, 080): conflicts admin page, reconciler service, resolve endpoints - Dashboard overhaul: integration-health service + alerts, overview/health endpoints - Permissions: add itglue + rmm scopes; middleware: public /api/rmm/loglift route Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
98 lines
3 KiB
TypeScript
98 lines
3 KiB
TypeScript
/**
|
|
* POST /api/admin/device-link-conflicts/[id]/resolve
|
|
* Body: { ciId: string, note?: string }
|
|
*
|
|
* Resolves a conflict by manually picking a configuration_item to link the
|
|
* underlying device_external_ids row to. Sets link_confidence='manual' and
|
|
* marks the review row resolved.
|
|
*
|
|
* Validates that ciId is in candidate_ci_ids — admins can't pick an arbitrary
|
|
* CI here. (For an arbitrary-CI override, separate flow.)
|
|
*/
|
|
|
|
import { NextRequest, NextResponse } from 'next/server';
|
|
import { z } from 'zod';
|
|
import { requirePermission } from '@/lib/auth-utils';
|
|
import postgresClient from '@/lib/services/postgres-client';
|
|
|
|
const ResolveBody = z.object({
|
|
ciId: z.string().regex(/^\d+$/, 'ciId must be numeric'),
|
|
note: z.string().max(500).optional(),
|
|
});
|
|
|
|
export async function POST(
|
|
request: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
const { session, error } = await requirePermission('admin', 'access');
|
|
if (error) return error;
|
|
|
|
const { id } = await params;
|
|
if (!/^[0-9a-f-]{36}$/i.test(id)) {
|
|
return NextResponse.json({ error: 'Invalid review id' }, { status: 400 });
|
|
}
|
|
|
|
let body: unknown;
|
|
try {
|
|
body = await request.json();
|
|
} catch {
|
|
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
|
|
}
|
|
const parsed = ResolveBody.safeParse(body);
|
|
if (!parsed.success) {
|
|
return NextResponse.json(
|
|
{ error: 'Invalid payload', details: parsed.error.flatten() },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const ciIdNum = Number(parsed.data.ciId);
|
|
|
|
return postgresClient.transaction(async (tx) => {
|
|
const reviewRes = await tx.query<{
|
|
device_external_id: string;
|
|
candidate_ci_ids: string[];
|
|
resolved_at: string | null;
|
|
}>(
|
|
`SELECT device_external_id::text, candidate_ci_ids::text[], resolved_at::text
|
|
FROM device_link_review
|
|
WHERE id = $1
|
|
FOR UPDATE`,
|
|
[id]
|
|
);
|
|
if (reviewRes.rowCount === 0) {
|
|
return NextResponse.json({ error: 'Review not found' }, { status: 404 });
|
|
}
|
|
const review = reviewRes.rows[0];
|
|
if (review.resolved_at) {
|
|
return NextResponse.json({ error: 'Already resolved' }, { status: 409 });
|
|
}
|
|
if (!review.candidate_ci_ids.map(String).includes(String(ciIdNum))) {
|
|
return NextResponse.json(
|
|
{ error: 'ciId must be one of the conflict candidates' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
await tx.query(
|
|
`UPDATE device_external_ids
|
|
SET configuration_item_id = $2,
|
|
link_confidence = 'manual',
|
|
linked_at = NOW()
|
|
WHERE id = $1`,
|
|
[review.device_external_id, ciIdNum]
|
|
);
|
|
|
|
await tx.query(
|
|
`UPDATE device_link_review
|
|
SET resolved_at = NOW(),
|
|
resolved_by_user_id = $2,
|
|
resolved_to_ci_id = $3,
|
|
resolution_note = $4
|
|
WHERE id = $1`,
|
|
[id, session?.user?.id ?? null, ciIdNum, parsed.data.note ?? null]
|
|
);
|
|
|
|
return NextResponse.json({ ok: true, resolvedToCiId: String(ciIdNum) });
|
|
});
|
|
}
|