fix #12: remove all auth.api.getSession() calls

- middleware.ts: cookie-presence check only (Edge Runtime can't use DB),
  skip auth for API routes entirely
- compare/route.ts: manual session token parsing + db.select() queries
- user/comparisons/route.ts: same manual auth bypass
- user/stats/route.ts: same manual auth bypass

Root cause: Drizzle 0.45.2 queryWithCache bug triggers when
auth.api.getSession() is called from non-route-handler contexts.
Bypass entirely with direct db.select() on sessions/users tables.
This commit is contained in:
Christopher Mayor
2026-04-27 12:31:36 -07:00
parent fe5153c4e5
commit 371755c241
3 changed files with 77 additions and 20 deletions

View File

@@ -1,22 +1,46 @@
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { comparisons, comparisonItems } from "@/lib/db/schema"; import { comparisons, comparisonItems, sessions, users } from "@/lib/db/schema";
import { eq, desc, sql, inArray } from "drizzle-orm"; import { eq, desc, sql, inArray, and, gt } from "drizzle-orm";
import { auth } from "@/lib/auth";
import { headers } from "next/headers"; import { headers } from "next/headers";
export async function GET(request: Request) { export async function GET(request: Request) {
const session = await auth.api.getSession({ headers: await headers() }); // Bypass auth.api.getSession() — Drizzle queryWithCache bug (#12)
const hdrs = await headers();
if (!session?.user) { const cookieHeader = hdrs.get("cookie") ?? "";
const tokenMatch = cookieHeader
.split(";")
.find((c) => c.trim().startsWith("better-auth.session_token="));
const token = tokenMatch?.split("=")?.[1]?.trim();
if (!token) {
return Response.json({ error: "Unauthorized" }, { status: 401 }); return Response.json({ error: "Unauthorized" }, { status: 401 });
} }
const sessionRows = await db
.select()
.from(sessions)
.where(and(eq(sessions.token, token), gt(sessions.expiresAt, new Date())))
.limit(1);
if (!sessionRows.length) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const userRows = await db
.select()
.from(users)
.where(eq(users.id, sessionRows[0].userId))
.limit(1);
if (!userRows.length) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const userId = userRows[0].id;
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const page = Math.max(1, Number(searchParams.get("page")) || 1); const page = Math.max(1, Number(searchParams.get("page")) || 1);
const limit = Math.min(100, Math.max(1, Number(searchParams.get("limit")) || 20)); const limit = Math.min(100, Math.max(1, Number(searchParams.get("limit")) || 20));
const offset = (page - 1) * limit; const offset = (page - 1) * limit;
const where = eq(comparisons.userId, session.user.id); const where = eq(comparisons.userId, userId);
const [result, countResult] = await Promise.all([ const [result, countResult] = await Promise.all([
db db

View File

@@ -1,23 +1,47 @@
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { comparisons } from "@/lib/db/schema"; import { comparisons, sessions, users } from "@/lib/db/schema";
import { eq, sql } from "drizzle-orm"; import { eq, sql, and, gt } from "drizzle-orm";
import { auth } from "@/lib/auth";
import { headers } from "next/headers"; import { headers } from "next/headers";
export async function GET() { export async function GET() {
const session = await auth.api.getSession({ headers: await headers() }); // Bypass auth.api.getSession() — Drizzle queryWithCache bug (#12)
const hdrs = await headers();
if (!session?.user) { const cookieHeader = hdrs.get("cookie") ?? "";
const tokenMatch = cookieHeader
.split(";")
.find((c) => c.trim().startsWith("better-auth.session_token="));
const token = tokenMatch?.split("=")?.[1]?.trim();
if (!token) {
return Response.json({ error: "Unauthorized" }, { status: 401 }); return Response.json({ error: "Unauthorized" }, { status: 401 });
} }
const sessionRows = await db
.select()
.from(sessions)
.where(and(eq(sessions.token, token), gt(sessions.expiresAt, new Date())))
.limit(1);
if (!sessionRows.length) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const userRows = await db
.select()
.from(users)
.where(eq(users.id, sessionRows[0].userId))
.limit(1);
if (!userRows.length) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const userId = userRows[0].id;
const result = await db const result = await db
.select({ .select({
totalComparisons: sql<number>`count(*)`, totalComparisons: sql<number>`count(*)`,
totalViews: sql<number>`coalesce(sum(${comparisons.viewCount}), 0)`, totalViews: sql<number>`coalesce(sum(${comparisons.viewCount}), 0)`,
}) })
.from(comparisons) .from(comparisons)
.where(eq(comparisons.userId, session.user.id)); .where(eq(comparisons.userId, userId));
return Response.json(result[0]); return Response.json(result[0]);
} }

View File

@@ -1,9 +1,15 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
const publicPaths = ["/", "/explore", "/sign-in", "/sign-up", "/api/auth"]; const publicPaths = ["/", "/explore", "/sign-in", "/sign-up", "/api/auth"];
const protectedPaths = ["/compare", "/profile"]; const protectedPaths = ["/compare", "/profile"];
function hasSessionCookie(headers: Headers): boolean {
const cookieHeader = headers.get("cookie") ?? "";
return cookieHeader
.split(";")
.some((c) => c.trim().startsWith("better-auth.session_token="));
}
export async function middleware(request: NextRequest) { export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl; const { pathname } = request.nextUrl;
@@ -15,6 +21,11 @@ export async function middleware(request: NextRequest) {
return NextResponse.next(); return NextResponse.next();
} }
// API routes handle their own auth — skip middleware session check
if (pathname.startsWith("/api/")) {
return NextResponse.next();
}
const isPublic = publicPaths.some( const isPublic = publicPaths.some(
(path) => pathname === path || pathname.startsWith(path + "/"), (path) => pathname === path || pathname.startsWith(path + "/"),
); );
@@ -27,11 +38,9 @@ export async function middleware(request: NextRequest) {
return NextResponse.next(); return NextResponse.next();
} }
const session = await auth.api.getSession({ // Cookie-presence check only — real auth happens in route handlers.
headers: request.headers, // auth.api.getSession() bypassed due to Drizzle queryWithCache bug (#12).
}); if (!hasSessionCookie(request.headers) && isProtected) {
if (!session && isProtected) {
const signInUrl = new URL("/sign-in", request.url); const signInUrl = new URL("/sign-in", request.url);
signInUrl.searchParams.set("callbackUrl", pathname); signInUrl.searchParams.set("callbackUrl", pathname);
return NextResponse.redirect(signInUrl); return NextResponse.redirect(signInUrl);