SPB Git forge

spb/spb-cloud

Public
10commits 1branches 0releases
3.2 MBsize
maindefault branch
3 h agolast push
JavaScript 56.7% TypeScript 42.6%
7.0 KB · 170 lines tsx
Raw Blame History
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&apos;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