-
Notifications
You must be signed in to change notification settings - Fork 0
fix(web): gate the dashboard on the server, repair two nav links #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,39 +1,42 @@ | ||
| "use client"; | ||
|
|
||
| import { useEffect } from "react"; | ||
| import { useRouter } from "next/navigation"; | ||
| import { useSession } from "@/lib/auth-client"; | ||
| import { redirect } from "next/navigation"; | ||
| import { getServerSession } from "@/lib/server-auth"; | ||
| import DashboardShell from "@/components/dashboard/DashboardShell"; | ||
| import { Loader2 } from "lucide-react"; | ||
|
|
||
| export default function DashboardLayout({ | ||
| /** | ||
| * Server-side gate for every /dashboard route. | ||
| * | ||
| * This was previously a client component that called `useSession()` and | ||
| * redirected from a `useEffect`. That is not protection. A client-side | ||
| * check runs only after the server has already sent the HTML and the | ||
| * browser has downloaded, parsed and hydrated the whole dashboard bundle | ||
| * — so an unauthenticated visitor received the entire screen, saw a | ||
| * spinner, and was then bounced. The route's existence, its layout, its | ||
| * navigation and its feature set all leaked, and a user whose session had | ||
| * expired got a flash of the UI before the redirect. | ||
| * | ||
| * Resolving the session here means an unauthenticated request never gets | ||
| * a dashboard response at all — it gets a redirect, before any of this | ||
| * subtree renders. | ||
| * | ||
| * Customer data was never exposed by the old version: every /api/dashboard | ||
| * route proxies through `forwardToCP`, which returns 401 when the | ||
| * `vls_session` cookie is absent and otherwise hands the cookie to the | ||
| * control plane to validate. The leak was the shell, not the contents. | ||
| * That is why this is a real defect and not an incident. | ||
| * | ||
| * This is a coarse gate. Per-page checks are still the standard — a | ||
| * route's protection should be readable in the route's own file — and | ||
| * every page under here is currently a client component, so they cannot | ||
| * do it yet. Converting them is tracked separately; this closes the hole | ||
| * in the meantime. | ||
| */ | ||
| export default async function DashboardLayout({ | ||
| children, | ||
| }: { | ||
| children: React.ReactNode; | ||
| }) { | ||
| const { data: session, isPending } = useSession(); | ||
| const router = useRouter(); | ||
|
|
||
| useEffect(() => { | ||
| if (!isPending && !session) { | ||
| router.push("/login"); | ||
| } | ||
| }, [isPending, session, router]); | ||
|
|
||
| if (isPending) { | ||
| return ( | ||
| <div className="flex h-screen items-center justify-center bg-[hsl(var(--muted))]"> | ||
| <div className="flex flex-col items-center gap-3"> | ||
| <Loader2 className="h-8 w-8 animate-spin text-primary" /> | ||
| <p className="text-sm text-muted-foreground">Loading...</p> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (!session) { | ||
| return null; | ||
| } | ||
| const session = await getServerSession(); | ||
| if (!session) redirect("/login"); | ||
|
Comment on lines
+38
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift Do not redirect valid users when the control plane is unavailable.
Return a distinct unavailable result for timeouts and 5xx responses. Render or throw a 5xx response for that result. Keep the redirect for only absent, invalid, or expired sessions. 🤖 Prompt for AI Agents |
||
|
|
||
| return <DashboardShell>{children}</DashboardShell>; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,6 +5,17 @@ import Link from 'next/link'; | |||||||||||||||||||||||||||||||||||||||||||
| import { Menu, X } from 'lucide-react'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { VectorlessDot } from './VectorlessIcon'; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||
| * Docs live on their own Fumadocs deployment, not in this app. The link | ||||||||||||||||||||||||||||||||||||||||||||
| * used to point at /dashboard, which sent anyone looking for | ||||||||||||||||||||||||||||||||||||||||||||
| * documentation into the product — and, before the layout was gated on | ||||||||||||||||||||||||||||||||||||||||||||
| * the server, into a login redirect. | ||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||
| * Overridable so a preview deployment can point at a staging docs site | ||||||||||||||||||||||||||||||||||||||||||||
| * without a code change. | ||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||
| const DOCS_URL = process.env.NEXT_PUBLIC_DOCS_URL ?? 'https://docs.vectorless.store'; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+8
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Treat blank documentation URLs as unset.
Proposed fix-const DOCS_URL = process.env.NEXT_PUBLIC_DOCS_URL ?? 'https://docs.vectorless.store';
+const DOCS_URL =
+ process.env.NEXT_PUBLIC_DOCS_URL?.trim() || 'https://docs.vectorless.store';📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
| export default function Nav() { | ||||||||||||||||||||||||||||||||||||||||||||
| const [isOpen, setIsOpen] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||
| const [scrolled, setScrolled] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -40,11 +51,11 @@ export default function Nav() { | |||||||||||||||||||||||||||||||||||||||||||
| </Link> | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| <div className="hidden md:flex items-center gap-1"> | ||||||||||||||||||||||||||||||||||||||||||||
| <Link href="#how" className="text-[14px] font-medium text-text-dark px-3.5 py-2 rounded-full hover:bg-black/5 transition-colors">How it works</Link> | ||||||||||||||||||||||||||||||||||||||||||||
| <Link href="/dashboard" className="text-[14px] font-medium text-text-dark px-3.5 py-2 rounded-full hover:bg-black/5 transition-colors">Docs</Link> | ||||||||||||||||||||||||||||||||||||||||||||
| <Link href="/#how" className="text-[14px] font-medium text-text-dark px-3.5 py-2 rounded-full hover:bg-black/5 transition-colors">How it works</Link> | ||||||||||||||||||||||||||||||||||||||||||||
| <a href={DOCS_URL} className="text-[14px] font-medium text-text-dark px-3.5 py-2 rounded-full hover:bg-black/5 transition-colors">Docs</a> | ||||||||||||||||||||||||||||||||||||||||||||
| <Link href="/whitepaper" className="text-[14px] font-medium text-text-dark px-3.5 py-2 rounded-full hover:bg-black/5 transition-colors">Whitepaper</Link> | ||||||||||||||||||||||||||||||||||||||||||||
| <Link href="#pricing" className="text-[14px] font-medium text-text-dark px-3.5 py-2 rounded-full hover:bg-black/5 transition-colors">Pricing</Link> | ||||||||||||||||||||||||||||||||||||||||||||
| <Link href="#faq" className="text-[14px] font-medium text-text-dark px-3.5 py-2 rounded-full hover:bg-black/5 transition-colors">FAQ</Link> | ||||||||||||||||||||||||||||||||||||||||||||
| <Link href="/#pricing" className="text-[14px] font-medium text-text-dark px-3.5 py-2 rounded-full hover:bg-black/5 transition-colors">Pricing</Link> | ||||||||||||||||||||||||||||||||||||||||||||
| <Link href="/#faq" className="text-[14px] font-medium text-text-dark px-3.5 py-2 rounded-full hover:bg-black/5 transition-colors">FAQ</Link> | ||||||||||||||||||||||||||||||||||||||||||||
| <div className="w-[1px] h-4 bg-black/10 mx-2" /> | ||||||||||||||||||||||||||||||||||||||||||||
| <Link href="/login" className="text-[14px] font-medium text-text-dark px-3 py-2 hover:text-primary-500 transition-colors">Login</Link> | ||||||||||||||||||||||||||||||||||||||||||||
| <Link href="/register" className="bg-bg-dark text-white px-5 py-2.5 rounded-full text-[14px] font-medium hover:bg-black transition-colors ml-1"> | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -64,11 +75,11 @@ export default function Nav() { | |||||||||||||||||||||||||||||||||||||||||||
| {/* Mobile menu — floating glass sheet below the pill */} | ||||||||||||||||||||||||||||||||||||||||||||
| {isOpen && ( | ||||||||||||||||||||||||||||||||||||||||||||
| <div className="md:hidden mx-auto mt-2 max-w-[1100px] rounded-2xl border border-white/50 bg-white/85 backdrop-blur-xl shadow-[0_8px_30px_rgba(0,0,0,0.12)] p-5 flex flex-col gap-3"> | ||||||||||||||||||||||||||||||||||||||||||||
| <Link href="#how" onClick={() => setIsOpen(false)} className="text-[15px] font-medium text-text-dark p-2 rounded-lg hover:bg-black/5">How it works</Link> | ||||||||||||||||||||||||||||||||||||||||||||
| <Link href="/dashboard" onClick={() => setIsOpen(false)} className="text-[15px] font-medium text-text-dark p-2 rounded-lg hover:bg-black/5">Docs</Link> | ||||||||||||||||||||||||||||||||||||||||||||
| <Link href="/#how" onClick={() => setIsOpen(false)} className="text-[15px] font-medium text-text-dark p-2 rounded-lg hover:bg-black/5">How it works</Link> | ||||||||||||||||||||||||||||||||||||||||||||
| <a href={DOCS_URL} onClick={() => setIsOpen(false)} className="text-[15px] font-medium text-text-dark p-2 rounded-lg hover:bg-black/5">Docs</a> | ||||||||||||||||||||||||||||||||||||||||||||
| <Link href="/whitepaper" onClick={() => setIsOpen(false)} className="text-[15px] font-medium text-text-dark p-2 rounded-lg hover:bg-black/5">Whitepaper</Link> | ||||||||||||||||||||||||||||||||||||||||||||
| <Link href="#pricing" onClick={() => setIsOpen(false)} className="text-[15px] font-medium text-text-dark p-2 rounded-lg hover:bg-black/5">Pricing</Link> | ||||||||||||||||||||||||||||||||||||||||||||
| <Link href="#faq" onClick={() => setIsOpen(false)} className="text-[15px] font-medium text-text-dark p-2 rounded-lg hover:bg-black/5">FAQ</Link> | ||||||||||||||||||||||||||||||||||||||||||||
| <Link href="/#pricing" onClick={() => setIsOpen(false)} className="text-[15px] font-medium text-text-dark p-2 rounded-lg hover:bg-black/5">Pricing</Link> | ||||||||||||||||||||||||||||||||||||||||||||
| <Link href="/#faq" onClick={() => setIsOpen(false)} className="text-[15px] font-medium text-text-dark p-2 rounded-lg hover:bg-black/5">FAQ</Link> | ||||||||||||||||||||||||||||||||||||||||||||
| <div className="h-[1px] w-full bg-black/10 my-1" /> | ||||||||||||||||||||||||||||||||||||||||||||
| <Link href="/login" onClick={() => setIsOpen(false)} className="text-[15px] font-medium text-text-dark p-2 rounded-lg hover:bg-black/5">Login</Link> | ||||||||||||||||||||||||||||||||||||||||||||
| <Link href="/register" onClick={() => setIsOpen(false)} className="bg-bg-dark text-white px-4 py-3 rounded-full text-[14px] font-medium hover:bg-black transition-colors flex items-center justify-center mt-1"> | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: hallelx2/vectorless
Length of output: 50376
🏁 Script executed:
Repository: hallelx2/vectorless
Length of output: 33731
🏁 Script executed:
Repository: hallelx2/vectorless
Length of output: 3140
Bound the control-plane session request.
getServerSession()callsfetch()for/admin/v1/auth/mewithout a cancellation signal, and the/dashboardlayoutawaits it before rendering. Add a bounded timeout to this auth request; returnnullwhen it times out so unauthenticated/failed requests redirect instead of exhausting the function’s runtime.🤖 Prompt for AI Agents