feat: Duo Security integration — full data sync from Accounts + Admin API
Duo API Client (lib/services/duo-client.ts): - HMAC-SHA1 request signing, GET/POST, automatic pagination - Rate-limit handling (429 + Retry-After), configurable timeout - Accounts API: listAccounts() via POST /accounts/v1/account/list - Admin API: getUsers, getPhones, getGroups, getIntegrations, getAuthLogs - Child account access: parent creds signed against child api_hostname + account_id - Factory helpers: getDuoAccountsClient(), getDuoAdminClient() Database (migration 058): - 6 tables: duo_accounts, duo_users, duo_phones, duo_auth_logs, duo_groups, duo_integrations - All with proper FKs, indexes, JSONB fields for capabilities/location/groups Sync Service (lib/services/duo-sync-service.ts): - syncAll(): accounts → per-child data + auth logs → parent account → company matching - Sequential child processing to respect rate limits - Incremental auth logs (mintime = last synced timestamp, default 30 days) - Company matching: exact → case-insensitive containment (30/32 = 94% matched) - Non-blocking with sync ID tracking API Routes: - POST/GET /api/duo/sync — trigger sync / check status - GET /api/duo/accounts — list all accounts with stats + matched company - GET /api/duo/accounts/[id]/users — users for a specific account - POST /api/openclaw/sync/duo — OpenClaw trigger with API key auth Results: 33 accounts, 832 users, 925 phones, 5927 auth logs, 46 groups, 78 integrations
This commit is contained in:
parent
9a448d111c
commit
a4242b81be
10 changed files with 1565 additions and 0 deletions
23
app/api/duo/accounts/[id]/users/route.ts
Normal file
23
app/api/duo/accounts/[id]/users/route.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { postgresClient } from '@/lib/services/postgres-client';
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
|
||||
const result = await postgresClient.query(`
|
||||
SELECT u.user_id, u.username, u.email, u.realname, u.status,
|
||||
u.is_enrolled, u.last_login, u.last_directory_sync, u.created,
|
||||
u.phones_count, u.groups, u.aliases, u.enable_auto_prompt, u.notes,
|
||||
u.synced_at
|
||||
FROM duo_users u
|
||||
WHERE u.duo_account_id = $1
|
||||
ORDER BY u.username ASC
|
||||
`, [id]);
|
||||
|
||||
return NextResponse.json({ users: result.rows, total: result.rowCount });
|
||||
} catch (error: any) {
|
||||
console.error('[DuoAPI] Error fetching users:', error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
22
app/api/duo/accounts/route.ts
Normal file
22
app/api/duo/accounts/route.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { postgresClient } from '@/lib/services/postgres-client';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const result = await postgresClient.query(`
|
||||
SELECT da.id, da.account_id, da.name, da.api_hostname,
|
||||
da.user_count, da.integration_count, da.edition,
|
||||
da.is_parent, da.synced_at, da.created_at,
|
||||
da.autotask_company_id,
|
||||
c.company_name as autotask_company_name
|
||||
FROM duo_accounts da
|
||||
LEFT JOIN companies c ON c.id = da.autotask_company_id
|
||||
ORDER BY da.is_parent DESC, da.name ASC
|
||||
`);
|
||||
|
||||
return NextResponse.json({ accounts: result.rows });
|
||||
} catch (error: any) {
|
||||
console.error('[DuoAPI] Error fetching accounts:', error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
31
app/api/duo/sync/route.ts
Normal file
31
app/api/duo/sync/route.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { duoSyncService } from '@/lib/services/duo-sync-service';
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
if (duoSyncService.isSyncInProgress()) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Duo sync already in progress', syncId: duoSyncService.getCurrentSyncId() },
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
|
||||
// Fire-and-forget — don't await
|
||||
const syncId = `duo_sync_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
duoSyncService.syncAll('api').catch(err => {
|
||||
console.error('[DuoSync] Background sync error:', err);
|
||||
});
|
||||
|
||||
return NextResponse.json({ message: 'Duo sync started', syncId });
|
||||
} catch (error: any) {
|
||||
console.error('[DuoSync] Error starting sync:', error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
inProgress: duoSyncService.isSyncInProgress(),
|
||||
currentSyncId: duoSyncService.getCurrentSyncId(),
|
||||
});
|
||||
}
|
||||
29
app/api/openclaw/sync/duo/route.ts
Normal file
29
app/api/openclaw/sync/duo/route.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { validateOpenClawKey } from '@/lib/utils/openclaw-auth';
|
||||
import { duoSyncService } from '@/lib/services/duo-sync-service';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const authError = validateOpenClawKey(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
if (duoSyncService.isSyncInProgress()) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Duo sync already in progress', syncId: duoSyncService.getCurrentSyncId() },
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
|
||||
duoSyncService.syncAll('openclaw').catch(err => {
|
||||
console.error('[DuoSync] Background sync error (openclaw):', err);
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'Duo sync started',
|
||||
syncId: duoSyncService.getCurrentSyncId(),
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[DuoSync] Error starting sync via OpenClaw:', error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue