'use client' import * as React from 'react' import { formatDistanceToNow } from 'date-fns' import { Loader2, Smartphone, Trash2 } from 'lucide-react' import Image from 'next/image' import { useRouter } from 'next/navigation.js' import { useT } from '@open-mercato/shared/lib/i18n/context' import { flash } from '@open-mercato/ui/backend/FlashMessages' import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall' import { Button } from '@open-mercato/ui/primitives/button' import { IconButton } from '@open-mercato/ui/primitives/icon-button' import { Input } from '@open-mercato/ui/primitives/input' import type { MfaMethod } from '../types' import MfaConfiguredBadge from './mfa-provider-list-items/MfaConfiguredBadge' type TotpSetupResponse = { setupId?: string clientData?: Record secret?: string uri?: string qrDataUrl?: string } type TotpSetupState = { setupId: string secret: string | null uri: string | null qrDataUrl: string | null } type TotpConfirmResponse = { ok?: boolean } type TotpProviderDetailsProps = { methods?: MfaMethod[] saving?: boolean onRemoveMethod?: (method: MfaMethod) => Promise | void onMethodsChanged?: () => Promise | void } function readNonEmptyString(value: unknown): string | null { if (typeof value !== 'string') return null const trimmed = value.trim() return trimmed.length > 0 ? trimmed : null } function normalizeTotpSetupResponse(response: TotpSetupResponse): TotpSetupState { const setupId = readNonEmptyString(response.setupId) if (!setupId) { throw new Error('Missing setupId') } const clientData = response.clientData && typeof response.clientData === 'object' ? response.clientData : {} return { setupId, secret: readNonEmptyString(clientData.secret) ?? readNonEmptyString(response.secret), uri: readNonEmptyString(clientData.uri) ?? readNonEmptyString(response.uri), qrDataUrl: readNonEmptyString(clientData.qrDataUrl) ?? readNonEmptyString(response.qrDataUrl), } } function formatRelative(value: string | null, fallback: string): string { if (!value) return fallback const parsed = new Date(value) if (Number.isNaN(parsed.getTime())) return fallback return formatDistanceToNow(parsed, { addSuffix: true }) } export default function TotpProviderDetails({ methods = [], saving = false, onRemoveMethod, onMethodsChanged, }: TotpProviderDetailsProps) { const t = useT() const router = useRouter() const [loading, setLoading] = React.useState(false) const [setup, setSetup] = React.useState(null) const [setupError, setSetupError] = React.useState(null) const [showManualSecret, setShowManualSecret] = React.useState(false) const [code, setCode] = React.useState('') const hasConfiguredMethod = methods.length > 0 const startSetup = React.useCallback(async () => { if (hasConfiguredMethod) return setLoading(true) setSetupError(null) try { const result = await readApiResultOrThrow( '/api/security/mfa/provider/totp', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({}), }, ) setSetup(normalizeTotpSetupResponse(result)) setShowManualSecret(false) setCode('') } catch (error) { const message = error instanceof Error ? error.message : t('security.profile.mfa.totp.setupError', 'Failed to prepare authenticator setup.') setSetupError(message) } finally { setLoading(false) } }, [hasConfiguredMethod, t]) const confirmSetup = React.useCallback(async () => { if (!setup || hasConfiguredMethod || code.trim().length === 0) return setLoading(true) setSetupError(null) try { await readApiResultOrThrow( '/api/security/mfa/provider/totp', { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ setupId: setup.setupId, payload: { code: code.trim() } }), }, ) setCode('') setSetup(null) setShowManualSecret(false) flash(t('security.profile.mfa.totp.confirmSuccess', 'TOTP method enabled.'), 'success') if (onMethodsChanged) { try { await onMethodsChanged() } catch { router.push('/backend/profile/security/mfa') } } else { router.push('/backend/profile/security/mfa') } } catch (error) { const message = error instanceof Error ? error.message : t('security.profile.mfa.totp.confirmError', 'Failed to enable authenticator app.') setSetupError(message) } finally { setLoading(false) } }, [code, hasConfiguredMethod, onMethodsChanged, router, setup, t]) React.useEffect(() => { if (hasConfiguredMethod || setup != null) return void startSetup() }, [hasConfiguredMethod, setup, startSetup]) const handleCancel = React.useCallback(() => { router.push('/backend/profile/security/mfa') }, [router]) return (

{t( 'security.profile.mfa.providers.totp.description', 'Use an authenticator app or browser extension to get two-factor authentication codes when prompted.', )}

{hasConfiguredMethod ? (
{methods.map((method) => (

{method.label ?? t('security.profile.mfa.providers.totp.title', 'Authenticator app')}

{t('ui.table.registeredAt', 'Registered')}:{' '} {formatRelative( method.createdAt, t('security.profile.mfa.method.unknownTime', 'Unknown'), )}

{onRemoveMethod ? ( { void onRemoveMethod(method) }} > ) : null}
))}
) : ( <>

{t('security.profile.mfa.totp.scanTitle', 'Scan the QR code')}

{t( 'security.profile.mfa.totp.scanInstructions', 'Use an authenticator app or browser extension to scan this code.', )}

{setup?.qrDataUrl ? ( {t('security.profile.mfa.totp.qrAlt', ) : (
{loading ? t('security.profile.mfa.totp.loadingQr', 'Preparing QR code...') : t('security.profile.mfa.totp.loadingQrFallback', 'QR code unavailable')}
)}

{t( 'security.profile.mfa.totp.manualHint', 'Unable to scan? You can use a setup key to configure your authenticator app.', )}

{showManualSecret && setup?.secret ? (
{setup.secret}
) : null}

{t('security.profile.mfa.totp.verifyTitle', 'Verify the code from the app')}

setCode(event.target.value)} placeholder={t('security.profile.mfa.totp.codePlaceholder', 'XXXXXX')} maxLength={6} inputMode="numeric" autoComplete="one-time-code" className="h-10 max-w-xs border-slate-700 bg-slate-950 text-slate-100 placeholder:text-slate-500" />
)} {setupError ? (

{setupError}

) : null}
) }