diff --git a/frontend/src/pages/LoginPage.jsx b/frontend/src/pages/LoginPage.jsx index 530cb4a..5e8c65b 100644 --- a/frontend/src/pages/LoginPage.jsx +++ b/frontend/src/pages/LoginPage.jsx @@ -3,64 +3,85 @@ import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; import * as authService from '../services/authService'; -const API_BASE = import.meta.env.VITE_API_BASE_URL; // e.g. http://localhost:8000/api +const API_BASE = import.meta.env.VITE_API_BASE_URL; // http://localhost:8000/api export default function LoginPage() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); + const [isEmailValid, setIsEmailValid] = useState(true); - const { login, isAuthenticated } = useAuth(); - const navigate = useNavigate(); - const [searchParams] = useSearchParams(); + const auth = useAuth(); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); - // Show GitHub OAuth errors forwarded from the callback + const isPasswordValid = password.length >= 6; + + const validateEmail = (value) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(value); + }; + + // Handle GitHub OAuth callback redirect: + // Backend sends: /login?authStatus=success#token=JWT useEffect(() => { const ghError = searchParams.get('githubAuthError') || searchParams.get('error'); if (ghError) { setError(decodeURIComponent(ghError)); + return; } - }, [searchParams]); - // If already authenticated, redirect away from login - useEffect(() => { - if (isAuthenticated) { - navigate('/dashboard', { replace: true }); + const authStatus = searchParams.get('authStatus'); + if (authStatus === 'success') { + // Token is in the URL hash: #token=JWT + const hash = window.location.hash.slice(1); + const hashParams = new URLSearchParams(hash); + const token = hashParams.get('token'); + + if (token) { + localStorage.setItem('token', token); + try { + const payload = JSON.parse(atob(token.split('.')[1])); + auth.login(token, { id: payload.userId, email: payload.email, role: payload.role }); + } catch { + auth.login(token, {}); + } + navigate('/dashboard', { replace: true }); + } } - }, [isAuthenticated, navigate]); + }, [searchParams]); const handleSubmit = async (e) => { e.preventDefault(); setError(''); + + if (!validateEmail(email)) { + setError('Please enter a valid email address'); + return; + } + + if (!isPasswordValid) { + setError('Password must be at least 6 characters'); + return; + } + setLoading(true); try { - // Server sets HttpOnly cookies on success. - // Response body: { success, message, data: { user } } const response = await authService.login(email, password); - const userData = response.data?.user; - if (!userData) { - throw new Error('Login succeeded but no user data returned.'); - } - login(userData); - navigate('/dashboard', { replace: true }); + const { token, user } = response.data; + auth.login(token, user); + navigate('/dashboard'); } catch (err) { - const msg = err.response?.data?.message || 'Login failed. Please check your credentials.'; - setError(msg); + setError(err.response?.data?.message || 'Login failed'); } finally { setLoading(false); } }; - /** - * GitHub login: - * Redirects the browser to the backend, which builds a state JWT and - * redirects to GitHub. GitHub redirects to /api/auth/github/callback, - * which sets cookies and redirects to /auth/github/callback (frontend). - */ + /** Redirect browser to backend — backend handles the entire OAuth dance */ const handleGitHubLogin = () => { - // Pass redirectPath so after GitHub auth the user lands on /dashboard - window.location.href = `${API_BASE}/auth/github/start?redirectPath=${encodeURIComponent('/dashboard')}`; + window.location.href = `${API_BASE}/auth/github/start`; }; return ( @@ -71,19 +92,20 @@ export default function LoginPage() { {error && ( -
-

+

+

{error}

)} - {/* ── GitHub button — top, most prominent ── */} + {/* ── GitHub button — top of form, most prominent ── */}
-
+
@@ -120,10 +151,16 @@ export default function LoginPage() { type="password" value={password} onChange={(e) => setPassword(e.target.value)} + aria-invalid={password.length > 0 && !isPasswordValid} className="w-full p-5 border-4 border-black rounded-none text-black font-bold focus:outline-none focus:border-gray-500" placeholder="••••••••" required /> + {password && !isPasswordValid && ( +

+ Password must be at least 6 characters +

+ )}