feat: add forgot-password and MFA verification Experience API functions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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<string> {
|
||||
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<void> {
|
||||
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<string> {
|
||||
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<void> {
|
||||
const verifiedId = await verifyCode(email, verificationId, code);
|
||||
await identifyUser(verifiedId);
|
||||
await addProfile('password', newPassword);
|
||||
await submitInteraction();
|
||||
}
|
||||
|
||||
export async function startForgotPassword(email: string): Promise<string> {
|
||||
await initForgotPassword();
|
||||
return forgotPasswordSendCode(email);
|
||||
}
|
||||
|
||||
// --- MFA Verification ---
|
||||
|
||||
export async function verifyTotp(code: string): Promise<string> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
await identifyUser(verificationId);
|
||||
return submitInteraction();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user