wulf-pulse/app/api/duo/accounts/route.ts
lorentz a4242b81be 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
2026-03-27 09:18:04 -04:00

22 lines
832 B
TypeScript

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 });
}
}