diff --git a/ui/sign-in/src/SignInPage.tsx b/ui/sign-in/src/SignInPage.tsx index b0576fa..14f1419 100644 --- a/ui/sign-in/src/SignInPage.tsx +++ b/ui/sign-in/src/SignInPage.tsx @@ -2,7 +2,12 @@ import { type FormEvent, useEffect, useMemo, useState } from 'react'; import { Eye, EyeOff } from 'lucide-react'; import { Card, Input, Button, Alert, FormField } from '@cameleer/design-system'; import cameleerLogo from '@cameleer/design-system/assets/cameleer-logo.svg'; -import { signIn, startRegistration, completeRegistration } from './experience-api'; +import { + signIn, startRegistration, completeRegistration, + startForgotPassword, forgotPasswordVerifyAndReset, + verifyTotp, verifyBackupCode, submitMfa, + MfaRequiredError, +} from './experience-api'; import styles from './SignInPage.module.css'; type Mode = 'signIn' | 'register' | 'verifyCode'; diff --git a/ui/sign-in/src/experience-api.ts b/ui/sign-in/src/experience-api.ts index 042a736..7ca5f12 100644 --- a/ui/sign-in/src/experience-api.ts +++ b/ui/sign-in/src/experience-api.ts @@ -64,11 +64,27 @@ export async function verifyPassword( return data.verificationId; } +export class MfaRequiredError extends Error { + constructor() { + super('MFA verification required'); + this.name = 'MfaRequiredError'; + } +} + export async function signIn(identifier: string, password: string): Promise { await initInteraction(); const verificationId = await verifyPassword(identifier, password); await identifyUser(verificationId); - return submitInteraction(); + const res = await request('POST', '/submit'); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + if (err.code === 'user.missing_mfa') { + throw new MfaRequiredError(); + } + throw new Error(err.message || `Submit failed (${res.status})`); + } + const data = await res.json(); + return data.redirectTo; } // --- Registration --- @@ -144,3 +160,82 @@ export async function completeRegistration( await identifyUser(verifiedId); return submitInteraction(); } + +// --- Forgot Password --- + +export async function initForgotPassword(): Promise { + const res = await request('PUT', '', { interactionEvent: 'ForgotPassword' }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.message || `Failed to initialize password reset (${res.status})`); + } +} + +export async function forgotPasswordSendCode(email: string): Promise { + const res = await request('POST', '/verification/verification-code', { + identifier: { type: 'email', value: email }, + interactionEvent: 'ForgotPassword', + }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + if (res.status === 422) { + throw new Error('No account found with this email'); + } + throw new Error(err.message || `Failed to send reset code (${res.status})`); + } + const data = await res.json(); + return data.verificationId; +} + +export async function forgotPasswordVerifyAndReset( + email: string, + verificationId: string, + code: string, + newPassword: string, +): Promise { + const verifiedId = await verifyCode(email, verificationId, code); + await identifyUser(verifiedId); + await addProfile('password', newPassword); + await submitInteraction(); +} + +export async function startForgotPassword(email: string): Promise { + await initForgotPassword(); + return forgotPasswordSendCode(email); +} + +// --- MFA Verification --- + +export async function verifyTotp(code: string): Promise { + const res = await request('POST', '/verification/totp/verify', { code }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + if (res.status === 422) { + throw new Error('Invalid code, please try again'); + } + throw new Error(err.message || `TOTP verification failed (${res.status})`); + } + const data = await res.json(); + return data.verificationId; +} + +export async function verifyBackupCode(code: string): Promise { + const res = await request('POST', '/verification/backup-code/verify', { code }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + if (res.status === 422) { + const msg = err.code === 'backup_code_consumed' + ? 'This backup code has already been used' + : 'Invalid backup code'; + throw new Error(msg); + } + throw new Error(err.message || `Backup code verification failed (${res.status})`); + } + const data = await res.json(); + return data.verificationId; +} + +export async function submitMfa(verificationId: string): Promise { + await identifyUser(verificationId); + return submitInteraction(); +}