80 lines
2.4 KiB
TypeScript
80 lines
2.4 KiB
TypeScript
'use client';
|
|
|
|
import { useMemo, useState } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { createSupabaseBrowserClient } from '@/lib/supabase/browser';
|
|
import {
|
|
PasskeyEnrollPanel,
|
|
RecoveryCodesPanel,
|
|
TotpEnrollPanel,
|
|
} from '../mfa-components';
|
|
|
|
/**
|
|
* Drives the mandatory first-factor enrollment: pick TOTP and/or passkey, then
|
|
* (once a factor is verified and the session is aal2) generate and display the
|
|
* one-time recovery codes before releasing the user to the dashboard.
|
|
*/
|
|
export function EnrollClient() {
|
|
const router = useRouter();
|
|
const supabase = useMemo(() => createSupabaseBrowserClient(), []);
|
|
const [phase, setPhase] = useState<'choose' | 'recovery'>('choose');
|
|
const [codes, setCodes] = useState<string[]>([]);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
async function handleVerified() {
|
|
setError(null);
|
|
try {
|
|
const res = await fetch('/api/security/recovery/generate', {
|
|
method: 'POST',
|
|
});
|
|
const json = (await res.json()) as { codes?: string[]; error?: string };
|
|
if (!res.ok || !json.codes) {
|
|
throw new Error(json.error || 'Could not generate recovery codes.');
|
|
}
|
|
setCodes(json.codes);
|
|
} catch (e) {
|
|
// The factor is already enabled; surface the error but still let the
|
|
// user proceed (they can regenerate codes later in Security settings).
|
|
setError(
|
|
`${(e as Error).message} You can generate recovery codes later in Security settings.`,
|
|
);
|
|
setCodes([]);
|
|
} finally {
|
|
setPhase('recovery');
|
|
}
|
|
}
|
|
|
|
function finish() {
|
|
router.replace('/dashboard');
|
|
router.refresh();
|
|
}
|
|
|
|
if (phase === 'recovery') {
|
|
return (
|
|
<div className="stack">
|
|
{error && <p className="error">{error}</p>}
|
|
{codes.length > 0 ? (
|
|
<RecoveryCodesPanel codes={codes} />
|
|
) : (
|
|
<div className="card">
|
|
<h2>Two-factor authentication enabled</h2>
|
|
<p className="muted">Your account is now protected.</p>
|
|
</div>
|
|
)}
|
|
<div className="row">
|
|
<button type="button" onClick={finish}>
|
|
Continue to dashboard
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="stack">
|
|
<TotpEnrollPanel supabase={supabase} onVerified={handleVerified} />
|
|
<PasskeyEnrollPanel supabase={supabase} onVerified={handleVerified} />
|
|
</div>
|
|
);
|
|
}
|