"use client" import { useState } from "react" import { Button } from "./ui/button" import { Input } from "./ui/input" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog" import { AlertCircle, CheckCircle, Copy, Shield, Check } from "lucide-react" import { getApiUrl } from "../lib/api-config" import { useT } from "../lib/i18n/provider" interface TwoFactorSetupProps { open: boolean onClose: () => void onSuccess: () => void } export function TwoFactorSetup({ open, onClose, onSuccess }: TwoFactorSetupProps) { const t = useT() const tf = (key: string) => t(`securityPage.twoFactorSetup.${key}`) const [step, setStep] = useState(1) const [qrCode, setQrCode] = useState("") const [secret, setSecret] = useState("") const [backupCodes, setBackupCodes] = useState([]) const [verificationCode, setVerificationCode] = useState("") const [error, setError] = useState("") const [loading, setLoading] = useState(false) const [copiedSecret, setCopiedSecret] = useState(false) const [copiedCodes, setCopiedCodes] = useState(false) const handleSetupStart = async () => { setError("") setLoading(true) try { const token = localStorage.getItem("proxmenux-auth-token") const response = await fetch(getApiUrl("/api/auth/totp/setup"), { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, }) const data = await response.json() if (!response.ok) { throw new Error(data.message || tf("setupFailed")) } setQrCode(data.qr_code) setSecret(data.secret) setBackupCodes(data.backup_codes) setStep(2) } catch (err) { setError(err instanceof Error ? err.message : tf("setupFailed")) } finally { setLoading(false) } } const handleVerify = async () => { if (!verificationCode || verificationCode.length !== 6) { setError(tf("enterSixDigitCode")) return } setError("") setLoading(true) try { const token = localStorage.getItem("proxmenux-auth-token") const response = await fetch(getApiUrl("/api/auth/totp/enable"), { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ token: verificationCode }), }) const data = await response.json() if (!response.ok) { throw new Error(data.message || tf("invalidCode")) } setStep(3) } catch (err) { setError(err instanceof Error ? err.message : tf("verificationFailed")) } finally { setLoading(false) } } const copyToClipboard = async (text: string, type: "secret" | "codes") => { let ok = false // Path 1: modern Clipboard API. Only works on HTTPS / localhost. try { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text) ok = true } } catch { // fall through } // Path 2: legacy execCommand. Picky — some browsers (iOS Safari // especially) refuse to copy from an element placed off-screen // (`left: -9999px`), which is the previous version's mistake. // Keep the textarea inside the viewport but visually invisible. if (!ok) { const textarea = document.createElement("textarea") textarea.value = text textarea.style.position = "fixed" textarea.style.top = "0" textarea.style.left = "0" textarea.style.width = "2em" textarea.style.height = "2em" textarea.style.padding = "0" textarea.style.border = "none" textarea.style.outline = "none" textarea.style.boxShadow = "none" textarea.style.background = "transparent" textarea.style.opacity = "0" textarea.setAttribute("readonly", "") textarea.setAttribute("aria-hidden", "true") document.body.appendChild(textarea) try { textarea.focus() textarea.select() textarea.setSelectionRange(0, text.length) ok = document.execCommand("copy") } catch { ok = false } finally { document.body.removeChild(textarea) } } // Path 3: last-resort window.prompt — ugly but unblockable. The // user can select+copy from the prompt manually. This guarantees // they can finish the 2FA setup even on plain-HTTP Monitor where // both the Clipboard API and execCommand may be locked down. if (!ok) { try { window.prompt(tf("copyPrompt"), text) ok = true } catch { // ignore } } if (!ok) { console.error("Failed to copy to clipboard") return } if (type === "secret") { setCopiedSecret(true) setTimeout(() => setCopiedSecret(false), 2000) } else { setCopiedCodes(true) setTimeout(() => setCopiedCodes(false), 2000) } } const handleClose = () => { setStep(1) setQrCode("") setSecret("") setBackupCodes([]) setVerificationCode("") setError("") onClose() } const handleFinish = () => { handleClose() onSuccess() } return ( {tf("title")} {tf("description")} {error && (

{error}

)} {step === 1 && (

{tf("intro")}

{tf("youWillNeed")}

  • {tf("needApp")}
  • {tf("needQrOrKey")}
  • {tf("needBackupCodes")}
)} {step === 2 && (

{tf("scanTitle")}

{tf("scanDescription")}

{qrCode && (
{tf("qrCodeAlt")}
)}

{tf("manualKey")}

{tf("verifyTitle")}

{tf("verifyDescription")}

setVerificationCode(e.target.value.replace(/\D/g, "").slice(0, 6))} className="text-center text-lg tracking-widest font-mono text-base" maxLength={6} disabled={loading} />
)} {step === 3 && (

{tf("enabledTitle")}

{tf("enabledDescription")}

{tf("saveCodesTitle")}

{tf("saveCodesDescription")}

{tf("backupCodes")}
{backupCodes.map((code, index) => (
{code}
))}
)}
) }