diff --git a/src/app/api/user/comparisons/route.ts b/src/app/api/user/comparisons/route.ts index d46356f..cb7b2d6 100644 --- a/src/app/api/user/comparisons/route.ts +++ b/src/app/api/user/comparisons/route.ts @@ -1,22 +1,46 @@ import { db } from "@/lib/db"; -import { comparisons, comparisonItems } from "@/lib/db/schema"; -import { eq, desc, sql, inArray } from "drizzle-orm"; -import { auth } from "@/lib/auth"; +import { comparisons, comparisonItems, sessions, users } from "@/lib/db/schema"; +import { eq, desc, sql, inArray, and, gt } from "drizzle-orm"; import { headers } from "next/headers"; export async function GET(request: Request) { - const session = await auth.api.getSession({ headers: await headers() }); - - if (!session?.user) { + // Bypass auth.api.getSession() — Drizzle queryWithCache bug (#12) + const hdrs = await headers(); + 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 }); } + 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 page = Math.max(1, Number(searchParams.get("page")) || 1); const limit = Math.min(100, Math.max(1, Number(searchParams.get("limit")) || 20)); 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([ db diff --git a/src/app/api/user/stats/route.ts b/src/app/api/user/stats/route.ts index b152dec..f5601e9 100644 --- a/src/app/api/user/stats/route.ts +++ b/src/app/api/user/stats/route.ts @@ -1,23 +1,47 @@ import { db } from "@/lib/db"; -import { comparisons } from "@/lib/db/schema"; -import { eq, sql } from "drizzle-orm"; -import { auth } from "@/lib/auth"; +import { comparisons, sessions, users } from "@/lib/db/schema"; +import { eq, sql, and, gt } from "drizzle-orm"; import { headers } from "next/headers"; export async function GET() { - const session = await auth.api.getSession({ headers: await headers() }); - - if (!session?.user) { + // Bypass auth.api.getSession() — Drizzle queryWithCache bug (#12) + const hdrs = await headers(); + 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 }); } + 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 .select({ totalComparisons: sql`count(*)`, totalViews: sql`coalesce(sum(${comparisons.viewCount}), 0)`, }) .from(comparisons) - .where(eq(comparisons.userId, session.user.id)); + .where(eq(comparisons.userId, userId)); return Response.json(result[0]); } diff --git a/src/middleware.ts b/src/middleware.ts index c88a27c..2bce440 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,9 +1,15 @@ import { NextRequest, NextResponse } from "next/server"; -import { auth } from "@/lib/auth"; const publicPaths = ["/", "/explore", "/sign-in", "/sign-up", "/api/auth"]; 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) { const { pathname } = request.nextUrl; @@ -15,6 +21,11 @@ export async function middleware(request: NextRequest) { return NextResponse.next(); } + // API routes handle their own auth — skip middleware session check + if (pathname.startsWith("/api/")) { + return NextResponse.next(); + } + const isPublic = publicPaths.some( (path) => pathname === path || pathname.startsWith(path + "/"), ); @@ -27,11 +38,9 @@ export async function middleware(request: NextRequest) { return NextResponse.next(); } - const session = await auth.api.getSession({ - headers: request.headers, - }); - - if (!session && isProtected) { + // Cookie-presence check only — real auth happens in route handlers. + // auth.api.getSession() bypassed due to Drizzle queryWithCache bug (#12). + if (!hasSessionCookie(request.headers) && isProtected) { const signInUrl = new URL("/sign-in", request.url); signInUrl.searchParams.set("callbackUrl", pathname); return NextResponse.redirect(signInUrl);