/** * Keyboard Interactive Authentication Modal * Global modal for handling SSH keyboard-interactive authentication (2FA/MFA) * This modal displays prompts from the SSH server and collects user responses. */ import { Eye, EyeOff, KeyRound, Loader2 } from "lucide-react"; import React, { useCallback, useEffect, useState } from "react"; import { useI18n } from "../application/i18n/I18nProvider"; import { Button } from "./ui/button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "./ui/dialog"; import { Input } from "./ui/input"; import { Label } from "./ui/label"; export interface KeyboardInteractivePrompt { prompt: string; echo: boolean; } export interface KeyboardInteractiveRequest { requestId: string; name: string; instructions: string; prompts: KeyboardInteractivePrompt[]; hostname?: string; savedPassword?: string | null; } interface KeyboardInteractiveModalProps { request: KeyboardInteractiveRequest | null; onSubmit: (requestId: string, responses: string[]) => void; onCancel: (requestId: string) => void; } export const KeyboardInteractiveModal: React.FC = ({ request, onSubmit, onCancel, }) => { const { t } = useI18n(); const [responses, setResponses] = useState([]); const [showPasswords, setShowPasswords] = useState([]); const [isSubmitting, setIsSubmitting] = useState(false); // Reset state when request changes useEffect(() => { if (request) { setResponses(request.prompts.map(() => "")); setShowPasswords(request.prompts.map(() => false)); setIsSubmitting(false); } }, [request]); const handleResponseChange = useCallback((index: number, value: string) => { setResponses((prev) => { const updated = [...prev]; updated[index] = value; return updated; }); }, []); const toggleShowPassword = useCallback((index: number) => { setShowPasswords((prev) => { const updated = [...prev]; updated[index] = !updated[index]; return updated; }); }, []); const handleSubmit = useCallback(() => { if (!request || isSubmitting) return; setIsSubmitting(true); onSubmit(request.requestId, responses); }, [request, responses, onSubmit, isSubmitting]); const handleCancel = useCallback(() => { if (!request) return; onCancel(request.requestId); }, [request, onCancel]); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "Enter" && !isSubmitting) { e.preventDefault(); handleSubmit(); } }, [handleSubmit, isSubmitting] ); if (!request) return null; const title = request.name?.trim() || t("keyboard.interactive.title"); const description = request.instructions?.trim() || (request.hostname ? t("keyboard.interactive.descWithHost", { hostname: request.hostname }) : t("keyboard.interactive.desc")); return ( !open && handleCancel()}>
{title} {description}
{request.prompts.map((prompt, index) => { const isPassword = !prompt.echo; const showPassword = showPasswords[index]; // Clean up prompt text (remove trailing colon and whitespace) const promptLabel = prompt.prompt.replace(/:\s*$/, "").trim(); return (
handleResponseChange(index, e.target.value)} onKeyDown={handleKeyDown} placeholder="" className={isPassword ? "pr-10" : undefined} autoFocus={index === 0} disabled={isSubmitting} /> {isPassword && ( )}
{/* Use saved password button - shown below input, right-aligned */} {isPassword && request.savedPassword && !responses[index] && (
)}
); })}
); }; export default KeyboardInteractiveModal;