aniqove / frontend /src /components /AuthModal.jsx
mwask's picture
Upload 59 files
d571830 verified
Raw
History Blame Contribute Delete
7.77 kB
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>
);
}