import { useCallback, useEffect, useState } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { TogglePill } from '@/components/ui/toggle-pill'; import { Separator } from '@/components/ui/separator'; import { AlertTriangle, RefreshCw, Shield, ShieldCheck } from 'lucide-react'; import { MfaEnrollDialog } from '@/components/mfa/MfaEnrollDialog'; import { MfaDisableDialog } from '@/components/mfa/MfaDisableDialog'; import { MfaBackupCodesDialog } from '@/components/mfa/MfaBackupCodesDialog'; import { toast } from '@/components/ui/toast-store'; import { apiFetch } from '@/lib/api'; interface MfaStatus { enabled: boolean; backupCodesRemaining: number; sso_enforce_mfa: boolean; } interface SSOProvider { provider: string; type: 'ldap' | 'oidc'; } export function AccountSection() { const [authData, setAuthData] = useState({ oldPassword: '', newPassword: '', confirmPassword: '' }); const [isSaving, setIsSaving] = useState(false); const [mfa, setMfa] = useState(null); const [mfaLoading, setMfaLoading] = useState(true); const [hasSso, setHasSso] = useState(false); const [enrollOpen, setEnrollOpen] = useState(false); const [disableOpen, setDisableOpen] = useState(false); const [regenOpen, setRegenOpen] = useState(false); const [togglingBypass, setTogglingBypass] = useState(false); const refreshMfa = useCallback(async () => { setMfaLoading(true); try { const res = await apiFetch('/auth/mfa/status', { localOnly: true }); if (res.ok) setMfa(await res.json()); } catch { // Non-fatal; surface as disabled card. } finally { setMfaLoading(false); } }, []); useEffect(() => { refreshMfa(); apiFetch('/auth/sso/providers', { localOnly: true }) .then((r) => (r.ok ? r.json() : [])) .then((providers: SSOProvider[]) => setHasSso(providers.length > 0)) .catch(() => setHasSso(false)); }, [refreshMfa]); const handlePasswordChange = async () => { if (!authData.oldPassword || !authData.newPassword || !authData.confirmPassword) { toast.error('All fields are required'); return; } if (authData.newPassword !== authData.confirmPassword) { toast.error('New passwords do not match'); return; } if (authData.newPassword.length < 8) { toast.error('New password must be at least 8 characters'); return; } setIsSaving(true); try { const res = await apiFetch('/auth/password', { method: 'PUT', body: JSON.stringify({ oldPassword: authData.oldPassword, newPassword: authData.newPassword }), }); if (res.ok) { toast.success('Password updated successfully'); setAuthData({ oldPassword: '', newPassword: '', confirmPassword: '' }); } else { const data = await res.json().catch(() => ({})); toast.error(data?.error || 'Failed to update password'); } } catch (e: unknown) { toast.error((e as Error)?.message || 'Network error during password change'); } finally { setIsSaving(false); } }; const handleBypassToggle = async (enforce: boolean) => { setTogglingBypass(true); try { const res = await apiFetch('/auth/mfa/sso-bypass', { method: 'PUT', localOnly: true, body: JSON.stringify({ enforce }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); toast.error(data?.error || data?.message || 'Could not update SSO preference'); return; } setMfa((prev) => (prev ? { ...prev, sso_enforce_mfa: enforce } : prev)); } catch (err) { const e = err as { message?: string; error?: string } | undefined; toast.error(e?.message || e?.error || 'Could not update SSO preference'); } finally { setTogglingBypass(false); } }; return (
setAuthData({ ...authData, oldPassword: e.target.value })} />
setAuthData({ ...authData, newPassword: e.target.value })} />
setAuthData({ ...authData, confirmPassword: e.target.value })} />
{/* Two-factor authentication card */}
{mfa?.enabled ? : }

Two-factor authentication

{mfa?.enabled && Enabled}

{mfa?.enabled ? 'Sign-in requires a code from your authenticator app. Back up your codes somewhere safe.' : 'Add a time-based one-time password to your account for an extra layer of security.'}

{mfaLoading ? (
Loading…
) : mfa?.enabled ? (
{mfa.backupCodesRemaining === 0 ? (
No backup codes left
Regenerate a new set before you lose access to your authenticator app. Without codes, recovery needs an administrator.
) : mfa.backupCodesRemaining <= 2 ? (
{mfa.backupCodesRemaining} backup code{mfa.backupCodesRemaining === 1 ? '' : 's'} remaining, regenerate a fresh set
) : (
{mfa.backupCodesRemaining} backup codes remaining
)} {hasSso && (
Require 2FA even when signing in via SSO
SSO logins skip the second factor by default.
)}
) : (
)}
); }