admin / Bid-Sentinel
publicBid Scrape and Tracking Application with AI Capability
Bid-Sentinel / bid-sentinel-v2 / frontend / src / pages / Login.jsx
7151 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 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { getSetupStatus } from "../api/client"; import { useAuth } from "../context/AuthContext.jsx"; import ThemeToggle from "../components/ThemeToggle.jsx"; import logo from "../assets/bid-sentinel-logo.png"; export default function Login() { const { login, setup } = useAuth(); const navigate = useNavigate(); // mode: "loading" | "setup" | "login" const [mode, setMode] = useState("loading"); const [email, setEmail] = useState(""); const [fullName, setFullName] = useState(""); const [password, setPassword] = useState(""); const [confirm, setConfirm] = useState(""); const [error, setError] = useState(""); const [submitting, setSubmitting] = useState(false); // On load, ask the backend whether the first-run admin setup is required. function checkSetup() { setMode("loading"); setError(""); getSetupStatus() .then((s) => setMode(s.needs_setup ? "setup" : "login")) .catch(() => { // Don't silently drop to a dead sign-in form — surface the connectivity // problem so it's clear the backend can't be reached. setError( "Cannot reach the Bid Sentinel server. Check the backend is running and reachable, then retry." ); setMode("error"); }); } useEffect(checkSetup, []); async function handleLogin(e) { e.preventDefault(); setError(""); setSubmitting(true); try { await login(email, password); navigate("/", { replace: true }); } catch (err) { setError(err.response?.data?.detail || "Login failed. Check your credentials."); } finally { setSubmitting(false); } } async function handleSetup(e) { e.preventDefault(); setError(""); if (password.length < 8) { setError("Password must be at least 8 characters."); return; } if (password !== confirm) { setError("Passwords do not match."); return; } setSubmitting(true); try { await setup({ email, full_name: fullName, password }); navigate("/", { replace: true }); } catch (err) { setError(err.response?.data?.detail || "Could not create the admin account."); // If setup was already completed elsewhere, switch to login. if (err.response?.status === 409) setMode("login"); } finally { setSubmitting(false); } } const inputClass = "w-full rounded-lg border border-slate-300 px-3 py-2 outline-none focus:border-brand focus:ring-2 focus:ring-brand/30"; return ( <div className="relative flex h-full items-center justify-center px-4"> <div className="absolute right-4 top-4"> <ThemeToggle variant="ghost" /> </div> <div className="w-full max-w-md rounded-2xl bg-white p-8 shadow-xl"> <div className="mb-6 text-center"> <img src={logo} alt="Bid Sentinel" className="mx-auto mb-2 h-28 w-auto object-contain" /> <p className="text-sm text-slate-500"> UK Public & Private Sector Cyber Security Opportunities </p> </div> {error && ( <div className="mb-4 rounded-lg bg-red-50 px-4 py-2 text-sm text-red-700">{error}</div> )} {mode === "loading" && ( <p className="py-6 text-center text-sm text-slate-400">Loading…</p> )} {mode === "error" && ( <button onClick={checkSetup} className="w-full rounded-lg bg-brand py-2.5 font-semibold text-white transition hover:bg-brand-dark" > Retry </button> )} {mode === "setup" && ( <> <div className="mb-4 rounded-lg bg-blue-50 px-4 py-3 text-sm text-blue-800"> <p className="font-semibold">Welcome to Bid Sentinel</p> <p>No accounts exist yet. Create the administrator account to get started.</p> </div> <form onSubmit={handleSetup} className="space-y-4"> <Field label="Full name"> <input type="text" value={fullName} onChange={(e) => setFullName(e.target.value)} className={inputClass} placeholder="Jane Doe" /> </Field> <Field label="Email"> <input type="email" required value={email} onChange={(e) => setEmail(e.target.value)} className={inputClass} placeholder="[email protected]" /> </Field> <Field label="Password (min 8 characters)"> <input type="password" required value={password} onChange={(e) => setPassword(e.target.value)} className={inputClass} placeholder="••••••••" /> </Field> <Field label="Confirm password"> <input type="password" required value={confirm} onChange={(e) => setConfirm(e.target.value)} className={inputClass} placeholder="••••••••" /> </Field> <button type="submit" disabled={submitting} className="w-full rounded-lg bg-brand py-2.5 font-semibold text-white transition hover:bg-brand-dark disabled:opacity-60" > {submitting ? "Creating account…" : "Create admin account"} </button> </form> </> )} {mode === "login" && ( <form onSubmit={handleLogin} className="space-y-4"> <Field label="Email"> <input type="email" required value={email} onChange={(e) => setEmail(e.target.value)} className={inputClass} placeholder="[email protected]" /> </Field> <Field label="Password"> <input type="password" required value={password} onChange={(e) => setPassword(e.target.value)} className={inputClass} placeholder="••••••••" /> </Field> <button type="submit" disabled={submitting} className="w-full rounded-lg bg-brand py-2.5 font-semibold text-white transition hover:bg-brand-dark disabled:opacity-60" > {submitting ? "Signing in…" : "Sign in"} </button> </form> )} </div> </div> ); } function Field({ label, children }) { return ( <div> <label className="mb-1 block text-sm font-medium text-slate-700">{label}</label> {children} </div> ); } |