| import { useState } from "react"; | |
| import { useSecuritySettings } from "@/hooks/useSecuritySettings"; | |
| import { GlassCard } from "@/components/ui/GlassCard"; | |
| import { motion, AnimatePresence } from "framer-motion"; | |
| import { Shield, QrCode, CheckCircle, AlertCircle, Copy, Download } from "lucide-react"; | |
| import { toast } from "react-toastify"; | |
| export function TwoFactorWindow() { | |
| const { | |
| twoFactorSecret, | |
| twoFactorQrCode, | |
| twoFactorCode, | |
| setTwoFactorCode, | |
| verifyTwoFactor, | |
| twoFactorError, | |
| twoFactorSuccess, | |
| twoFactorLoading, | |
| } = useSecuritySettings(); | |
| const [step, setStep] = useState<"setup" | "verify">("setup"); | |
| const copySecret = () => { | |
| if (twoFactorSecret) { | |
| navigator.clipboard.writeText(twoFactorSecret); | |
| toast.info("Secret copied to clipboard"); | |
| } | |
| }; | |
| const downloadQr = () => { | |
| if (twoFactorQrCode) { | |
| const link = document.createElement("a"); | |
| link.href = twoFactorQrCode; | |
| link.download = "2fa-qr-code.png"; | |
| link.click(); | |
| } | |
| }; | |
| return ( | |
| <GlassCard className="p-6 w-full max-w-md mx-auto"> | |
| <motion.div | |
| initial={{ opacity: 0, y: 10 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ duration: 0.3 }} | |
| > | |
| <div className="flex items-center gap-2 mb-4"> | |
| <Shield className="w-6 h-6 text-primary" /> | |
| <h2 className="text-xl font-bold text-gray-900 dark:text-white">Two‑Factor Authentication</h2> | |
| </div> | |
| <AnimatePresence mode="wait"> | |
| {step === "setup" && ( | |
| <motion.div | |
| key="setup" | |
| initial={{ opacity: 0, x: -20 }} | |
| animate={{ opacity: 1, x: 0 }} | |
| exit={{ opacity: 0, x: 20 }} | |
| className="space-y-4 text-center" | |
| > | |
| <p className="text-sm text-gray-600 dark:text-gray-400"> | |
| Scan this QR code with Google Authenticator or any TOTP app: | |
| </p> | |
| {twoFactorQrCode && ( | |
| <div className="bg-white p-2 rounded-xl inline-block mx-auto"> | |
| <img src={twoFactorQrCode} alt="QR Code" className="w-48 h-48 mx-auto" /> | |
| </div> | |
| )} | |
| <div className="bg-gray-50 dark:bg-gray-700/50 p-3 rounded-lg"> | |
| <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Secret key (if QR fails):</p> | |
| <code className="text-sm font-mono break-all bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded"> | |
| {twoFactorSecret} | |
| </code> | |
| <button | |
| onClick={copySecret} | |
| className="ml-2 text-primary hover:text-primary/80" | |
| title="Copy secret" | |
| > | |
| <Copy className="w-4 h-4 inline" /> | |
| </button> | |
| </div> | |
| <div className="flex gap-2"> | |
| <button | |
| onClick={downloadQr} | |
| className="flex-1 flex items-center justify-center gap-2 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition" | |
| > | |
| <Download className="w-4 h-4" /> Save QR | |
| </button> | |
| <button | |
| onClick={() => setStep("verify")} | |
| className="flex-1 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition" | |
| > | |
| Next | |
| </button> | |
| </div> | |
| </motion.div> | |
| )} | |
| {step === "verify" && ( | |
| <motion.form | |
| key="verify" | |
| initial={{ opacity: 0, x: 20 }} | |
| animate={{ opacity: 1, x: 0 }} | |
| exit={{ opacity: 0, x: -20 }} | |
| onSubmit={(e) => { | |
| e.preventDefault(); | |
| verifyTwoFactor(); | |
| }} | |
| className="space-y-4" | |
| > | |
| <p className="text-sm text-gray-600 dark:text-gray-400"> | |
| Enter the 6‑digit code from your authenticator app: | |
| </p> | |
| <div className="relative"> | |
| <Shield className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" /> | |
| <input | |
| type="text" | |
| placeholder="000000" | |
| value={twoFactorCode} | |
| onChange={(e) => setTwoFactorCode(e.target.value.replace(/\D/g, "").slice(0, 6))} | |
| className="w-full pl-9 pr-3 py-2 glass-card rounded-lg text-center text-2xl tracking-widest font-mono" | |
| required | |
| autoFocus | |
| maxLength={6} | |
| /> | |
| </div> | |
| {twoFactorError && ( | |
| <div className="flex items-center gap-2 p-2 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg text-sm"> | |
| <AlertCircle className="w-4 h-4" /> | |
| <span>{twoFactorError}</span> | |
| </div> | |
| )} | |
| {twoFactorSuccess && ( | |
| <div className="flex items-center gap-2 p-2 bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400 rounded-lg text-sm"> | |
| <CheckCircle className="w-4 h-4" /> | |
| <span>{twoFactorSuccess}</span> | |
| </div> | |
| )} | |
| <button | |
| type="submit" | |
| disabled={twoFactorLoading || twoFactorCode.length !== 6} | |
| className="w-full py-2 rounded-lg bg-gradient-to-r from-primary to-secondary text-white font-semibold disabled:opacity-50 flex items-center justify-center gap-2" | |
| > | |
| {twoFactorLoading ? ( | |
| <div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" /> | |
| ) : ( | |
| <CheckCircle className="w-5 h-5" /> | |
| )} | |
| Enable 2FA | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => setStep("setup")} | |
| className="w-full text-sm text-gray-500 hover:text-primary transition" | |
| > | |
| ← Back to QR code | |
| </button> | |
| </motion.form> | |
| )} | |
| </AnimatePresence> | |
| </motion.div> | |
| </GlassCard> | |
| ); | |
| } |
Xet Storage Details
- Size:
- 6.38 kB
- Xet hash:
- f76d6a3c6cdaf692a26807f38485b4870c5701b72a55e7a0e16b053f0eb155e6
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.