Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 33 additions & 30 deletions apps/web/app/(dashboard)/layout.tsx
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();

Copy link
Copy Markdown

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:

#!/bin/bash
set -euo pipefail

# Inspect the authentication call and the declared runtime limits.
ast-grep outline apps/web/lib/server-auth.ts --items all
rg -n -C 4 'getServerSession|fetch\(|AbortSignal\.timeout|signal:|maxDuration' apps/web
fd -HI 'package.json|.nvmrc|.node-version|Dockerfile|next.config.*|vercel.json' . -x sed -n '1,180p' {}

Repository: hallelx2/vectorless

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate files =="
git ls-files | rg '(^|/)apps/web/lib/server-auth\.(ts|tsx|js|jsx)$|^apps/web/next\.config\.(ts|js|mjs|cjs)$|(^|/)vercel\.json$|package\.json$' || true

echo
echo "== server-auth outline =="
ast-grep outline apps/web/lib/server-auth.ts --items all || true

echo
echo "== server-auth source =="
cat -n apps/web/lib/server-auth.ts

echo
echo "== dashboard layout relevant =="
sed -n '1,140p' apps/web/app/'(dashboard)'/layout.tsx | cat -n

echo
echo "== focused auth/control-plane references =="
rg -n -C 3 'getServerSession|setServerSession|fetch\(|CLOVA|control|signal:|AbortSignal\.timeout|maxDuration' apps/web/lib apps/web/next.config.* 2>/dev/null || true

echo
echo "== focused config references =="
jq '{scripts, dependencies, devDependencies, next, vercel}' apps/web/package.json 2>/dev/null || sed -n '1,220p' apps/web/package.json
for f in apps/web/next.config.* vercel.json; do
  [ -f "$f" ] && { echo "--- $f"; sed -n '1,220p' "$f"; }
done

Repository: hallelx2/vectorless

Length of output: 33731


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Vercel runtime config =="
cat -n apps/web/vercel.json

echo
echo "== top package runtime context =="
for f in package.json apps/web/package.json; do
  echo "--- $f"
  jq '{engines, scripts, dependencies, devDependencies}' "$f" 2>/dev/null || sed -n '1,220p' "$f"
done

Repository: hallelx2/vectorless

Length of output: 3140


Bound the control-plane session request.

getServerSession() calls fetch() for /admin/v1/auth/me without a cancellation signal, and the /dashboard layout awaits it before rendering. Add a bounded timeout to this auth request; return null when it times out so unauthenticated/failed requests redirect instead of exhausting the function’s runtime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/`(dashboard)/layout.tsx at line 38, Update the dashboard
layout’s getServerSession call to use a bounded timeout for the underlying
/admin/v1/auth/me request, passing a cancellation signal through the existing
session-fetch configuration. Convert timeout failures to a null session so the
existing unauthenticated redirect path runs, while preserving successful session
handling and other error behavior.

if (!session) redirect("/login");
Comment on lines +38 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.

getServerSession() returns null for fetch failures and every non-401/403 response from the control plane. This branch redirects those failures to /login.

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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/`(dashboard)/layout.tsx around lines 38 - 39, Update the session
handling around getServerSession so control-plane timeouts and 5xx failures
produce a distinct unavailable result that renders or throws a 5xx response
instead of redirecting. Restrict redirect("/login") to genuinely absent,
invalid, or expired sessions, preserving normal rendering for valid sessions.


return <DashboardShell>{children}</DashboardShell>;
}
27 changes: 19 additions & 8 deletions apps/web/components/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Treat blank documentation URLs as unset.

?? does not replace an empty string. If NEXT_PUBLIC_DOCS_URL is empty or whitespace-only, both Docs links receive an invalid URL instead of the documented fallback.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* 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';
/**
* 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?.trim() || 'https://docs.vectorless.store';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/components/Nav.tsx` around lines 8 - 18, Update the DOCS_URL
initialization to trim NEXT_PUBLIC_DOCS_URL and use the documented default URL
when the value is empty or whitespace-only, while preserving nonblank override
values for both Docs links.

export default function Nav() {
const [isOpen, setIsOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
Expand Down Expand Up @@ -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">
Expand All @@ -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">
Expand Down
Loading