Spaces:
Build error
Build error
| import { useEffect, useState } from "react"; | |
| import { api } from "../api.js"; | |
| import { setSession } from "../utils/auth.js"; | |
| const SQLI_RE = /(\b(select|insert|update|delete|drop|union|exec|cast|convert|declare|xp_|char|nchar|varchar|alter|create|truncate|sleep|benchmark|waitfor|information_schema)\b|--|;|\/\*|\*\/|0x[0-9a-fA-F]+|\bor\b\s+.{0,30}[=<>]|\band\b\s+.{0,30}[=<>])/i; | |
| function hasSQLI(val) { return SQLI_RE.test(val); } | |
| function getPasswordStrength(pw) { | |
| const score = [/.{8,}/, /[A-Z]/, /[a-z]/, /[0-9]/, /[^A-Za-z0-9]/].filter(r => r.test(pw)).length; | |
| const widths = ['0%', '25%', '45%', '70%', '100%']; | |
| const colors = ['#ef4444', '#f97316', '#eab308', '#22c55e', '#7c3aed']; | |
| return { width: widths[score] || '0%', color: colors[score - 1] || '#334155' }; | |
| } | |
| export default function AuthModal({ open, onClose, onAuthed }) { | |
| const [mode, setMode] = useState("login"); // "login" | "register" | |
| const [identifier, setIdentifier] = useState(""); | |
| const [username, setUsername] = useState(""); | |
| const [email, setEmail] = useState(""); | |
| const [password, setPassword] = useState(""); | |
| const [error, setError] = useState(""); | |
| const [busy, setBusy] = useState(false); | |
| const [pwStrength, setPwStrength] = useState({ width: '0%', color: '#334155' }); | |
| // Reset state + close on Escape. | |
| useEffect(() => { | |
| if (!open) return; | |
| setError(""); | |
| setBusy(false); | |
| setPassword(""); | |
| const onKey = (e) => e.key === "Escape" && onClose(); | |
| window.addEventListener("keydown", onKey); | |
| return () => window.removeEventListener("keydown", onKey); | |
| }, [open, onClose]); | |
| if (!open) return null; | |
| async function handleSubmit(e) { | |
| e.preventDefault(); | |
| setError(""); | |
| if (mode === "login") { | |
| if (!identifier.trim() || !password) { | |
| setError("Enter your username/email and password."); | |
| return; | |
| } | |
| if (hasSQLI(identifier) || hasSQLI(password)) { | |
| setError("Invalid input detected."); | |
| return; | |
| } | |
| } else { | |
| if (username.trim().length < 3) { | |
| setError("Username must be at least 3 characters."); | |
| return; | |
| } | |
| if (username.trim().length > 15) { | |
| setError("Username must be 15 characters or less."); | |
| return; | |
| } | |
| if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email.trim())) { | |
| setError("Enter a valid email address."); | |
| return; | |
| } | |
| if (email.trim().length > 20) { | |
| setError("Email must be 20 characters or less."); | |
| return; | |
| } | |
| if (password.length < 6) { | |
| setError("Password must be at least 6 characters."); | |
| return; | |
| } | |
| if (hasSQLI(username) || hasSQLI(email) || hasSQLI(password)) { | |
| setError("Invalid input detected."); | |
| return; | |
| } | |
| } | |
| setBusy(true); | |
| try { | |
| const res = | |
| mode === "login" | |
| ? await api.authLogin(identifier.trim(), password) | |
| : await api.authRegister(username.trim(), email.trim(), password); | |
| setSession(res.token, res.user); | |
| onAuthed?.(res.user); | |
| onClose(); | |
| } catch (err) { | |
| setError(err.message || "Something went wrong. Try again."); | |
| } finally { | |
| setBusy(false); | |
| } | |
| } | |
| return ( | |
| <div className="auth-overlay" onClick={onClose}> | |
| <div className="auth-modal" onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true"> | |
| <button className="auth-modal-close" onClick={onClose} aria-label="Close"> | |
| ✕ | |
| </button> | |
| <div className="auth-modal-logo"><span className="auth-modal-logo-text">ANICOVE</span></div> | |
| <div className="auth-tabs"> | |
| <button | |
| className={`auth-tab${mode === "login" ? " active" : ""}`} | |
| onClick={() => { setMode("login"); setError(""); }} | |
| > | |
| Sign in | |
| </button> | |
| <button | |
| className={`auth-tab${mode === "register" ? " active" : ""}`} | |
| onClick={() => { setMode("register"); setError(""); }} | |
| > | |
| Create account | |
| </button> | |
| </div> | |
| <form className="auth-form" onSubmit={handleSubmit}> | |
| {mode === "login" ? ( | |
| <label className="auth-field"> | |
| <span>Username or email</span> | |
| <input | |
| type="text" | |
| value={identifier} | |
| onChange={(e) => setIdentifier(e.target.value)} | |
| placeholder="username@example.com" | |
| autoComplete="username" | |
| autoFocus | |
| /> | |
| </label> | |
| ) : ( | |
| <> | |
| <label className="auth-field"> | |
| <span>Username</span> | |
| <input | |
| type="text" | |
| value={username} | |
| onChange={(e) => setUsername(e.target.value)} | |
| placeholder="Your username" | |
| autoComplete="username" | |
| autoFocus | |
| /> | |
| </label> | |
| <label className="auth-field"> | |
| <span>Email</span> | |
| <input | |
| type="email" | |
| value={email} | |
| onChange={(e) => setEmail(e.target.value)} | |
| placeholder="you@example.com" | |
| autoComplete="email" | |
| /> | |
| </label> | |
| </> | |
| )} | |
| <label className="auth-field"> | |
| <span>Password {mode === "register" && <span style={{color:'#475569',fontWeight:400,textTransform:'none',letterSpacing:0}}>(min 6 characters)</span>}</span> | |
| <input | |
| type="password" | |
| value={password} | |
| onChange={(e) => { | |
| setPassword(e.target.value); | |
| if (mode === "register") setPwStrength(getPasswordStrength(e.target.value)); | |
| }} | |
| placeholder="••••••••" | |
| autoComplete={mode === "login" ? "current-password" : "new-password"} | |
| maxLength={18} | |
| /> | |
| {mode === "register" && ( | |
| <div className="auth-pw-bar"><div className="auth-pw-fill" style={{width: pwStrength.width, background: pwStrength.color}}></div></div> | |
| )} | |
| </label> | |
| {error ? <div className="auth-error show">{error}</div> : null} | |
| <button type="submit" className="btn btn-primary auth-submit" disabled={busy}> | |
| <span className="auth-submit-text">{busy ? "Please wait…" : mode === "login" ? "Sign in" : "Create account"}</span> | |
| <span className="auth-submit-spinner"></span> | |
| </button> | |
| </form> | |
| <div className="auth-divider"><span>OR</span></div> | |
| <button | |
| className="auth-anilist-btn" | |
| onClick={() => { window.location.href = "/api/auth/anilist/login"; }} | |
| title="Sign in with your AniList account" | |
| > | |
| <img src="https://anilist.co/img/icons/icon.svg" style={{width:20,height:20}} alt="AniList" /> | |
| <span>Continue with AniList</span> | |
| </button> | |
| {mode === "login" ? ( | |
| <p className="auth-switch">No account? <a onClick={() => { setMode("register"); setError(""); }}>Create one</a></p> | |
| ) : ( | |
| <p className="auth-switch">Already have an account? <a onClick={() => { setMode("login"); setError(""); }}>Sign in</a></p> | |
| )} | |
| <div className="auth-recaptcha-terms"> | |
| This site is protected by reCAPTCHA and the Google | |
| <a href="https://policies.google.com/privacy" target="_blank" rel="noopener noreferrer">Privacy Policy</a> and | |
| <a href="https://policies.google.com/terms" target="_blank" rel="noopener noreferrer">Terms of Service</a> apply. | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |