import { useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { KeyRound, LogOut, ShieldCheck, UserPlus } from 'lucide-react'
import { adminApi, ROLES, type UserSummary } from '../lib/admin'
import { authApi } from '../lib/auth'
import { useAuth } from '../lib/useAuth'
import { Badge, Panel } from '../components/ui'
const isAdmin = (role: string) => role === 'owner' || role === 'admin'
export function Settings() {
const { principal, logout } = useAuth()
return (
{principal && isAdmin(principal.role) &&
}
Federated SSO (Login.gov, Entra Gov, PIV CAC) and public signup are a later sub phase. Invites are
delivered manually at pilot, the link is shown after you invite a teammate.
)
}
function AccountPanel({ onLogout, role, orgID }: { onLogout: () => void; role: string; orgID: string }) {
const [oldPw, setOldPw] = useState('')
const [newPw, setNewPw] = useState('')
const [msg, setMsg] = useState('')
const change = useMutation({
mutationFn: () => authApi.changePassword(oldPw, newPw),
onSuccess: () => {
setMsg('Password changed.')
setOldPw('')
setNewPw('')
},
onError: () => setMsg('Could not change the password. Check the current one.'),
})
const logoutAll = useMutation({ mutationFn: () => authApi.logoutAll() })
return (
{role || 'unknown role'}
org {orgID.slice(0, 8)}
logoutAll.mutate()}
className="flex items-center gap-1.5 rounded-md border border-line/70 px-3 py-1.5 text-xs font-semibold text-muted hover:text-ink"
>
{logoutAll.isSuccess ? 'All sessions revoked' : 'Sign out all sessions'}
Sign out
)
}
function TeamPanel({ meRole }: { meRole: string }) {
const qc = useQueryClient()
const users = useQuery({ queryKey: ['users'], queryFn: adminApi.listUsers })
const invalidate = () => qc.invalidateQueries({ queryKey: ['users'] })
const [email, setEmail] = useState('')
const [name, setName] = useState('')
const [role, setRole] = useState('member')
const [inviteLink, setInviteLink] = useState('')
const invite = useMutation({
mutationFn: () => adminApi.invite(email.trim(), name.trim(), role),
onSuccess: (r) => {
setInviteLink(`${window.location.origin}/accept?token=${r.token}`)
setEmail('')
setName('')
invalidate()
},
})
return (
{inviteLink && (
Send this invite link to the new teammate:
{inviteLink}
)}
User
Role
Status
MFA
{(users.data ?? []).map((u) => (
))}
{users.isLoading &&
Loading team…
}
)
}
function UserRow({ u, meRole, onChanged }: { u: UserSummary; meRole: string; onChanged: () => void }) {
const setRole = useMutation({ mutationFn: (role: string) => adminApi.changeRole(u.id, role), onSuccess: onChanged })
const setStatus = useMutation({
mutationFn: (status: 'active' | 'disabled') => adminApi.changeStatus(u.id, status),
onSuccess: onChanged,
})
const resetMfa = useMutation({ mutationFn: () => adminApi.resetMFA(u.id), onSuccess: onChanged })
const disabled = u.status === 'disabled'
return (
{u.full_name}
{u.email}
{u.role === 'owner' ? (
owner
) : (
setRole.mutate(e.target.value)}
disabled={setRole.isPending}
className="rounded-md border border-line/70 bg-navy-950/60 px-2 py-1 text-xs outline-none"
>
{ROLES.filter((r) => meRole === 'owner' || r !== 'admin').map((r) => (
{r}
))}
)}
{u.status}
{u.mfa_enrolled ? 'enrolled' : 'not enrolled'}
{u.role !== 'owner' && (
setStatus.mutate(disabled ? 'active' : 'disabled')}
disabled={setStatus.isPending}
className="rounded-md border border-line/70 px-2 py-1 text-[11px] font-semibold text-muted hover:text-ink disabled:opacity-50"
>
{disabled ? 'Enable' : 'Disable'}
resetMfa.mutate()}
disabled={resetMfa.isPending}
title="Reset MFA so the user re enrolls at next login"
className="rounded-md border border-line/70 px-2 py-1 text-[11px] font-semibold text-muted hover:text-ink disabled:opacity-50"
>
{resetMfa.isSuccess ? 'MFA reset' : 'Reset MFA'}
)}
)
}
function LabeledInput({
label,
value,
onChange,
type = 'text',
}: {
label: string
value: string
onChange: (v: string) => void
type?: string
}) {
return (
{label}
onChange(e.target.value)}
className="rounded-md border border-line/70 bg-navy-950/60 px-3 py-1.5 text-sm outline-none focus:border-blue/60"
/>
)
}