- Add SentinelOne API client (lib/services/sentinelone-client.ts) - Paginated fetching for sites, agents, threats - JWT token auth via S1_API_URL / S1_API_TOKEN env vars - Add SentinelOne sync service (lib/services/sentinelone-sync-service.ts) - Full sync: sites, agents, threats into s1_* tables - Sync history tracking with per-entity results - Add DB migration 038: s1_sites, s1_agents, s1_threats, s1_company_mappings, s1_sync_history tables - Add API routes: - POST/GET /api/sentinelone/sync - GET/POST/DELETE /api/sentinelone/company-mappings - GET /api/sentinelone/coverage (fixed Cartesian product bug) - Add UI pages: - /admin/sync/sentinelone — sync admin with history + stats - /sentinelone/coverage — AV coverage report per site - /sentinelone/mappings — map S1 sites to Autotask companies - Wire SentinelOne into admin sync overview card grid - Add SentinelOne Sync to app navigation - Fix docker-compose: remove explicit S1 env var entries that were overwriting env_file values with empty strings
104 lines
3.4 KiB
TypeScript
104 lines
3.4 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { postgresClient } from '@/lib/services/postgres-client';
|
|
|
|
export async function GET(request: NextRequest) {
|
|
try {
|
|
const { searchParams } = request.nextUrl;
|
|
const includeUnmapped = searchParams.get('includeUnmapped') === 'true';
|
|
|
|
const mappings = await postgresClient.query(
|
|
`SELECT m.id, m.s1_site_id, m.s1_site_name, m.company_id, m.company_name,
|
|
m.notes, m.created_at, m.updated_at,
|
|
s.state, s.active_licenses, s.health_status, s.sku
|
|
FROM s1_company_mappings m
|
|
LEFT JOIN s1_sites s ON s.id = m.s1_site_id
|
|
ORDER BY m.s1_site_name`
|
|
);
|
|
|
|
if (!includeUnmapped) {
|
|
return NextResponse.json({ mappings: mappings.rows });
|
|
}
|
|
|
|
const allSites = await postgresClient.query(
|
|
`SELECT id, name, state, active_licenses, health_status, sku FROM s1_sites ORDER BY name`
|
|
);
|
|
|
|
const mappedIds = new Set(mappings.rows.map((r: any) => r.s1_site_id));
|
|
const unmapped = allSites.rows
|
|
.filter((s: any) => !mappedIds.has(s.id))
|
|
.map((s: any) => ({
|
|
id: null,
|
|
s1_site_id: s.id,
|
|
s1_site_name: s.name,
|
|
company_id: null,
|
|
company_name: null,
|
|
notes: null,
|
|
state: s.state,
|
|
active_licenses: s.active_licenses,
|
|
health_status: s.health_status,
|
|
sku: s.sku,
|
|
}));
|
|
|
|
return NextResponse.json({
|
|
mappings: [...mappings.rows, ...unmapped],
|
|
stats: {
|
|
total: allSites.rows.length,
|
|
mapped: mappings.rows.length,
|
|
unmapped: unmapped.length,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
const msg = error instanceof Error ? error.message : String(error);
|
|
return NextResponse.json({ error: msg }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const body = await request.json();
|
|
const { s1SiteId, s1SiteName, companyId, companyName, notes = null } = body;
|
|
|
|
if (!s1SiteId || !companyId) {
|
|
return NextResponse.json(
|
|
{ error: 'Missing required fields: s1SiteId and companyId' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const result = await postgresClient.query(
|
|
`INSERT INTO s1_company_mappings (s1_site_id, s1_site_name, company_id, company_name, notes)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
ON CONFLICT (s1_site_id) DO UPDATE SET
|
|
s1_site_name = $2, company_id = $3, company_name = $4,
|
|
notes = $5, updated_at = NOW()
|
|
RETURNING *`,
|
|
[s1SiteId, s1SiteName, companyId, companyName || null, notes]
|
|
);
|
|
|
|
return NextResponse.json({ success: true, mapping: result.rows[0] });
|
|
} catch (error) {
|
|
const msg = error instanceof Error ? error.message : String(error);
|
|
return NextResponse.json({ error: msg }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
export async function DELETE(request: NextRequest) {
|
|
try {
|
|
const id = request.nextUrl.searchParams.get('id');
|
|
if (!id) return NextResponse.json({ error: 'Missing id' }, { status: 400 });
|
|
|
|
const result = await postgresClient.query(
|
|
'DELETE FROM s1_company_mappings WHERE id = $1 RETURNING *',
|
|
[id]
|
|
);
|
|
|
|
if (result.rowCount === 0) {
|
|
return NextResponse.json({ error: 'Mapping not found' }, { status: 404 });
|
|
}
|
|
|
|
return NextResponse.json({ success: true });
|
|
} catch (error) {
|
|
const msg = error instanceof Error ? error.message : String(error);
|
|
return NextResponse.json({ error: msg }, { status: 500 });
|
|
}
|
|
}
|