Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/screens/Settings.tsx |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 09015496d565a172bcaa15cfe9a8e4b072b13df0 |
| Branch | workstream-b-screenmatch |
| Path or archive member | frontend/src/screens/Settings.tsx |
| Original SHA-256 | 7d8d14eb634072776b102a23851bf6a1880fe94e0de9fd3ef89a5af64ee956e0 |
| Git blob SHA | 6ce74aa8022f145cbfa6ff8b337519a2c8c4b275 |
| Reading-copy SHA-256 | 7d8d14eb634072776b102a23851bf6a1880fe94e0de9fd3ef89a5af64ee956e0 |
Every recorded occurrence
These 57 records contain identical original source bytes. Their paths and revisions remain part of the archive.
| Original record | Branch or archive | Revision / identifier | Original location |
|---|---|---|---|
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | workstream-b-screenmatch | 09015496d565a172bcaa15cfe9a8e4b072b13df0 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | master | 1e9249840a4302ec82e123b9c9cb6f75badea3e4 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | workstream-d-missionmatch | 3be46ba89edb737182075a2b320509b42dc916f8 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | m5-price-to-win | 41000ad30fff4129fdaf5ec84e15ba14c27d4890 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | workstream-e-dataflow-deploy | 433815a042f9741411e60fa553fe72143e9b10db | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | w3-ai-staffing-recommendations | 43d56387270ef4da4cabd794d97ca6a49909bc7d | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | ws3-clearance | 4b8e7ce594c068621867f9c9deed0ec64ae6c553 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | collision-deconfliction | 4ee5a09972608ed89f8377c6665542d5f450f0a6 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | workstream-c-slatematch | 5cd60c918301b46ecd6ed3e03e20c395da0062e2 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | ai-intelligence-layer | 5d5ddff70393a197c5f54a45280bb5dd42b24755 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | ws7-federal-signal | 6a9f6ce9cfb13345ffb47770d4960e7c2d9c17c2 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | ws6-retrieval | 6c56e29231ec05735927fa265c97d517499dfeaf | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | per-tenant-routing | 9bde0f9a74b89d01980fa658e5253acda7528d0c | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | ai-brain-live | aac32fc49c878cc5bca099459307611562d0bac1 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | graphrag-foundation | b0ddeb9f633fd138d09309d1a17b7d13bc3a67f7 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | ws1-ontology-sync | b596f449a32a1016fbd403c36505cc00a1ac9ee6 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | time-to-fill-survival | b66e1e505863d4f17741a4b9ede73509e45bd6c6 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | ruthless-debug-sweep | b7d742f8f032984be3817efffa3eb8b13b651c1f | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | mag-substrate | b82c7100a492dec67dfd1eda6b3f53def859be35 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | capture-readiness | bae6af7cb2997e819174a9e9cfff5e16067a1975 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | deploy-and-ops | bdacd9d0b4d5998d25fc74bf4007f4c3ec538c7a | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | ws8-teaching-loop | c823f0fb6bb2aba0f39001eb6de96f4000d9321f | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | phase2-eval-fairness | d361af254701975fe239927656ababb56374655e | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | phase2-connectors | e038d4c687aec16f8bd9bc4d907eea14d91f8704 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | workstream-a-shared-core | e0de03ac3a1ec826bd15d0e048e3b02baa23eeff | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | ws2-alias-relationship | e4dada4601c6eb101830f9d223a9655dfa120616 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | presentation-readiness | eb574acaa29d71ce4d0f852572e87c275c79fdb7 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | ws9-bench-ingestion | f585a9ef32fa554663ce1fba9a414a54b6e49cf0 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | vendor-intelligence | fe9796a19c261951d3f2565206c0c1b5ac294465 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | per-tenant-routing | 9bde0f9a74b89d01980fa658e5253acda7528d0c | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | collision-deconfliction | 4ee5a09972608ed89f8377c6665542d5f450f0a6 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | ws6-retrieval | 6c56e29231ec05735927fa265c97d517499dfeaf | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | workstream-a-shared-core | e0de03ac3a1ec826bd15d0e048e3b02baa23eeff | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | m5-price-to-win | 41000ad30fff4129fdaf5ec84e15ba14c27d4890 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | workstream-c-slatematch | 5cd60c918301b46ecd6ed3e03e20c395da0062e2 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | ws1-ontology-sync | b596f449a32a1016fbd403c36505cc00a1ac9ee6 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | master | 1e9249840a4302ec82e123b9c9cb6f75badea3e4 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | ws8-teaching-loop | c823f0fb6bb2aba0f39001eb6de96f4000d9321f | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | w3-ai-staffing-recommendations | 43d56387270ef4da4cabd794d97ca6a49909bc7d | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | mag-substrate | b82c7100a492dec67dfd1eda6b3f53def859be35 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | ws3-clearance | 4b8e7ce594c068621867f9c9deed0ec64ae6c553 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | phase2-eval-fairness | d361af254701975fe239927656ababb56374655e | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | workstream-e-dataflow-deploy | 433815a042f9741411e60fa553fe72143e9b10db | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | ruthless-debug-sweep | b7d742f8f032984be3817efffa3eb8b13b651c1f | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | time-to-fill-survival | b66e1e505863d4f17741a4b9ede73509e45bd6c6 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | ai-intelligence-layer | 5d5ddff70393a197c5f54a45280bb5dd42b24755 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | graphrag-foundation | b0ddeb9f633fd138d09309d1a17b7d13bc3a67f7 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | deploy-and-ops | bdacd9d0b4d5998d25fc74bf4007f4c3ec538c7a | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | ws9-bench-ingestion | f585a9ef32fa554663ce1fba9a414a54b6e49cf0 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | ws2-alias-relationship | e4dada4601c6eb101830f9d223a9655dfa120616 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | presentation-readiness | eb574acaa29d71ce4d0f852572e87c275c79fdb7 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | workstream-b-screenmatch | 09015496d565a172bcaa15cfe9a8e4b072b13df0 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | ws7-federal-signal | 6a9f6ce9cfb13345ffb47770d4960e7c2d9c17c2 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | ai-brain-live | aac32fc49c878cc5bca099459307611562d0bac1 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | workstream-d-missionmatch | 3be46ba89edb737182075a2b320509b42dc916f8 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | phase2-connectors | e038d4c687aec16f8bd9bc4d907eea14d91f8704 | Open original |
| Janady13/missionmatch / frontend/src/screens/Settings.tsx | capture-readiness | bae6af7cb2997e819174a9e9cfff5e16067a1975 | Open original |
Recovered source transcript
Download complete source text ↓286 lines · 10.4 KB
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 (
<div className="space-y-4">
<AccountPanel onLogout={logout} role={principal?.role ?? ''} orgID={principal?.org_id ?? ''} />
{principal && isAdmin(principal.role) && <TeamPanel meRole={principal.role} />}
<p className="px-1 text-[11px] text-faint">
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.
</p>
</div>
)
}
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 (
<Panel title="Your account">
<div className="flex flex-wrap items-center gap-2 text-sm">
<Badge color="var(--color-blue)" subtle>
{role || 'unknown role'}
</Badge>
<span className="text-[11px] text-faint">org {orgID.slice(0, 8)}</span>
</div>
<form
className="mt-3 max-w-sm space-y-2"
onSubmit={(e) => {
e.preventDefault()
setMsg('')
if (newPw.length < 8) {
setMsg('Use at least 8 characters.')
return
}
change.mutate()
}}
>
<div className="text-[11px] font-bold uppercase tracking-wider text-muted">Change password</div>
<label className="block">
<span className="sr-only">Current password</span>
<input
type="password"
placeholder="Current password"
value={oldPw}
maxLength={64}
onChange={(e) => setOldPw(e.target.value)}
className="w-full rounded-md border border-line/70 bg-navy-950/60 px-3 py-1.5 text-sm outline-none focus:border-blue/60"
/>
</label>
<label className="block">
<span className="sr-only">New password</span>
<input
type="password"
placeholder="New password"
value={newPw}
maxLength={64}
onChange={(e) => setNewPw(e.target.value)}
className="w-full rounded-md border border-line/70 bg-navy-950/60 px-3 py-1.5 text-sm outline-none focus:border-blue/60"
/>
</label>
{msg && <p className="text-[11px] text-muted">{msg}</p>}
<button
type="submit"
disabled={change.isPending}
className="flex items-center gap-1.5 rounded-md bg-blue/20 px-3 py-1.5 text-sm font-semibold text-blue-soft hover:bg-blue/30 disabled:opacity-50"
>
<KeyRound size={14} /> Update password
</button>
</form>
<div className="mt-4 flex flex-wrap gap-2">
<button
onClick={() => 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"
>
<ShieldCheck size={13} /> {logoutAll.isSuccess ? 'All sessions revoked' : 'Sign out all sessions'}
</button>
<button
onClick={onLogout}
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:border-risk/60 hover:text-risk"
>
<LogOut size={13} /> Sign out
</button>
</div>
</Panel>
)
}
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 (
<Panel title="Team" hint="owner and admin only">
<form
className="flex flex-wrap items-end gap-2"
onSubmit={(e) => {
e.preventDefault()
if (email.trim() && name.trim()) {
invite.mutate()
}
}}
>
<LabeledInput label="Email" value={email} onChange={setEmail} type="email" />
<LabeledInput label="Full name" value={name} onChange={setName} />
<div>
<div className="mb-1 text-[11px] font-semibold uppercase tracking-wider text-muted">Role</div>
<select
value={role}
onChange={(e) => setRole(e.target.value)}
className="rounded-md border border-line/70 bg-navy-950/60 px-2 py-1.5 text-sm outline-none"
>
{ROLES.filter((r) => meRole === 'owner' || r !== 'admin').map((r) => (
<option key={r} value={r}>
{r}
</option>
))}
</select>
</div>
<button
type="submit"
disabled={invite.isPending}
className="flex items-center gap-1.5 rounded-md bg-blue/20 px-3 py-1.5 text-sm font-semibold text-blue-soft hover:bg-blue/30 disabled:opacity-50"
>
<UserPlus size={14} /> Invite
</button>
</form>
{inviteLink && (
<div className="mt-2 rounded-md bg-navy-950/60 p-2 text-[11px]">
<span className="text-faint">Send this invite link to the new teammate: </span>
<span className="break-all font-mono text-blue-soft">{inviteLink}</span>
</div>
)}
<div className="mt-4 overflow-x-auto">
<table className="w-full min-w-[44rem] text-sm">
<thead>
<tr className="text-left text-[11px] uppercase tracking-wider text-faint">
<th className="pb-2 font-semibold">User</th>
<th className="pb-2 font-semibold">Role</th>
<th className="pb-2 font-semibold">Status</th>
<th className="pb-2 font-semibold">MFA</th>
<th className="pb-2" />
</tr>
</thead>
<tbody className="divide-y divide-line/50">
{(users.data ?? []).map((u) => (
<UserRow key={u.id} u={u} meRole={meRole} onChanged={invalidate} />
))}
</tbody>
</table>
{users.isLoading && <p className="py-3 text-sm text-muted">Loading team…</p>}
</div>
</Panel>
)
}
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 (
<tr className="align-middle">
<td className="py-2.5 pr-3">
<div className="font-semibold">{u.full_name}</div>
<div className="text-[11px] text-faint">{u.email}</div>
</td>
<td className="py-2.5 pr-3">
{u.role === 'owner' ? (
<Badge color="var(--color-gold)" subtle>
owner
</Badge>
) : (
<select
value={u.role}
onChange={(e) => 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) => (
<option key={r} value={r}>
{r}
</option>
))}
</select>
)}
</td>
<td className="py-2.5 pr-3">
<Badge color={disabled ? 'var(--color-risk)' : 'var(--color-ok)'} subtle>
{u.status}
</Badge>
</td>
<td className="py-2.5 pr-3 text-[11px] text-muted">{u.mfa_enrolled ? 'enrolled' : 'not enrolled'}</td>
<td className="py-2.5 text-right">
{u.role !== 'owner' && (
<div className="inline-flex items-center gap-1.5">
<button
onClick={() => 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'}
</button>
<button
onClick={() => 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'}
</button>
</div>
)}
</td>
</tr>
)
}
function LabeledInput({
label,
value,
onChange,
type = 'text',
}: {
label: string
value: string
onChange: (v: string) => void
type?: string
}) {
return (
<label className="block">
<span className="mb-1 block text-[11px] font-semibold uppercase tracking-wider text-muted">{label}</span>
<input
type={type}
value={value}
onChange={(e) => 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"
/>
</label>
)
}