admin / Synapse-Sonar
publicAttack Surface Simulation
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> ); } |