Files
linumiq_net-web_app/app/security/enroll/enroll-client.tsx
T

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>
);
}