Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/screens/Login.tsx |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | frontend/src/screens/Login.tsx |
| Original SHA-256 | 5fcb6e07b3d55fa5cc8bc833b64b86f6ae08b64093e842b23cf39e3348736143 |
| Git blob SHA | 70456e083050a8bcf67cfc3a62050427f57ffb53 |
| Reading-copy SHA-256 | 5fcb6e07b3d55fa5cc8bc833b64b86f6ae08b64093e842b23cf39e3348736143 |
Every recorded occurrence
These 2 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/Login.tsx | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / frontend/src/screens/Login.tsx | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
Recovered source transcript
Download complete source text ↓205 lines · 7.1 KB
import { useState } from 'react'
import { Loader2, Lock, ShieldCheck } from 'lucide-react'
import { authApi, type EnrollResult } from '../lib/auth'
import { useAuth } from '../lib/useAuth'
type Step = 'credentials' | 'verify' | 'enroll'
// Login is the unauthenticated gate. Stage 1 is email and password (returns a pre MFA session cookie);
// stage 2 is mandatory TOTP, either verifying an existing enrollment or enrolling for the first time.
// On an active session it calls refresh, which flips the AuthProvider to show the app.
export function Login() {
const { refresh } = useAuth()
const [step, setStep] = useState<Step>('credentials')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [code, setCode] = useState('')
const [useRecovery, setUseRecovery] = useState(false)
const [enroll, setEnroll] = useState<EnrollResult | null>(null)
const [err, setErr] = useState('')
const [busy, setBusy] = useState(false)
async function beginEnroll() {
setBusy(true)
try {
setEnroll(await authApi.mfaEnroll())
setStep('enroll')
} catch {
setErr('Could not start MFA enrollment. Try again.')
} finally {
setBusy(false)
}
}
async function onCredentials(e: React.FormEvent) {
e.preventDefault()
setErr('')
setBusy(true)
try {
const r = await authApi.login(email, password)
if (!r.mfa_required) {
// MFA is disabled for this pilot: the session is already active, go straight in.
await refresh()
setBusy(false)
return
}
setCode('')
if (r.mfa_enrolled) {
setStep('verify')
setBusy(false)
} else {
await beginEnroll()
}
} catch {
setErr('Invalid email or password.')
setBusy(false)
}
}
async function onVerify(e: React.FormEvent) {
e.preventDefault()
setErr('')
setBusy(true)
try {
if (useRecovery) {
await authApi.mfaRecover(code.trim())
} else {
await authApi.mfaVerify(code.trim())
}
await refresh()
} catch {
setErr(useRecovery ? 'Invalid recovery code.' : 'Invalid authentication code.')
} finally {
// Clear busy on both paths so a session that does not flip the gate (for example a refresh
// that returns no active session) leaves the form usable instead of a stuck disabled button.
setBusy(false)
}
}
async function onActivate(e: React.FormEvent) {
e.preventDefault()
setErr('')
setBusy(true)
try {
await authApi.mfaActivate(code.trim())
await refresh()
} catch {
setErr('Invalid code. Make sure the time on your authenticator is correct.')
} finally {
setBusy(false)
}
}
return (
<div className="grid min-h-screen place-items-center bg-navy-950 p-4 text-ink">
<div className="w-full max-w-sm rounded-xl border border-line/70 bg-surface p-6 shadow-2xl">
<div className="mb-5 flex items-center gap-2">
<ShieldCheck className="text-gold" size={22} />
<div className="text-lg font-extrabold tracking-tight">missionMATCH™</div>
</div>
{step === 'credentials' && (
<form onSubmit={onCredentials} className="space-y-3">
<h1 className="text-sm font-bold">Sign in</h1>
<Field label="Email" type="email" value={email} onChange={setEmail} autoFocus />
<Field label="Password" type="password" value={password} onChange={setPassword} maxLength={64} />
{err && <p className="text-sm text-risk">{err}</p>}
<Submit busy={busy} icon={<Lock size={15} />} label="Continue" />
</form>
)}
{step === 'verify' && (
<form onSubmit={onVerify} className="space-y-3">
<h1 className="text-sm font-bold">Two factor authentication</h1>
<p className="text-xs text-muted">
{useRecovery
? 'Enter one of your single use recovery codes.'
: 'Enter the 6 digit code from your authenticator app.'}
</p>
<Field label={useRecovery ? 'Recovery code' : 'Authentication code'} value={code} onChange={setCode} autoFocus />
{err && <p className="text-sm text-risk">{err}</p>}
<Submit busy={busy} icon={<ShieldCheck size={15} />} label="Verify" />
<button
type="button"
onClick={() => {
setUseRecovery(!useRecovery)
setCode('')
setErr('')
}}
className="text-[11px] text-muted hover:text-ink"
>
{useRecovery ? 'Use an authenticator code instead' : 'Use a recovery code instead'}
</button>
</form>
)}
{step === 'enroll' && enroll && (
<form onSubmit={onActivate} className="space-y-3">
<h1 className="text-sm font-bold">Set up two factor authentication</h1>
<p className="text-xs text-muted">
Add this account to your authenticator app, then enter the 6 digit code it shows. MFA is
required for every account.
</p>
<div className="rounded-lg bg-navy-950/60 p-3">
<div className="text-[11px] uppercase tracking-wider text-faint">Setup key</div>
<div className="mt-1 break-all font-mono text-sm text-blue-soft">{enroll.secret}</div>
<a href={enroll.otpauth_uri} className="mt-2 inline-block text-[11px] text-muted underline">
Open in authenticator
</a>
</div>
<Field label="Authentication code" value={code} onChange={setCode} autoFocus />
{err && <p className="text-sm text-risk">{err}</p>}
<Submit busy={busy} icon={<ShieldCheck size={15} />} label="Activate and sign in" />
</form>
)}
</div>
<p className="fixed bottom-4 left-0 right-0 text-center text-[10px] text-faint">
Powered by the Adaptive Intelligence Optimization Engine™
</p>
</div>
)
}
function Field({
label,
value,
onChange,
type = 'text',
autoFocus,
maxLength,
}: {
label: string
value: string
onChange: (v: string) => void
type?: string
autoFocus?: boolean
maxLength?: number
}) {
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}
autoFocus={autoFocus}
maxLength={maxLength}
onChange={(e) => onChange(e.target.value)}
className="w-full rounded-lg border border-line/70 bg-navy-950/60 px-3 py-2 text-sm text-ink outline-none focus:border-blue/60"
/>
</label>
)
}
function Submit({ busy, icon, label }: { busy: boolean; icon: React.ReactNode; label: string }) {
return (
<button
type="submit"
disabled={busy}
className="flex w-full items-center justify-center gap-2 rounded-lg bg-gradient-to-r from-blue to-navy-700 px-4 py-2 text-sm font-bold text-white hover:brightness-110 disabled:opacity-50"
>
{busy ? <Loader2 className="animate-spin" size={15} /> : icon}
{label}
</button>
)
}