feat: Add bypass/disabled users panel to Duo sync page

- Created GET /api/duo/users/flagged — returns users with status bypass or disabled, joined with account name
- Clickable warning banner expands to show full user table
- Table shows: user, email, account, status badge (yellow=bypass, red=disabled), enrolled, last login, notes
- Fixed JOIN: duo_users.duo_account_id is varchar account_id, not integer id
This commit is contained in:
lorentz 2026-03-27 11:31:31 -04:00
parent 72bdc6a241
commit 5037d64948
2 changed files with 102 additions and 6 deletions

View file

@ -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<DuoAccount[]>([]);
const [loading, setLoading] = useState(true);
const [syncing, setSyncing] = useState(false);
const [flaggedUsers, setFlaggedUsers] = useState<FlaggedUser[]>([]);
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 */}
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<span>Last sync: <strong className="text-foreground">{fmtDate(status?.lastSync ?? null)}</strong></span>
{status && status.counts.bypassed > 0 && (
<span className="flex items-center gap-1 text-yellow-600">
{flaggedUsers.length > 0 && (
<button
onClick={() => setShowFlagged(!showFlagged)}
className="flex items-center gap-1 text-yellow-600 hover:text-yellow-500 transition-colors"
>
<AlertTriangle className="w-4 h-4" />
{status.counts.bypassed} user(s) in bypass/disabled status
</span>
{flaggedUsers.length} user(s) in bypass/disabled status
{showFlagged ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
</button>
)}
</div>
{/* Bypass / Disabled users panel */}
{showFlagged && flaggedUsers.length > 0 && (
<div className="rounded-lg border border-yellow-500/30 bg-yellow-500/5 overflow-hidden">
<div className="px-4 py-3 border-b border-yellow-500/20 flex items-center gap-2">
<ShieldOff className="w-4 h-4 text-yellow-600" />
<h3 className="text-sm font-semibold text-yellow-700">Bypass / Disabled Users ({flaggedUsers.length})</h3>
</div>
<table className="w-full text-sm">
<thead className="bg-yellow-500/5">
<tr>
<th className="text-left px-4 py-2 font-medium">User</th>
<th className="text-left px-4 py-2 font-medium">Email</th>
<th className="text-left px-4 py-2 font-medium">Account</th>
<th className="text-center px-4 py-2 font-medium">Status</th>
<th className="text-center px-4 py-2 font-medium">Enrolled</th>
<th className="text-left px-4 py-2 font-medium">Last Login</th>
<th className="text-left px-4 py-2 font-medium">Notes</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{flaggedUsers.map(u => (
<tr key={u.user_id} className="hover:bg-yellow-500/5">
<td className="px-4 py-2">
<div className="font-medium">{u.realname || u.username}</div>
{u.realname && <div className="text-xs text-muted-foreground">{u.username}</div>}
</td>
<td className="px-4 py-2 text-muted-foreground">{u.email || '—'}</td>
<td className="px-4 py-2">{u.account_name}</td>
<td className="px-4 py-2 text-center">
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
u.status === 'bypass'
? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400'
: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400'
}`}>
{u.status}
</span>
</td>
<td className="px-4 py-2 text-center">
{u.is_enrolled
? <CheckCircle2 className="w-4 h-4 text-green-500 mx-auto" />
: <span className="text-muted-foreground">No</span>
}
</td>
<td className="px-4 py-2 text-muted-foreground">{u.last_login ? fmtDate(u.last_login) : 'Never'}</td>
<td className="px-4 py-2 text-muted-foreground text-xs max-w-[200px] truncate">{u.notes || '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Parent account */}
{parentAccount && (
<div className="rounded-lg border border-border p-4">