Spaces:
Sleeping
Sleeping
| import { useState, useEffect, useCallback, useRef } from "react" | |
| import "./App.css" | |
| const API = window.location.hostname === "localhost" ? "http://localhost:8000" : "" | |
| function App() { | |
| const [page, setPage] = useState("login") | |
| const [token, setToken] = useState(localStorage.getItem("access_token")) | |
| const [user, setUser] = useState(null) | |
| const [form, setForm] = useState({ username: "", email: "", password: "" }) | |
| const [otp, setOtp] = useState("") | |
| const [msg, setMsg] = useState("") | |
| const hasCheckedAuth = useRef(false) | |
| const refreshAccessToken = useCallback(async () => { | |
| const refreshToken = localStorage.getItem("refresh_token") | |
| if (!refreshToken) { logout(); return null } | |
| try { | |
| const res = await fetch(`${API}/auth/refresh`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ refresh_token: refreshToken }) | |
| }) | |
| if (res.ok) { | |
| const data = await res.json() | |
| localStorage.setItem("access_token", data.access_token) | |
| localStorage.setItem("refresh_token", data.refresh_token) | |
| setToken(data.access_token) | |
| return data.access_token | |
| } else { logout(); return null } | |
| } catch { logout(); return null } | |
| }, []) | |
| const apiCall = useCallback(async (url, options = {}) => { | |
| let currentToken = localStorage.getItem("access_token") | |
| let res = await fetch(url, { | |
| ...options, | |
| headers: { "Content-Type": "application/json", ...options.headers, Authorization: `Bearer ${currentToken}` } | |
| }) | |
| if (res.status === 401) { | |
| const newToken = await refreshAccessToken() | |
| if (newToken) { | |
| res = await fetch(url, { | |
| ...options, | |
| headers: { "Content-Type": "application/json", ...options.headers, Authorization: `Bearer ${newToken}` } | |
| }) | |
| } | |
| } | |
| return res | |
| }, [refreshAccessToken]) | |
| function navigateTo(newPage) { | |
| setPage(newPage) | |
| setMsg("") | |
| setOtp("") | |
| } | |
| function resetAll() { | |
| setPage("login") | |
| setMsg("") | |
| setOtp("") | |
| setForm({ username: "", email: "", password: "" }) | |
| } | |
| useEffect(() => { | |
| if (!hasCheckedAuth.current) { hasCheckedAuth.current = true; if (token) getMe() } | |
| }, []) | |
| async function getMe() { | |
| const res = await apiCall(`${API}/me`) | |
| if (res.ok) { setUser(await res.json()); navigateTo("dashboard") } | |
| else logout() | |
| } | |
| async function register() { | |
| try { | |
| if (!/^[a-zA-Z0-9_]{3,20}$/.test(form.username)) { setMsg("Username: 3-20 chars, letters/numbers/_ only"); return } | |
| if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) { setMsg("Invalid email format"); return } | |
| if (form.password.length < 8) { setMsg("Password must be at least 8 characters"); return } | |
| const res = await fetch(`${API}/auth/register`, { | |
| method: "POST", headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ username: form.username, email: form.email, password: form.password }) | |
| }) | |
| const data = await res.json() | |
| if (res.ok) { | |
| setPage("verify-register") | |
| setOtp("") | |
| setMsg("OTP sent to your email. Enter it to verify.") | |
| } else { | |
| setMsg(data.detail || "Registration failed") | |
| } | |
| } catch (err) { | |
| setMsg("Network error. Is the backend running?") | |
| } | |
| } | |
| async function verifyRegister() { | |
| try { | |
| if (otp.length !== 6) { setMsg("Enter the 6-digit OTP"); return } | |
| const res = await fetch(`${API}/auth/verify-email`, { | |
| method: "POST", headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ email: form.email, code: otp }) | |
| }) | |
| const data = await res.json() | |
| if (res.ok) { | |
| setMsg("Account verified! Now login") | |
| navigateTo("login") | |
| } else { | |
| setMsg(data.detail || "Verification failed") | |
| } | |
| } catch (err) { | |
| setMsg("Network error. Is the backend running?") | |
| } | |
| } | |
| async function login() { | |
| try { | |
| const body = new URLSearchParams() | |
| body.append("username", form.username) | |
| body.append("password", form.password) | |
| const res = await fetch(`${API}/auth/login`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/x-www-form-urlencoded" }, | |
| body: body.toString() | |
| }) | |
| const data = await res.json() | |
| if (res.ok) { | |
| localStorage.setItem("access_token", data.access_token) | |
| localStorage.setItem("refresh_token", data.refresh_token) | |
| setToken(data.access_token) | |
| const meRes = await apiCall(`${API}/me`) | |
| if (meRes.ok) setUser(await meRes.json()) | |
| navigateTo("dashboard") | |
| } else { | |
| setMsg(data.detail || "Login failed") | |
| } | |
| } catch (err) { | |
| setMsg("Network error. Is the backend running?") | |
| } | |
| } | |
| function logout() { | |
| localStorage.removeItem("access_token") | |
| localStorage.removeItem("refresh_token") | |
| setToken(null); setUser(null); resetAll() | |
| } | |
| async function forgotPassword() { | |
| try { | |
| if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) { setMsg("Enter a valid email"); return } | |
| const res = await fetch(`${API}/auth/forgot-password`, { | |
| method: "POST", headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ email: form.email }) | |
| }) | |
| const data = await res.json() | |
| if (res.ok) { | |
| setPage("verify-forgot") | |
| setOtp("") | |
| setMsg("OTP sent to your email") | |
| } else { | |
| setMsg(data.detail || "Failed to send OTP") | |
| } | |
| } catch (err) { | |
| setMsg("Network error. Is the backend running?") | |
| } | |
| } | |
| async function resetPassword() { | |
| try { | |
| if (otp.length !== 6) { setMsg("Enter the 6-digit OTP"); return } | |
| if (form.password.length < 8) { setMsg("Password must be at least 8 characters"); return } | |
| const res = await fetch(`${API}/auth/reset-password`, { | |
| method: "POST", headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ email: form.email, code: otp, new_password: form.password }) | |
| }) | |
| const data = await res.json() | |
| if (res.ok) { | |
| setMsg("Password reset successful!") | |
| navigateTo("login") | |
| } else { | |
| setMsg(data.detail || "Reset failed") | |
| } | |
| } catch (err) { | |
| setMsg("Network error. Is the backend running?") | |
| } | |
| } | |
| return ( | |
| <div className="container"> | |
| {page === "login" && ( | |
| <div className="card"> | |
| <h2>Login</h2> | |
| {msg && <p className="msg">{msg}</p>} | |
| <input placeholder="Username or Email" value={form.username} onChange={e => setForm({ ...form, username: e.target.value })} /> | |
| <input type="password" placeholder="Password" value={form.password} onChange={e => setForm({ ...form, password: e.target.value })} /> | |
| <button onClick={login}>Login</button> | |
| <p className="link" onClick={() => navigateTo("register")}>No account? Register</p> | |
| <p className="link" onClick={() => navigateTo("forgot-password")}>Forgot password?</p> | |
| </div> | |
| )} | |
| {page === "register" && ( | |
| <div className="card"> | |
| <h2>Register</h2> | |
| {msg && <p className="msg">{msg}</p>} | |
| <input placeholder="Username" value={form.username} onChange={e => setForm({ ...form, username: e.target.value })} /> | |
| <input type="email" placeholder="Email" value={form.email} onChange={e => setForm({ ...form, email: e.target.value })} /> | |
| <input type="password" placeholder="Password (min 8 chars)" value={form.password} onChange={e => setForm({ ...form, password: e.target.value })} /> | |
| <button onClick={register}>Register</button> | |
| <p className="link" onClick={() => navigateTo("login")}>Have account? Login</p> | |
| </div> | |
| )} | |
| {page === "verify-register" && ( | |
| <div className="card"> | |
| <h2>Verify Account</h2> | |
| {msg && <p className="msg">{msg}</p>} | |
| <p className="info">OTP sent to: {form.email || "your email"}</p> | |
| <input placeholder="Enter 6-digit OTP" value={otp} onChange={e => setOtp(e.target.value)} /> | |
| <button onClick={verifyRegister}>Verify</button> | |
| </div> | |
| )} | |
| {page === "forgot-password" && ( | |
| <div className="card"> | |
| <h2>Forgot Password</h2> | |
| {msg && <p className="msg">{msg}</p>} | |
| <input type="email" placeholder="Email" value={form.email} onChange={e => setForm({ ...form, email: e.target.value })} /> | |
| <button onClick={forgotPassword}>Send OTP</button> | |
| <p className="link" onClick={() => navigateTo("login")}>Back to Login</p> | |
| </div> | |
| )} | |
| {page === "verify-forgot" && ( | |
| <div className="card"> | |
| <h2>Reset Password</h2> | |
| {msg && <p className="msg">{msg}</p>} | |
| <p className="info">OTP sent to: {form.email || "your email"}</p> | |
| <input placeholder="Enter 6-digit OTP" value={otp} onChange={e => setOtp(e.target.value)} /> | |
| <input type="password" placeholder="New Password (min 8 chars)" value={form.password} onChange={e => setForm({ ...form, password: e.target.value })} /> | |
| <button onClick={resetPassword}>Reset Password</button> | |
| <p className="link" onClick={() => navigateTo("forgot-password")}>Back</p> | |
| </div> | |
| )} | |
| {page === "dashboard" && ( | |
| <div className="card"> | |
| <h2>Dashboard</h2> | |
| <p>ID: {user?.id}</p> | |
| <p>Username: {user?.username}</p> | |
| <p>Email: {user?.email}</p> | |
| <button onClick={logout}>Logout</button> | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| } | |
| export default App | |