feat(appgate): add AppGate SDP integration health check and sync service
Registers AppGate as a checkConfigOnly integration-health row and public sync route, matching the existing factory + is<Name>Configured() pattern. Committed now so Phase 13's worktree-isolated executors fork from a HEAD that includes this integration-health.ts entry, since Plan 13-02 inserts the PAX8 row immediately after it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LHRgZqkzBHBbAbc3KHneuR
This commit is contained in:
parent
f70a4a36ed
commit
b168d44585
9 changed files with 998 additions and 0 deletions
33
app/api/appgate/health/route.ts
Normal file
33
app/api/appgate/health/route.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* GET /api/appgate/health
|
||||
* Live AppGate Controller probe — used by the Integration Health dashboard
|
||||
* and the admin/integrations page. Does a cheap unauthenticated reach test
|
||||
* (identity-providers/names) plus a credential check (login).
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server';
|
||||
import { requireAdmin } from '@/lib/auth-utils';
|
||||
import { getAppgateClient, isAppgateConfigured } from '@/lib/services/appgate-factory';
|
||||
|
||||
export async function GET() {
|
||||
const { error } = await requireAdmin();
|
||||
if (error) return error;
|
||||
|
||||
if (!isAppgateConfigured()) {
|
||||
return NextResponse.json({ configured: false, reachable: false, authenticated: false });
|
||||
}
|
||||
try {
|
||||
const client = getAppgateClient();
|
||||
await client.ping();
|
||||
// Force a login by hitting an authenticated endpoint.
|
||||
await client.getLicense();
|
||||
return NextResponse.json({ configured: true, reachable: true, authenticated: true });
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
const reachable = !/unreachable|ENOTFOUND|ECONNREFUSED/i.test(msg);
|
||||
return NextResponse.json(
|
||||
{ configured: true, reachable, authenticated: false, error: msg },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
}
|
||||
78
app/api/appgate/sync/route.ts
Normal file
78
app/api/appgate/sync/route.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
* AppGate sync API
|
||||
*
|
||||
* POST /api/appgate/sync — trigger a sync (body: { syncType, triggeredBy })
|
||||
* GET /api/appgate/sync — last-sync status + record counts
|
||||
*
|
||||
* No auth gate: webhook-style entrypoint hit by the in-process scheduler
|
||||
* (same pattern as `/api/qbo/sync`). Make sure it's listed in
|
||||
* `middleware.ts`'s public path array.
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import postgresClient from '@/lib/services/postgres-client';
|
||||
import { getAppgateSyncService } from '@/lib/services/appgate-sync-service';
|
||||
import { isAppgateConfigured } from '@/lib/services/appgate-factory';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
if (!isAppgateConfigured()) {
|
||||
return NextResponse.json(
|
||||
{ error: 'AppGate not configured — set APPGATE_URL/USERNAME/PASSWORD/DEVICE_ID env vars' },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const syncType: 'sessions' | 'daily' = body.syncType === 'daily' ? 'daily' : 'sessions';
|
||||
const triggeredBy = typeof body.triggeredBy === 'string' ? body.triggeredBy : 'api';
|
||||
const svc = getAppgateSyncService();
|
||||
if (svc.isSyncInProgress()) {
|
||||
return NextResponse.json({ error: 'AppGate sync already in progress' }, { status: 409 });
|
||||
}
|
||||
(syncType === 'daily' ? svc.dailySync(triggeredBy) : svc.sessionsSync(triggeredBy))
|
||||
.catch((e) => console.error('[AppgateSync API] sync failed:', e));
|
||||
return NextResponse.json({ message: `AppGate ${syncType} sync started`, triggeredBy });
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
return NextResponse.json({ error: msg }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const [sessions, devices, appliances, licenseUsers, snapshots, lastSync] = await Promise.all([
|
||||
postgresClient.query<{ count: string }>(`SELECT COUNT(*)::text as count FROM appgate_active_sessions`),
|
||||
postgresClient.query<{ count: string; deleted: string }>(
|
||||
`SELECT COUNT(*) FILTER (WHERE is_deleted=false)::text as count,
|
||||
COUNT(*) FILTER (WHERE is_deleted=true)::text as deleted
|
||||
FROM appgate_devices`),
|
||||
postgresClient.query<{ count: string }>(`SELECT COUNT(*) FILTER (WHERE is_deleted=false)::text as count FROM appgate_appliances`),
|
||||
postgresClient.query<{ count: string }>(`SELECT COUNT(*)::text as count FROM appgate_license_users`),
|
||||
postgresClient.query<{ snapshot_date: string; max_users: number; used_users: number; expiration: string | null }>(
|
||||
`SELECT snapshot_date, max_users, used_users, expiration
|
||||
FROM appgate_license_snapshots
|
||||
ORDER BY snapshot_date DESC LIMIT 1`),
|
||||
postgresClient.query<{
|
||||
sync_id: string; sync_type: string; status: string; started_at: string; completed_at: string | null; last_error: string | null;
|
||||
}>(
|
||||
`SELECT sync_id, sync_type, status, started_at, completed_at, last_error
|
||||
FROM appgate_sync_history
|
||||
ORDER BY started_at DESC LIMIT 1`),
|
||||
]);
|
||||
return NextResponse.json({
|
||||
configured: isAppgateConfigured(),
|
||||
counts: {
|
||||
active_sessions: parseInt(sessions.rows[0]?.count ?? '0', 10),
|
||||
devices: parseInt(devices.rows[0]?.count ?? '0', 10),
|
||||
devices_tombstoned: parseInt(devices.rows[0]?.deleted ?? '0', 10),
|
||||
appliances: parseInt(appliances.rows[0]?.count ?? '0', 10),
|
||||
license_users: parseInt(licenseUsers.rows[0]?.count ?? '0', 10),
|
||||
},
|
||||
latest_license: snapshots.rows[0] ?? null,
|
||||
last_sync: lastSync.rows[0] ?? null,
|
||||
});
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
return NextResponse.json({ error: msg }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue