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 | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 |
| Branch | feature/aioe-upgrade |
| Path or archive member | frontend/src/screens/Settings.tsx |
| Original SHA-256 | 72d5d63f60516d6b9bf94ea08756fd19d25f10035589782c277c1fe98d757580 |
| Git blob SHA | 04e72d29abbb8a2b0fe5795be1a3605f9ecde2d2 |
| Reading-copy SHA-256 | 72d5d63f60516d6b9bf94ea08756fd19d25f10035589782c277c1fe98d757580 |
Recovered source transcript
Download complete source text ↓131 lines · 5.2 KB
import { useState } from 'react'
import { useMutation } from '@tanstack/react-query'
import { KeyRound, LogOut, ShieldCheck } from 'lucide-react'
import { Link } from 'react-router-dom'
import { authApi } from '../lib/auth'
import { useAuth } from '../lib/useAuth'
import { CAP, can } from '../lib/can'
import { TimeZonePanel } from './TimeZonePanel'
import { Badge, Panel } from '../components/ui'
export function Settings() {
const { principal, logout } = useAuth()
return (
<div className="space-y-4">
<AccountPanel onLogout={logout} role={principal?.role ?? ''} orgID={principal?.org_id ?? ''} />
{/* Account management lives on ONE screen. Settings used to carry its own Team panel: a second
user list on a different query key, whose invite sent no tool set and so created accounts
entitled to nothing, and whose role changes did not invalidate the other screen. Two surfaces
for the same thing disagreed about what access a person had, which is the worst possible thing
for an access control screen to be ambiguous about. */}
{can(principal, CAP.userInvite) && (
<Panel title="Accounts and access" hint="people, roles, and tools">
<p className="text-sm text-muted">
Create accounts, set roles, and choose which tools each person can see.
</p>
<Link
to="/admin"
className="mt-3 inline-flex rounded-lg border border-line/70 px-3 py-2 text-sm font-semibold text-muted hover:text-ink"
>
Open accounts and access
</Link>
</Panel>
)}
{/* Owner only: the server refuses it for anyone else, so offering it would be a button that fails. */}
{can(principal, CAP.orgSettings) && <TimeZonePanel />}
<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>
)
}