"use client"; import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import { useTheme } from "next-themes"; import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { FlickeringGrid } from "@/components/ui/flickering-grid"; import { Input } from "@/components/ui/input"; import { useAuth } from "@/core/auth/AuthProvider"; import { parseAuthError } from "@/core/auth/types"; /** * Validate next parameter * Prevent open redirect attacks * Per RFC-001: Only allow relative paths starting with / */ function validateNextParam(next: string | null): string | null { if (!next) { return null; } // Need start with / (relative path) if (!next.startsWith("/")) { return null; } // Disallow protocol-relative URLs if ( next.startsWith("//") || next.startsWith("http://") || next.startsWith("https://") ) { return null; } // Disallow URLs with different protocols (e.g., javascript:, data:, etc) if (next.includes(":") && !next.startsWith("/")) { return null; } // Valid relative path return next; } export default function LoginPage() { const router = useRouter(); const searchParams = useSearchParams(); const { isAuthenticated } = useAuth(); const { theme, resolvedTheme } = useTheme(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [isLogin, setIsLogin] = useState(true); const [error, setError] = useState(""); const [loading, setLoading] = useState(false); // Get next parameter for validated redirect const nextParam = searchParams.get("next"); const redirectPath = validateNextParam(nextParam) ?? "/workspace"; // Redirect if already authenticated (client-side, post-login) useEffect(() => { if (isAuthenticated) { router.push(redirectPath); } }, [isAuthenticated, redirectPath, router]); // Redirect to setup if the system has no users yet useEffect(() => { let cancelled = false; void fetch("/api/v1/auth/setup-status") .then((r) => r.json()) .then((data: { needs_setup?: boolean }) => { if (!cancelled && data.needs_setup) { router.push("/setup"); } }) .catch(() => { // Ignore errors; user stays on login page }); return () => { cancelled = true; }; }, [router]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(""); setLoading(true); try { const endpoint = isLogin ? "/api/v1/auth/login/local" : "/api/v1/auth/register"; const body = isLogin ? `username=${encodeURIComponent(email)}&password=${encodeURIComponent(password)}` : JSON.stringify({ email, password }); const headers: HeadersInit = isLogin ? { "Content-Type": "application/x-www-form-urlencoded" } : { "Content-Type": "application/json" }; const res = await fetch(endpoint, { method: "POST", headers, body, credentials: "include", // Important: include HttpOnly cookie }); if (!res.ok) { const data = await res.json(); const authError = parseAuthError(data); setError(authError.message); return; } // Both login and register set a cookie — redirect to workspace router.push(redirectPath); } catch { setError("Network error. Please try again."); } finally { setLoading(false); } }; const actualTheme = theme === "system" ? resolvedTheme : theme; return (

DeerFlow

{isLogin ? "Sign in to your account" : "Create a new account"}

setEmail(e.target.value)} placeholder="you@example.com" required />
setPassword(e.target.value)} placeholder="•••••••" required minLength={isLogin ? 6 : 8} />
{error &&

{error}

}
← Back to home
); }