Sessions 5-7a: 955 tests, deployment ready

This commit is contained in:
Kev
2026-06-08 18:35:13 -04:00
parent 06b82624a2
commit 1fa04dc776
371 changed files with 49366 additions and 955 deletions
+238
View File
@@ -0,0 +1,238 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/contexts/AuthContext';
import { getBrowserSupabase } from '@/lib/supabase';
import ExplainModeToggle from '@/components/ExplainModeToggle';
// MFA settings page. Gated to paid tiers (analyst | desk) — free users get
// redirected to /pricing. We don't *force* MFA, only strongly encourage it,
// because we don't want to lock paying users out of an account they own.
type EnrollState =
| { status: 'idle' }
| { status: 'enrolling'; factorId: string; qr: string; secret: string }
| { status: 'enrolled' }
| { status: 'error'; message: string };
export default function SecuritySettingsPage() {
const router = useRouter();
const { user, tier, loading: authLoading } = useAuth();
const [hasMFA, setHasMFA] = useState<boolean | null>(null);
const [enrollState, setEnrollState] = useState<EnrollState>({ status: 'idle' });
const [code, setCode] = useState('');
const [submitting, setSubmitting] = useState(false);
const refreshFactors = useCallback(async () => {
const supabase = getBrowserSupabase();
if (!supabase) return;
const { data, error } = await supabase.auth.mfa.listFactors();
if (error) return;
const verified = (data?.totp ?? []).some((f) => f.status === 'verified');
setHasMFA(verified);
}, []);
useEffect(() => {
if (authLoading) return;
if (!user) {
router.replace('/login?next=/settings/security');
return;
}
if (tier === 'free') {
router.replace('/upgrade/desk');
return;
}
void refreshFactors();
}, [authLoading, user, tier, router, refreshFactors]);
const startEnroll = async () => {
const supabase = getBrowserSupabase();
if (!supabase) return;
setSubmitting(true);
try {
const { data, error } = await supabase.auth.mfa.enroll({ factorType: 'totp', friendlyName: 'VYNDR' });
if (error) {
setEnrollState({ status: 'error', message: error.message });
return;
}
setEnrollState({
status: 'enrolling',
factorId: data.id,
qr: data.totp.qr_code,
secret: data.totp.secret,
});
} finally {
setSubmitting(false);
}
};
const verify = async () => {
if (enrollState.status !== 'enrolling') return;
const supabase = getBrowserSupabase();
if (!supabase) return;
setSubmitting(true);
try {
const challenge = await supabase.auth.mfa.challenge({ factorId: enrollState.factorId });
if (challenge.error) {
setEnrollState({ status: 'error', message: challenge.error.message });
return;
}
const verifyRes = await supabase.auth.mfa.verify({
factorId: enrollState.factorId,
challengeId: challenge.data.id,
code,
});
if (verifyRes.error) {
setEnrollState({ status: 'error', message: verifyRes.error.message });
return;
}
setEnrollState({ status: 'enrolled' });
setHasMFA(true);
setCode('');
} finally {
setSubmitting(false);
}
};
const disable = async () => {
const supabase = getBrowserSupabase();
if (!supabase) return;
if (!window.confirm('Disable two-factor authentication? Your account will be less secure.')) return;
setSubmitting(true);
try {
const { data } = await supabase.auth.mfa.listFactors();
const factor = (data?.totp ?? []).find((f) => f.status === 'verified');
if (!factor) return;
const res = await supabase.auth.mfa.unenroll({ factorId: factor.id });
if (res.error) {
setEnrollState({ status: 'error', message: res.error.message });
return;
}
setHasMFA(false);
} finally {
setSubmitting(false);
}
};
if (authLoading || hasMFA === null) {
return (
<div className="mx-auto max-w-2xl px-4 py-12 text-sm" style={{ color: 'var(--text-secondary)' }}>
Loading
</div>
);
}
return (
<div className="mx-auto max-w-2xl px-4 py-12">
<h1 className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
Account security
</h1>
<p className="mt-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
Two-factor authentication adds a one-time code on top of your password.
</p>
<section
className="mt-8 rounded-lg border p-5"
style={{ background: 'var(--bg-surface)', borderColor: 'var(--border-light)' }}
>
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-semibold" style={{ color: 'var(--text-primary)' }}>
Authenticator app
</div>
<div className="text-xs" style={{ color: 'var(--text-secondary)' }}>
{hasMFA ? 'Enabled — codes from your authenticator are required at sign-in.' : 'Not enabled.'}
</div>
</div>
<span
className="rounded-full px-2 py-1 text-xs font-semibold"
style={{
background: hasMFA ? 'var(--grade-a)' : 'var(--bg-elevated)',
color: hasMFA ? 'var(--bg-0)' : 'var(--text-secondary)',
}}
>
{hasMFA ? 'ON' : 'OFF'}
</span>
</div>
{!hasMFA && enrollState.status === 'idle' && (
<button
type="button"
onClick={startEnroll}
disabled={submitting}
className="mt-4 rounded px-4 py-2 text-sm font-semibold disabled:opacity-50"
style={{ background: 'var(--grade-a)', color: 'var(--bg-0)' }}
>
Set up
</button>
)}
{enrollState.status === 'enrolling' && (
<div className="mt-4 space-y-3">
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>
Scan this code with Google Authenticator, Authy, or 1Password.
</p>
{/* QR is a data: URI emitted by Supabase — safe to embed directly. */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={enrollState.qr} alt="MFA QR code" width={180} height={180} />
<p className="text-xs" style={{ color: 'var(--text-tertiary)' }}>
Can&apos;t scan? Enter this code manually: <code>{enrollState.secret}</code>
</p>
<input
inputMode="numeric"
pattern="[0-9]*"
maxLength={6}
value={code}
onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
placeholder="6-digit code"
className="w-32 rounded border px-3 py-2 text-sm"
style={{
background: 'var(--bg-elevated)',
borderColor: 'var(--border-light)',
color: 'var(--text-primary)',
}}
/>
<button
type="button"
onClick={verify}
disabled={code.length !== 6 || submitting}
className="ml-2 rounded px-4 py-2 text-sm font-semibold disabled:opacity-50"
style={{ background: 'var(--grade-a)', color: 'var(--bg-0)' }}
>
Verify
</button>
</div>
)}
{enrollState.status === 'enrolled' && (
<p className="mt-4 text-sm" style={{ color: 'var(--grade-a)' }}>
MFA enabled. You&apos;ll be asked for a code the next time you sign in.
</p>
)}
{enrollState.status === 'error' && (
<p className="mt-4 text-sm" style={{ color: 'var(--grade-d)' }}>
{enrollState.message}
</p>
)}
{hasMFA && enrollState.status !== 'enrolling' && (
<button
type="button"
onClick={disable}
disabled={submitting}
className="mt-4 rounded border px-4 py-2 text-sm font-semibold disabled:opacity-50"
style={{ borderColor: 'var(--border-light)', color: 'var(--text-secondary)' }}
>
Disable MFA
</button>
)}
</section>
<section className="mt-6">
<ExplainModeToggle variant="full" />
</section>
</div>
);
}