wulf-pulse/app/api/rmm/site-mappings/route.ts
root 6eee14f8af Add comprehensive admin features and multi-system integration
- Add admin dashboard with sync controls and data browser
- Implement RMM, Auvik, and Addigy organization mappings
- Add chunked ticket sync with progress tracking
- Implement entity sync service with rate limiting
- Add analytics engine and performance optimizer
- Create data browser for all PSA entities
- Add navigation components and UI improvements
- Implement background processing and sync services
- Add comprehensive documentation and migration scripts
- Update configuration items with multi-system support
- Enhance contact management and purchase history
- Add issue type assignment and LLM analyzer
- Improve error handling and logging utilities
2025-11-19 14:18:16 -05:00

323 lines
8.4 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { Pool } from 'pg';
import { getDattoRMMClient } from '@/lib/services/datto-rmm-factory';
import { DattoRMMSite } from '@/lib/types/datto-rmm';
const pool = new Pool({
host: process.env.POSTGRES_HOST,
port: parseInt(process.env.POSTGRES_PORT || '5432'),
database: process.env.POSTGRES_DB,
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
});
interface RMMSiteMapping {
id: number;
company_id: number;
company_name?: string;
rmm_site_uid: string;
rmm_site_name: string;
is_primary: boolean;
device_count: number;
notes: string | null;
last_sync_at: string | null;
created_at: string;
updated_at: string;
created_by: string | null;
}
// GET /api/rmm/site-mappings
// Get all RMM site mappings, optionally including unmapped sites
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const includeUnmapped = searchParams.get('includeUnmapped') === 'true';
const companyId = searchParams.get('companyId');
// Get all existing mappings
let query = `
SELECT
rsm.id,
rsm.company_id,
rsm.rmm_site_uid,
rsm.rmm_site_name,
rsm.is_primary,
rsm.device_count,
rsm.notes,
rsm.last_sync_at,
rsm.created_at,
rsm.updated_at,
rsm.created_by,
c.company_name
FROM rmm_site_mappings rsm
JOIN companies c ON c.id = rsm.company_id
`;
const queryParams: any[] = [];
if (companyId) {
query += ' WHERE rsm.company_id = $1';
queryParams.push(companyId);
}
query += ' ORDER BY c.company_name, rsm.rmm_site_name';
const mappingsResult = await pool.query(query, queryParams);
const mappings = mappingsResult.rows;
if (!includeUnmapped) {
return NextResponse.json({ mappings });
}
// Get all RMM sites from the RMM API
const rmmClient = getDattoRMMClient();
const allSites = await rmmClient.getSites();
// Get list of already mapped site UIDs
const mappedSiteUids = new Set(mappings.map((m: any) => m.rmm_site_uid));
// Create mapping entries for unmapped sites
const unmappedSites = allSites
.filter((site: DattoRMMSite) => !mappedSiteUids.has(site.uid))
.map((site: DattoRMMSite) => ({
id: null,
company_id: null,
company_name: null,
rmm_site_uid: site.uid,
rmm_site_name: site.name,
is_primary: false,
device_count: 0,
notes: null,
last_sync_at: null,
created_at: null,
updated_at: null,
created_by: null,
}));
// Combine mapped and unmapped sites
const allMappings = [...mappings, ...unmappedSites];
// Sort by mapping status (mapped first), then by site name
allMappings.sort((a, b) => {
if (a.company_id && !b.company_id) return -1;
if (!a.company_id && b.company_id) return 1;
return (a.rmm_site_name || '').localeCompare(b.rmm_site_name || '');
});
return NextResponse.json({
mappings: allMappings,
stats: {
total: allMappings.length,
mapped: mappings.length,
unmapped: unmappedSites.length
}
});
} catch (error) {
console.error('Error fetching RMM site mappings:', error);
return NextResponse.json(
{ error: 'Failed to fetch RMM site mappings' },
{ status: 500 }
);
}
}
// POST /api/rmm/site-mappings
// Create or update an RMM site mapping
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const {
rmmSiteUid,
rmmSiteName,
companyId,
companyName,
isPrimary = false,
notes = null,
createdBy = 'system'
} = body;
if (!rmmSiteUid || !rmmSiteName || !companyId) {
return NextResponse.json(
{ error: 'Missing required fields: rmmSiteUid, rmmSiteName, and companyId are required' },
{ status: 400 }
);
}
// If setting as primary, unset other primary sites for this company
if (isPrimary) {
await pool.query(
'UPDATE rmm_site_mappings SET is_primary = false WHERE company_id = $1',
[companyId]
);
}
// Insert or update the mapping
const query = `
INSERT INTO rmm_site_mappings (
company_id,
rmm_site_uid,
rmm_site_name,
is_primary,
notes,
created_by
)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (company_id, rmm_site_uid)
DO UPDATE SET
rmm_site_name = EXCLUDED.rmm_site_name,
is_primary = EXCLUDED.is_primary,
notes = EXCLUDED.notes,
updated_at = CURRENT_TIMESTAMP
RETURNING *
`;
const result = await pool.query(query, [
companyId,
rmmSiteUid,
rmmSiteName,
isPrimary,
notes,
createdBy
]);
return NextResponse.json({
success: true,
mapping: result.rows[0]
});
} catch (error) {
console.error('Error saving RMM site mapping:', error);
return NextResponse.json(
{ error: 'Failed to save RMM site mapping' },
{ status: 500 }
);
}
}
// DELETE /api/rmm/site-mappings
// Delete an RMM site mapping
export async function DELETE(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const id = searchParams.get('id');
if (!id) {
return NextResponse.json(
{ error: 'Missing required parameter: id' },
{ status: 400 }
);
}
const result = await pool.query(
'DELETE FROM rmm_site_mappings WHERE id = $1 RETURNING *',
[id]
);
if (result.rowCount === 0) {
return NextResponse.json(
{ error: 'Mapping not found' },
{ status: 404 }
);
}
return NextResponse.json({
success: true,
deleted: result.rows[0]
});
} catch (error) {
console.error('Error deleting RMM site mapping:', error);
return NextResponse.json(
{ error: 'Failed to delete RMM site mapping' },
{ status: 500 }
);
}
}
// PUT /api/rmm/site-mappings/bulk
// Create multiple mappings at once
export async function PUT(request: NextRequest) {
try {
const body = await request.json();
const { mappings, createdBy = 'system' } = body;
if (!mappings || !Array.isArray(mappings)) {
return NextResponse.json(
{ error: 'Missing required field: mappings (array)' },
{ status: 400 }
);
}
const client = await pool.connect();
try {
await client.query('BEGIN');
const results = [];
for (const mapping of mappings) {
const {
rmmSiteUid,
rmmSiteName,
companyId,
isPrimary = false,
notes = null
} = mapping;
if (!rmmSiteUid || !rmmSiteName || !companyId) {
await client.query('ROLLBACK');
return NextResponse.json(
{ error: 'Each mapping must have rmmSiteUid, rmmSiteName, and companyId' },
{ status: 400 }
);
}
// If setting as primary, unset other primary sites for this company
if (isPrimary) {
await client.query(
'UPDATE rmm_site_mappings SET is_primary = false WHERE company_id = $1',
[companyId]
);
}
const result = await client.query(
`
INSERT INTO rmm_site_mappings (
company_id,
rmm_site_uid,
rmm_site_name,
is_primary,
notes,
created_by
)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (company_id, rmm_site_uid)
DO UPDATE SET
rmm_site_name = EXCLUDED.rmm_site_name,
is_primary = EXCLUDED.is_primary,
notes = EXCLUDED.notes,
updated_at = CURRENT_TIMESTAMP
RETURNING *
`,
[companyId, rmmSiteUid, rmmSiteName, isPrimary, notes, createdBy]
);
results.push(result.rows[0]);
}
await client.query('COMMIT');
return NextResponse.json({
success: true,
mappings: results
});
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
} catch (error) {
console.error('Error saving bulk RMM site mappings:', error);
return NextResponse.json(
{ error: 'Failed to save bulk RMM site mappings' },
{ status: 500 }
);
}
}