import { useState } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { authListUsers, authDeactivateUser, authReactivateUser, type AuthUser } from '../../api/client'; import PageHeader from '../../components/PageHeader'; import ErrorState from '../../components/ErrorState'; import { STALE_TIME } from '../../api/queryConstants'; // ============================================================================= // Audit 2026-05-10 MED-11 closure — Federated-user admin GUI. // // Lists every federated identity in the active tenant (one row per // (oidc_provider_id, oidc_subject) tuple) with last-login + OIDC // binding visible. Admins can soft-delete a user via the Deactivate // button — server-side sets `deactivated_at` and cascade-revokes // active sessions in the same operation. The row is the OIDC binding // so destroying it would re-mint a fresh user on next login under the // same subject (losing the audit trail); deactivation preserves // forensics. // ============================================================================= export default function UsersPage() { const qc = useQueryClient(); const [providerFilter, setProviderFilter] = useState(''); const [pending, setPending] = useState(null); const [err, setErr] = useState(null); const usersQuery = useQuery({ queryKey: ['auth', 'users', providerFilter], queryFn: () => authListUsers(providerFilter || undefined), staleTime: STALE_TIME.REAL_TIME, // operator-facing user list }); async function deactivate(u: AuthUser) { if (!confirm(`Deactivate user ${u.email} (${u.id})?\n\n` + `This sets deactivated_at on the row and revokes every active session.\n` + `The row is preserved (audit trail) — a future login under the same OIDC subject will fail.`)) { return; } setPending(u.id); setErr(null); try { await authDeactivateUser(u.id); await qc.invalidateQueries({ queryKey: ['auth', 'users'] }); } catch (e) { setErr(e instanceof Error ? e.message : String(e)); } finally { setPending(null); } } // Audit 2026-05-11 A-2 — Reactivate inverse. Clears deactivated_at; // the next OIDC login under the same (provider, subject) tuple // proceeds normally. Sessions revoked at deactivation stay revoked // (the cascade is irreversible by design — the user must complete // a fresh login). async function reactivate(u: AuthUser) { if (!confirm(`Reactivate user ${u.email} (${u.id})?\n\n` + `This clears deactivated_at. The user can OIDC-login again. ` + `Previously-revoked sessions stay revoked.`)) { return; } setPending(u.id); setErr(null); try { await authReactivateUser(u.id); await qc.invalidateQueries({ queryKey: ['auth', 'users'] }); } catch (e) { setErr(e instanceof Error ? e.message : String(e)); } finally { setPending(null); } } return (
setProviderFilter(e.target.value)} style={{ width: 280, padding: 4 }} />
{err && } {usersQuery.isLoading &&

Loading users…

} {usersQuery.error && } {usersQuery.data && ( {usersQuery.data.map((u) => { const deactivated = Boolean(u.deactivated_at); return ( ); })} {usersQuery.data.length === 0 && ( )}
ID Email Display Name Provider Last Login Status Actions
{u.id} {u.email} {u.display_name} {u.oidc_provider_id} {u.last_login_at} {deactivated ? `Deactivated ${u.deactivated_at}` : 'Active'} {!deactivated && ( )} {deactivated && ( )}
No users matching filter.
)}
); }