'use client' import * as React from 'react' import { AlertTriangle, ChevronDown, ChevronUp } from 'lucide-react' import { useT } from '@open-mercato/shared/lib/i18n/context' import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog' import { Button } from '@open-mercato/ui/primitives/button' import { PasswordInput } from '@open-mercato/ui/primitives/password-input' import { useProviderChallengeComponent } from './mfa-ui-registry' export type SudoChallengeMethod = { type: string label: string icon: string components?: { list?: string details?: string challenge?: string } } export type PendingSudoChallenge = { sessionId: string targetIdentifier: string method: 'password' | 'mfa' availableMfaMethods: SudoChallengeMethod[] } type SudoChallengeModalProps = { open: boolean challenge: PendingSudoChallenge | null onResolve: (result: { sudoToken: string; expiresAt: string } | null) => void } type PrepareResponse = { clientData?: Record } type VerifyResponse = { sudoToken: string expiresAt: string } function readErrorMessage(error: unknown, fallback: string): string { if (error && typeof error === 'object' && 'message' in error && typeof error.message === 'string') { return error.message } return fallback } export default function SudoChallengeModal({ open, challenge, onResolve, }: SudoChallengeModalProps) { const t = useT() const [password, setPassword] = React.useState('') const [loading, setLoading] = React.useState(false) const [error, setError] = React.useState(null) const [preparedMethods, setPreparedMethods] = React.useState>({}) const [preparedClientData, setPreparedClientData] = React.useState | undefined>>({}) const [selectedMethod, setSelectedMethod] = React.useState('') const [showMoreOptions, setShowMoreOptions] = React.useState(false) React.useEffect(() => { if (!open || !challenge) return setPassword('') setError(null) setPreparedMethods({}) setPreparedClientData({}) setSelectedMethod(challenge.availableMfaMethods[0]?.type ?? '') setShowMoreOptions(false) }, [challenge, open]) const prepare = React.useCallback(async (methodType: string) => { if (!challenge) return undefined const result = await readApiResultOrThrow('/api/security/sudo/prepare', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ sessionId: challenge.sessionId, methodType, }), }) return result.clientData }, [challenge]) React.useEffect(() => { if (!challenge || challenge.method !== 'mfa' || !selectedMethod) return if (preparedMethods[selectedMethod]) return let active = true setLoading(true) setError(null) prepare(selectedMethod) .then((clientData) => { if (!active) return setPreparedClientData((current) => ({ ...current, [selectedMethod]: clientData })) setPreparedMethods((current) => ({ ...current, [selectedMethod]: true })) }) .catch((err) => { if (!active) return setError( readErrorMessage( err, t('security.admin.sudo.challenge.errors.prepare', 'Failed to prepare sudo challenge.'), ), ) }) .finally(() => { if (!active) return setLoading(false) }) return () => { active = false } }, [challenge, prepare, preparedMethods, selectedMethod, t]) const verify = React.useCallback(async (methodType: string, payload: Record) => { if (!challenge) return const result = await readApiResultOrThrow('/api/security/sudo/verify', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ sessionId: challenge.sessionId, targetIdentifier: challenge.targetIdentifier, methodType, payload, }), }) onResolve(result) }, [challenge, onResolve]) const handlePasswordVerify = React.useCallback(async () => { if (!challenge) return setLoading(true) setError(null) try { await verify('password', { password }) } catch (err) { setError( readErrorMessage( err, t('security.admin.sudo.challenge.errors.verify', 'Failed to verify sudo challenge.'), ), ) } finally { setLoading(false) } }, [challenge, password, t, verify]) const handleMfaVerify = React.useCallback(async (payload: Record) => { if (!selectedMethod) return setLoading(true) setError(null) try { await verify(selectedMethod, payload) } catch (err) { setError( readErrorMessage( err, t('security.admin.sudo.challenge.errors.verify', 'Failed to verify sudo challenge.'), ), ) } finally { setLoading(false) } }, [selectedMethod, t, verify]) const handlePrepare = React.useCallback(async () => { if (!selectedMethod) return const cached = preparedClientData[selectedMethod] if (preparedMethods[selectedMethod]) { return cached } setLoading(true) setError(null) try { const clientData = await prepare(selectedMethod) setPreparedClientData((current) => ({ ...current, [selectedMethod]: clientData })) setPreparedMethods((current) => ({ ...current, [selectedMethod]: true })) return clientData } catch (err) { setError( readErrorMessage( err, t('security.admin.sudo.challenge.errors.prepare', 'Failed to prepare sudo challenge.'), ), ) } finally { setLoading(false) } return undefined }, [prepare, preparedClientData, preparedMethods, selectedMethod, t]) const handleClose = React.useCallback(() => { onResolve(null) }, [onResolve]) const selected = challenge?.availableMfaMethods.find((method) => method.type === selectedMethod) ?? null const alternativeMethods = React.useMemo( () => challenge?.availableMfaMethods.filter((method) => method.type !== selectedMethod) ?? [], [challenge, selectedMethod], ) const ChallengeComponent = useProviderChallengeComponent(selected ?? { type: 'unknown' }) return ( { if (!nextOpen) handleClose() }}> { if ((event.metaKey || event.ctrlKey) && event.key === 'Enter' && challenge?.method === 'password') { event.preventDefault() void handlePasswordVerify() } }} > {t('security.admin.sudo.challenge.title', 'Confirm sensitive action')} {t( 'security.admin.sudo.challenge.description', 'Re-authenticate to continue with this protected action.', )} {challenge?.method === 'password' ? (
setPassword(event.target.value)} placeholder={t('security.admin.sudo.challenge.password.placeholder', 'Enter your password')} autoComplete="current-password" />
{error ?

{error}

: null}
) : challenge?.method === 'mfa' && selected ? (
{alternativeMethods.length > 0 ? (
{showMoreOptions ? (
{alternativeMethods.map((method) => ( ))}
) : null}
) : null} {error ?

{error}

: null}
) : (
{t('security.admin.sudo.challenge.noMethods', 'No sudo authentication methods are available.')}
)}
) }