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:
hsiegeln
2026-04-26 13:39:31 +02:00
parent cfcf852e2d
commit 08a3ad03b7
2 changed files with 102 additions and 2 deletions

View File

@@ -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';

View File

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