From efcf20b8f6afce3a05e6c7eb18b0d2d93d9ad262 Mon Sep 17 00:00:00 2001 From: Mark Henderson Date: Wed, 18 Feb 2026 16:33:47 -0800 Subject: [PATCH] fix: validate Theia auth via direct Postgres session lookup Avoid importing authOptions/PrismaClient in the forwardAuth endpoint. Under --network host (Coolify's build flag), routes that import Prisma at module evaluation time are silently dropped from the build output. Instead, read the NextAuth session-token cookie directly and verify it with a raw SQL query against the sessions table - the same pattern used by other working API routes via @/lib/db-postgres. Co-authored-by: Cursor --- app/api/theia-auth/route.ts | 87 +++++++++++++++++++++++++++---------- 1 file changed, 63 insertions(+), 24 deletions(-) diff --git a/app/api/theia-auth/route.ts b/app/api/theia-auth/route.ts index 1e5d14b..e938965 100644 --- a/app/api/theia-auth/route.ts +++ b/app/api/theia-auth/route.ts @@ -5,48 +5,87 @@ * * Traefik calls this URL for every request to the Theia IDE, forwarding * the user's Cookie header via authRequestHeaders. We validate the - * NextAuth session and return: - * 200 — session valid, Traefik lets the request through - * 302 — no session, redirect browser to Vibn login + * NextAuth database session (strategy: "database") by looking up the + * session token directly in Postgres — avoiding Prisma / authOptions + * imports that cause build-time issues under --network host. + * + * Returns: + * 200 — valid session, Traefik lets the request through + * 302 — no/expired session, redirect browser to Vibn login */ import { NextRequest, NextResponse } from 'next/server'; -import { getServerSession } from 'next-auth'; -import { authOptions } from '@/lib/auth/authOptions'; +import { query } from '@/lib/db-postgres'; + +export const dynamic = 'force-dynamic'; const APP_URL = process.env.NEXTAUTH_URL ?? 'https://vibnai.com'; const THEIA_URL = 'https://theia.vibnai.com'; +// NextAuth v4 uses these cookie names for database sessions +const SESSION_COOKIE_NAMES = [ + '__Secure-next-auth.session-token', // HTTPS (production) + 'next-auth.session-token', // HTTP fallback +]; + export async function GET(request: NextRequest) { - let session: Awaited> = null; + // Extract session token from cookies + let sessionToken: string | null = null; + for (const name of SESSION_COOKIE_NAMES) { + const val = request.cookies.get(name)?.value; + if (val) { sessionToken = val; break; } + } + + if (!sessionToken) { + return redirectToLogin(request); + } + + // Look up session in Postgres (NextAuth stores sessions in the "sessions" table) + let userEmail: string | null = null; + let userName: string | null = null; try { - session = await getServerSession(authOptions); + const result = await query<{ email: string; name: string }>( + `SELECT u.email, u.name + FROM sessions s + JOIN users u ON u.id = s."userId" + WHERE s."sessionToken" = $1 + AND s.expires > NOW() + LIMIT 1`, + [sessionToken], + ); + if (result.rows.length > 0) { + userEmail = result.rows[0].email; + userName = result.rows[0].name; + } } catch { - // Treat any session-validation errors as unauthenticated + // DB error → treat as unauthenticated + return redirectToLogin(request); } - if (!session?.user) { - // Build a callbackUrl so the user lands back in Theia after login - const forwardedHost = request.headers.get('x-forwarded-host'); - const forwardedProto = request.headers.get('x-forwarded-proto') ?? 'https'; - const forwardedUri = request.headers.get('x-forwarded-uri') ?? '/'; - - const destination = forwardedHost - ? `${forwardedProto}://${forwardedHost}${forwardedUri}` - : THEIA_URL; - - const loginUrl = `${APP_URL}/auth?callbackUrl=${encodeURIComponent(destination)}`; - - return NextResponse.redirect(loginUrl, { status: 302 }); + if (!userEmail) { + return redirectToLogin(request); } - // Session is valid — forward user identity to Theia via response headers + // Session valid — pass user identity to Theia via response headers return new NextResponse(null, { status: 200, headers: { - 'X-Auth-Email': session.user.email ?? '', - 'X-Auth-Name': session.user.name ?? '', + 'X-Auth-Email': userEmail, + 'X-Auth-Name': userName ?? '', }, }); } + +function redirectToLogin(request: NextRequest): NextResponse { + const forwardedHost = request.headers.get('x-forwarded-host'); + const forwardedProto = request.headers.get('x-forwarded-proto') ?? 'https'; + const forwardedUri = request.headers.get('x-forwarded-uri') ?? '/'; + + const destination = forwardedHost + ? `${forwardedProto}://${forwardedHost}${forwardedUri}` + : THEIA_URL; + + const loginUrl = `${APP_URL}/auth?callbackUrl=${encodeURIComponent(destination)}`; + return NextResponse.redirect(loginUrl, { status: 302 }); +}