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:
parent
72bdc6a241
commit
5037d64948
2 changed files with 102 additions and 6 deletions
|
|
@ -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">
|
||||
|
|
|
|||
21
app/api/duo/users/flagged/route.ts
Normal file
21
app/api/duo/users/flagged/route.ts
Normal file
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue