diff --git a/app/admin/sync/duo/page.tsx b/app/admin/sync/duo/page.tsx index f7c4312..43dfe8b 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, ChevronDown, ChevronUp, ShieldOff } from 'lucide-react'; +import { RefreshCw, ArrowLeft, Loader2, CheckCircle2, AlertTriangle, Shield, Users, Smartphone, ScrollText, Layers, AppWindow, ChevronDown, ChevronUp, ShieldOff, ShieldAlert, ShieldX } from 'lucide-react'; interface DuoStatus { connected: boolean; @@ -16,7 +16,8 @@ interface DuoStatus { authLogs: number; groups: number; integrations: number; - bypassed: number; + bypass: number; + disabled: number; }; } @@ -50,8 +51,10 @@ 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 [bypassUsers, setBypassUsers] = useState([]); + const [disabledUsers, setDisabledUsers] = useState([]); + const [showBypass, setShowBypass] = useState(false); + const [showDisabled, setShowDisabled] = useState(false); const fetchData = async () => { try { @@ -67,7 +70,8 @@ export default function DuoSyncPage() { } if (flaggedRes.ok) { const d = await flaggedRes.json(); - setFlaggedUsers(d.users ?? []); + setBypassUsers(d.bypass ?? []); + setDisabledUsers(d.disabled ?? []); } } catch (e) { console.error(e); @@ -151,70 +155,56 @@ export default function DuoSyncPage() { )} {/* Last sync + warnings */} -
+
Last sync: {fmtDate(status?.lastSync ?? null)} - {flaggedUsers.length > 0 && ( + {bypassUsers.length > 0 && ( + )} + {disabledUsers.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 || '—'}
-
+ {/* Bypass users — security concern */} + {showBypass && bypassUsers.length > 0 && ( + } + borderColor="border-red-500/30" + bgColor="bg-red-500/5" + headerColor="text-red-700 dark:text-red-400" + fmtDate={fmtDate} + /> + )} + + {/* Disabled users — informational */} + {showDisabled && disabledUsers.length > 0 && ( + } + borderColor="border-border" + bgColor="bg-muted/5" + headerColor="text-muted-foreground" + fmtDate={fmtDate} + /> )} {/* Parent account */} @@ -284,3 +274,60 @@ function StatCard({ icon, label, value }: { icon: React.ReactNode; label: string
); } + +function FlaggedUsersTable({ title, description, users, icon, borderColor, bgColor, headerColor, fmtDate }: { + title: string; + description: string; + users: FlaggedUser[]; + icon: React.ReactNode; + borderColor: string; + bgColor: string; + headerColor: string; + fmtDate: (d: string | null) => string; +}) { + return ( +
+
+
+ {icon} +

{title} ({users.length})

+
+

{description}

+
+
+ + + + + + + + + + + + + {users.map(u => ( + + + + + + + + + ))} + +
UserEmailAccountEnrolledLast LoginNotes
+
{u.realname || u.username}
+ {u.realname &&
{u.username}
} +
{u.email || '\u2014'}{u.account_name} + {u.is_enrolled + ? + : No + } + {u.last_login ? fmtDate(u.last_login) : 'Never'}{u.notes || '\u2014'}
+
+
+ ); +} diff --git a/app/admin/sync/page.tsx b/app/admin/sync/page.tsx index 9b2320f..6ab999e 100644 --- a/app/admin/sync/page.tsx +++ b/app/admin/sync/page.tsx @@ -176,7 +176,7 @@ export default function SyncOverviewPage() { users: c.users ?? 0, phones: c.phones ?? 0, authLogs: c.authLogs ?? 0, - bypassed: c.bypassed ?? 0, + bypass: c.bypass ?? 0, }; } return null; @@ -215,7 +215,7 @@ export default function SyncOverviewPage() { } if (id === 'duo') { if (!summary.connected) return ; - if ((summary as any).bypassed > 0) return ; + if ((summary as any).bypass > 0) return ; return ; } return ; @@ -400,10 +400,10 @@ export default function SyncOverviewPage() { Auth logs {(summary as any).authLogs?.toLocaleString()} - {(summary as any).bypassed > 0 && ( -
- Bypass / Disabled - {(summary as any).bypassed} + {(summary as any).bypass > 0 && ( +
+ Bypass (MFA skipped) + {(summary as any).bypass}
)} diff --git a/app/api/duo/status/route.ts b/app/api/duo/status/route.ts index aa36acc..d17dee2 100644 --- a/app/api/duo/status/route.ts +++ b/app/api/duo/status/route.ts @@ -14,9 +14,10 @@ export async function GET() { postgresClient.query(`SELECT MAX(synced_at) as last_sync FROM duo_accounts`), ]); - const bypassed = await postgresClient.query( - `SELECT COUNT(*) as cnt FROM duo_users WHERE status IN ('bypass', 'disabled')` - ); + const [bypassRes, disabledRes] = await Promise.all([ + postgresClient.query(`SELECT COUNT(*) as cnt FROM duo_users WHERE status = 'bypass'`), + postgresClient.query(`SELECT COUNT(*) as cnt FROM duo_users WHERE status = 'disabled'`), + ]); return NextResponse.json({ connected: true, @@ -29,7 +30,8 @@ export async function GET() { authLogs: Number(authLogs.rows[0].cnt), groups: Number(groups.rows[0].cnt), integrations: Number(integrations.rows[0].cnt), - bypassed: Number(bypassed.rows[0].cnt), + bypass: Number(bypassRes.rows[0].cnt), + disabled: Number(disabledRes.rows[0].cnt), }, }); } catch (error: any) { diff --git a/app/api/duo/users/flagged/route.ts b/app/api/duo/users/flagged/route.ts index e878195..9852367 100644 --- a/app/api/duo/users/flagged/route.ts +++ b/app/api/duo/users/flagged/route.ts @@ -6,14 +6,18 @@ export async function GET() { 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 + da.name as account_name, da.account_id as duo_account_id, + da.is_parent as is_parent_account 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 + ORDER BY da.name ASC, u.username ASC `); - return NextResponse.json({ users: result.rows, total: result.rowCount }); + const bypass = result.rows.filter((u: any) => u.status === 'bypass'); + const disabled = result.rows.filter((u: any) => u.status === 'disabled'); + + return NextResponse.json({ bypass, disabled, total: result.rowCount }); } catch (error: any) { console.error('[DuoAPI] Error fetching flagged users:', error); return NextResponse.json({ error: error.message }, { status: 500 });