JavaScript 56.7%
TypeScript 42.6%
1"use client";23import { useState } from "react";4import { Shield, ShieldOff, ShieldCheck, Copy, Smartphone } from "lucide-react";5import Button from "../ui/Button";6import Modal from "../ui/Modal";7import toast from "react-hot-toast";89interface TwoFactorSetupProps {10 enabled: boolean;11 onStatusChange: () => void;12}1314export default function TwoFactorSetup({ enabled, onStatusChange }: TwoFactorSetupProps) {15 const [qrCode, setQrCode] = useState<string | null>(null);16 const [secret, setSecret] = useState<string | null>(null);17 const [token, setToken] = useState("");18 const [loading, setLoading] = useState(false);19 const [step, setStep] = useState<"idle" | "setup" | "verify">("idle");20 const [confirmDisable, setConfirmDisable] = useState(false);2122 const startSetup = async () => {23 setLoading(true);24 try {25 const res = await fetch("/api/auth/2fa/setup", { method: "POST" });26 if (res.ok) {27 const data = await res.json();28 setQrCode(data.qrCodeUrl);29 setSecret(data.secret);30 setStep("setup");31 } else toast.error("Impossible de démarrer la configuration");32 } catch {33 toast.error("Erreur réseau");34 }35 setLoading(false);36 };3738 const verifyAndEnable = async () => {39 setLoading(true);40 try {41 const res = await fetch("/api/auth/2fa/verify", {42 method: "POST",43 headers: { "Content-Type": "application/json" },44 body: JSON.stringify({ token, enable: true }),45 });46 if (res.ok) {47 toast.success("Authentification à deux facteurs activée");48 setStep("idle");49 setQrCode(null);50 setToken("");51 onStatusChange();52 } else {53 const data = await res.json();54 toast.error(data.error || "Code invalide");55 }56 } catch {57 toast.error("Erreur réseau");58 }59 setLoading(false);60 };6162 const disable2FA = async () => {63 setLoading(true);64 const res = await fetch("/api/auth/2fa/disable", { method: "POST" });65 setLoading(false);66 setConfirmDisable(false);67 if (res.ok) {68 toast.success("2FA désactivée");69 onStatusChange();70 } else toast.error("Erreur");71 };7273 return (74 <div className="card p-4 sm:p-5">75 <div className="flex items-start gap-3">76 <div className={`w-10 h-10 rounded-2xl flex items-center justify-center shrink-0 ${enabled ? "bg-emerald-500/10 text-emerald-500" : "bg-gray-100 dark:bg-white/[0.06] text-gray-400"}`}>77 {enabled ? <ShieldCheck className="w-5 h-5" /> : <Shield className="w-5 h-5" />}78 </div>79 <div className="flex-1 min-w-0">80 <div className="flex items-center gap-2 flex-wrap">81 <h3 className="text-[15px] font-semibold text-gray-900 dark:text-white">Authentification à deux facteurs</h3>82 <span className={`text-[10.5px] font-semibold px-2 py-0.5 rounded-full uppercase tracking-wide ${enabled ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400" : "bg-gray-100 dark:bg-white/10 text-gray-500"}`}>83 {enabled ? "Activée" : "Désactivée"}84 </span>85 </div>86 <p className="text-[13px] text-gray-500 dark:text-gray-400 mt-0.5">87 {enabled ? "Un code temporaire est demandé à chaque connexion." : "Ajoutez un code temporaire (TOTP) en plus de votre mot de passe."}88 </p>89 </div>90 {step === "idle" && (91 enabled ? (92 <Button size="sm" variant="danger" onClick={() => setConfirmDisable(true)} className="shrink-0">93 <ShieldOff className="w-4 h-4" /> Désactiver94 </Button>95 ) : (96 <Button size="sm" variant="accent" onClick={startSetup} loading={loading} className="shrink-0">97 <Smartphone className="w-4 h-4" /> Activer98 </Button>99 )100 )}101 </div>102103 {step === "setup" && qrCode && (104 <div className="mt-5 grid sm:grid-cols-[auto_1fr] gap-5 items-center">105 <div className="mx-auto p-3 rounded-2xl bg-white shadow-card border border-line">106 <img src={qrCode} alt="QR Code 2FA" className="w-44 h-44 rounded-lg" />107 </div>108 <div className="space-y-3">109 <ol className="text-[13px] text-gray-600 dark:text-gray-300 space-y-1.5 list-decimal pl-4">110 <li>Ouvrez votre application d'authentification (Google Authenticator, Authy, 1Password…).</li>111 <li>Scannez le QR code ou saisissez la clé manuelle.</li>112 <li>Entrez le code à 6 chiffres pour confirmer.</li>113 </ol>114 {secret && (115 <button116 onClick={() => { navigator.clipboard.writeText(secret); toast.success("Clé copiée"); }}117 className="flex items-center gap-2 max-w-full px-3 py-2 rounded-xl bg-gray-100 dark:bg-white/[0.06] text-[12px] font-mono text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-white/10 transition-colors"118 title="Copier la clé manuelle"119 >120 <span className="truncate">{secret}</span>121 <Copy className="w-3.5 h-3.5 shrink-0 text-gray-400" />122 </button>123 )}124 <div className="flex gap-2">125 <Button variant="ghost" size="sm" onClick={() => { setStep("idle"); setQrCode(null); }}>Annuler</Button>126 <Button size="sm" onClick={() => setStep("verify")}>Continuer</Button>127 </div>128 </div>129 </div>130 )}131132 {step === "verify" && (133 <div className="mt-5 max-w-sm space-y-3">134 <p className="text-[13px] text-gray-600 dark:text-gray-300">Entrez le code à 6 chiffres affiché dans votre application.</p>135 <input136 type="text"137 inputMode="numeric"138 value={token}139 onChange={(e) => setToken(e.target.value.replace(/\D/g, "").slice(0, 6))}140 placeholder="000000"141 maxLength={6}142 className="field h-14 text-center text-2xl tracking-[0.4em] font-mono"143 autoFocus144 autoComplete="one-time-code"145 />146 <div className="flex gap-2">147 <Button variant="ghost" onClick={() => { setStep("setup"); setToken(""); }} className="flex-1">Retour</Button>148 <Button variant="accent" onClick={verifyAndEnable} loading={loading} disabled={token.length !== 6} className="flex-1">Vérifier et activer</Button>149 </div>150 </div>151 )}152153 <Modal154 isOpen={confirmDisable}155 onClose={() => setConfirmDisable(false)}156 title="Désactiver la 2FA ?"157 description="Votre compte ne sera plus protégé que par votre mot de passe."158 footer={159 <>160 <Button variant="ghost" onClick={() => setConfirmDisable(false)}>Annuler</Button>161 <Button variant="danger" onClick={disable2FA} loading={loading}><ShieldOff className="w-4 h-4" /> Désactiver</Button>162 </>163 }164 >165 <p className="text-sm text-gray-600 dark:text-gray-300">Vous pourrez la réactiver à tout moment depuis cette page.</p>166 </Modal>167 </div>168 );169}170