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 / LoginForm.tsx 4957 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
"use client";

import { Suspense, useState } from "react";
import { signIn } from "next-auth/react";
import { useRouter, useSearchParams } from "next/navigation";
import Logo from "@/components/Logo";

function LoginInner() {
  const router = useRouter();
  const params = useSearchParams();

  const [form, setForm] = useState({
    username: params.get("u") ?? "",
    password: "",
    totp: "",
  });
  const [needsMfa, setNeedsMfa] = useState(false);
  const [submitting, setSubmitting] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const set = (k: keyof typeof form, v: string) => setForm((f) => ({ ...f, [k]: v }));

  const submit = async (e: React.FormEvent) => {
    e.preventDefault();
    setSubmitting(true);
    setError(null);

    const res = await signIn("credentials", {
      ...form,
      redirect: false,
    });

    setSubmitting(false);

    if (res?.ok) {
      router.push("/");
      return;
    }

    // The server only asks for a code when the account actually has MFA enabled.
    // Anything else is a plain credential failure — don't imply MFA.
    if (res?.error === "MFA_REQUIRED") {
      setNeedsMfa(true);
      setError("Enter the 6-digit code from your authenticator app.");
    } else if (res?.error === "MFA_INVALID") {
      setNeedsMfa(true);
      setError("That code is incorrect or expired. Try again.");
    } else {
      setError("Invalid username or password.");
    }
  };

  return (
    <div className="flex min-h-screen items-center justify-center bg-[#05080f] px-4">
      <form
        onSubmit={submit}
        className="w-full max-w-md rounded-2xl border border-slate-700/60 bg-gradient-to-b from-[#1e293b] to-[#0f172a] p-8 shadow-2xl"
      >
        <div className="flex justify-center">
          <Logo variant="full" className="h-28 drop-shadow-[0_0_24px_rgba(59,130,246,0.35)]" />
        </div>
        <h1 className="mt-6 text-center text-xl font-semibold text-slate-100">Sign in</h1>
        <p className="mt-1 text-center text-sm text-slate-400">
          Access your security command center.
        </p>

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

        <div className="mt-5 space-y-4">
          <Field label="Username">
            <input
              value={form.username}
              onChange={(e) => set("username", e.target.value)}
              placeholder="admin"
              autoCapitalize="none"
              autoCorrect="off"
              className={`${inputCls} font-mono`}
              required
            />
          </Field>
          <Field label="Password">
            <input
              type="password"
              value={form.password}
              onChange={(e) => set("password", e.target.value)}
              placeholder="••••••••••"
              className={inputCls}
              required
            />
          </Field>

          {needsMfa && (
            <Field label="MFA code" hint="6-digit code from your authenticator app">
              <input
                value={form.totp}
                onChange={(e) => set("totp", e.target.value.replace(/\D/g, "").slice(0, 6))}
                placeholder="123456"
                inputMode="numeric"
                className={`${inputCls} font-mono tracking-[0.4em]`}
              />
            </Field>
          )}

          <button
            type="submit"
            disabled={submitting}
            className="w-full rounded-lg bg-synapse-cyan px-4 py-2.5 text-sm font-semibold text-[#05080f] transition hover:brightness-110 disabled:opacity-40"
          >
            {submitting ? "Signing in…" : "Sign in"}
          </button>

          {!needsMfa && (
            <button
              type="button"
              onClick={() => setNeedsMfa(true)}
              className="w-full text-center text-xs text-slate-500 transition hover:text-slate-300"
            >
              Using an authenticator app? Enter a code
            </button>
          )}
        </div>
      </form>
    </div>
  );
}

export default function LoginForm() {
  // useSearchParams requires a Suspense boundary in the app router.
  return (
    <Suspense fallback={<div className="min-h-screen bg-[#05080f]" />}>
      <LoginInner />
    </Suspense>
  );
}

const inputCls =
  "w-full rounded-lg border border-slate-600/60 bg-[#05080f] px-3 py-2 text-sm text-slate-100 outline-none transition placeholder:text-slate-600 focus:border-synapse-cyan";

function Field({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) {
  return (
    <div>
      <label className="mb-1.5 block text-xs font-medium text-slate-300">{label}</label>
      {children}
      {hint && <span className="mt-1 block text-[11px] text-slate-500">{hint}</span>}
    </div>
  );
}