Catalyst / admin/Synapse-Sonar 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Synapse-Sonar

public

Attack Surface Simulation

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
Synapse-Sonar / synapse-sonar / components / MfaEnrollment.tsx 5541 B · main
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
"use client";

import { useState } from "react";

type Phase = "idle" | "enrolling" | "verifying" | "enabled";

export default function MfaEnrollment({
  initialEnabled = false,
  continueHref,
}: {
  initialEnabled?: boolean;
  /** When set, show a "Continue" link after enrollment (used by the mandatory gate). */
  continueHref?: string;
}) {
  const [phase, setPhase] = useState<Phase>(initialEnabled ? "enabled" : "idle");
  const [qr, setQr] = useState<string | null>(null);
  const [otpauth, setOtpauth] = useState<string | null>(null);
  const [token, setToken] = useState("");
  const [error, setError] = useState<string | null>(null);
  const [busy, setBusy] = useState(false);

  const begin = async () => {
    setBusy(true);
    setError(null);
    try {
      const res = await fetch("/api/mfa/enroll", { method: "POST" });
      const data = await res.json();
      if (!res.ok) {
        setError(data.error ?? "Could not start enrollment");
        return;
      }
      setQr(data.qrDataUrl);
      setOtpauth(data.otpauth);
      setPhase("verifying");
    } finally {
      setBusy(false);
    }
  };

  const verify = async () => {
    setBusy(true);
    setError(null);
    try {
      const res = await fetch("/api/mfa/enroll", {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ token }),
      });
      const data = await res.json();
      if (!res.ok) {
        setError(data.error ?? "Verification failed");
        return;
      }
      setPhase("enabled");
    } finally {
      setBusy(false);
    }
  };

  return (
    <div className="rounded-xl border border-slate-700/60 bg-gradient-to-b from-[#1e293b] to-[#0f172a] p-6">
      <div className="flex items-center justify-between">
        <div>
          <h2 className="text-base font-semibold text-slate-100">
            Multi-Factor Authentication
          </h2>
          <p className="text-xs text-slate-400">TOTP via any authenticator app (optional)</p>
        </div>
        <span
          className={`rounded px-2 py-0.5 text-[11px] font-medium ${
            phase === "enabled"
              ? "bg-emerald-500/15 text-emerald-300"
              : "bg-slate-500/15 text-slate-400"
          }`}
        >
          {phase === "enabled" ? "Enabled" : "Disabled"}
        </span>
      </div>

      {error && (
        <div className="mt-4 rounded-lg border border-rose-500/40 bg-rose-500/10 px-3 py-2 text-xs text-rose-300">
          {error}
        </div>
      )}

      {phase === "idle" && (
        <div className="mt-5">
          <p className="text-sm text-slate-400">
            Add a second factor to protect your account. Youll scan a QR code and
            confirm a one-time code.
          </p>
          <button
            onClick={begin}
            disabled={busy}
            className="mt-4 rounded-lg bg-synapse-violet px-4 py-2 text-sm font-semibold text-white transition hover:brightness-110 disabled:opacity-40"
          >
            {busy ? "Starting…" : "Enable MFA"}
          </button>
        </div>
      )}

      {phase === "verifying" && (
        <div className="mt-5 flex flex-col items-center gap-4 sm:flex-row sm:items-start">
          <div className="rounded-lg border border-slate-700/60 bg-white p-3">
            {qr ? (
              // eslint-disable-next-line @next/next/no-img-element
              <img src={qr} alt="MFA QR code" width={160} height={160} />
            ) : null}
          </div>
          <div className="flex-1">
            <p className="text-sm text-slate-300">
              1. Scan the QR with your authenticator app.
            </p>
            {otpauth && (
              <p className="mt-1 break-all font-mono text-[10px] text-slate-500">{otpauth}</p>
            )}
            <p className="mt-3 text-sm text-slate-300">2. Enter the 6-digit code to confirm.</p>
            <div className="mt-2 flex gap-2">
              <input
                value={token}
                onChange={(e) => setToken(e.target.value.replace(/\D/g, "").slice(0, 6))}
                placeholder="123456"
                inputMode="numeric"
                className="w-32 rounded-lg border border-slate-600/60 bg-[#05080f] px-3 py-2 text-center font-mono tracking-[0.3em] text-slate-100 outline-none focus:border-synapse-violet"
              />
              <button
                onClick={verify}
                disabled={busy || token.length !== 6}
                className="rounded-lg bg-synapse-violet px-4 py-2 text-sm font-semibold text-white transition hover:brightness-110 disabled:opacity-40"
              >
                {busy ? "Verifying…" : "Verify & enable"}
              </button>
            </div>
          </div>
        </div>
      )}

      {phase === "enabled" && (
        <div className="mt-5">
          <div className="flex items-center gap-2 text-sm text-emerald-300">
            <span className="flex h-6 w-6 items-center justify-center rounded-full bg-emerald-500/15">
              
            </span>
            MFA is active. Youll be asked for a code at every sign-in.
          </div>
          {continueHref && (
            <a
              href={continueHref}
              className="mt-4 inline-block w-full rounded-lg bg-synapse-cyan px-4 py-2.5 text-center text-sm font-semibold text-[#05080f] transition hover:brightness-110"
            >
              Continue to Synapse Sonar
            </a>
          )}
        </div>
      )}
    </div>
  );
}