diff --git a/app/admin/sync/duo/page.tsx b/app/admin/sync/duo/page.tsx index 5715594..f7c4312 100644 --- a/app/admin/sync/duo/page.tsx +++ b/app/admin/sync/duo/page.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from 'react'; import Link from 'next/link'; import { Button } from '@/components/ui/button'; -import { RefreshCw, ArrowLeft, Loader2, CheckCircle2, AlertTriangle, Shield, Users, Smartphone, ScrollText, Layers, AppWindow } from 'lucide-react'; +import { RefreshCw, ArrowLeft, Loader2, CheckCircle2, AlertTriangle, Shield, Users, Smartphone, ScrollText, Layers, AppWindow, ChevronDown, ChevronUp, ShieldOff } from 'lucide-react'; interface DuoStatus { connected: boolean; @@ -20,6 +20,18 @@ interface DuoStatus { }; } +interface FlaggedUser { + user_id: string; + username: string; + email: string | null; + realname: string | null; + status: string; + is_enrolled: boolean; + last_login: string | null; + notes: string | null; + account_name: string; +} + interface DuoAccount { id: number; account_id: string; @@ -38,18 +50,25 @@ export default function DuoSyncPage() { const [accounts, setAccounts] = useState([]); const [loading, setLoading] = useState(true); const [syncing, setSyncing] = useState(false); + const [flaggedUsers, setFlaggedUsers] = useState([]); + const [showFlagged, setShowFlagged] = useState(false); const fetchData = async () => { try { - const [statusRes, accountsRes] = await Promise.all([ + const [statusRes, accountsRes, flaggedRes] = await Promise.all([ fetch('/api/duo/status'), fetch('/api/duo/accounts'), + fetch('/api/duo/users/flagged'), ]); if (statusRes.ok) setStatus(await statusRes.json()); if (accountsRes.ok) { const d = await accountsRes.json(); setAccounts(d.accounts ?? []); } + if (flaggedRes.ok) { + const d = await flaggedRes.json(); + setFlaggedUsers(d.users ?? []); + } } catch (e) { console.error(e); } finally { @@ -134,14 +153,70 @@ export default function DuoSyncPage() { {/* Last sync + warnings */}
Last sync: {fmtDate(status?.lastSync ?? null)} - {status && status.counts.bypassed > 0 && ( - + {flaggedUsers.length > 0 && ( + )}
+ {/* Bypass / Disabled users panel */} + {showFlagged && flaggedUsers.length > 0 && ( +
+
+ +

Bypass / Disabled Users ({flaggedUsers.length})

+
+ + + + + + + + + + + + + + {flaggedUsers.map(u => ( + + + + + + + + + + ))} + +
UserEmailAccountStatusEnrolledLast LoginNotes
+
{u.realname || u.username}
+ {u.realname &&
{u.username}
} +
{u.email || '—'}{u.account_name} + + {u.status} + + + {u.is_enrolled + ? + : No + } + {u.last_login ? fmtDate(u.last_login) : 'Never'}{u.notes || '—'}
+
+ )} + {/* Parent account */} {parentAccount && (
diff --git a/app/api/duo/users/flagged/route.ts b/app/api/duo/users/flagged/route.ts new file mode 100644 index 0000000..e878195 --- /dev/null +++ b/app/api/duo/users/flagged/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from 'next/server'; +import { postgresClient } from '@/lib/services/postgres-client'; + +export async function GET() { + try { + const result = await postgresClient.query(` + SELECT u.user_id, u.username, u.email, u.realname, u.status, + u.is_enrolled, u.last_login, u.notes, u.synced_at, + da.name as account_name, da.account_id as duo_account_id + FROM duo_users u + JOIN duo_accounts da ON da.account_id = u.duo_account_id + WHERE u.status IN ('bypass', 'disabled') + ORDER BY u.status ASC, da.name ASC, u.username ASC + `); + + return NextResponse.json({ users: result.rows, total: result.rowCount }); + } catch (error: any) { + console.error('[DuoAPI] Error fetching flagged users:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +}