/** * GET /api/theia-auth * * Traefik ForwardAuth endpoint for Theia IDE domains. * * Handles two cases: * 1. theia.vibnai.com — shared IDE: any authenticated user may access * 2. {slug}.ide.vibnai.com — per-project IDE: only the project owner may access * * Traefik calls this URL for every request to those Theia domains, forwarding * the user's Cookie header via authRequestHeaders. We validate the NextAuth * database session directly in Postgres (avoids Prisma / authOptions build-time * issues under --network host). * * Returns: * 200 — valid session (and owner check passed), Traefik lets the request through * 302 — no/expired session, redirect browser to Vibn login * 403 — authenticated but not the project owner */ import { NextRequest, NextResponse } from 'next/server'; 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'; const IDE_SUFFIX = '.ide.vibnai.com'; const SESSION_COOKIE_NAMES = [ '__Secure-next-auth.session-token', 'next-auth.session-token', ]; export async function GET(request: NextRequest) { // ── 1. Extract session token ────────────────────────────────────────────── 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); // ── 2. Validate session in Postgres ────────────────────────────────────── let userEmail: string | null = null; let userName: string | null = null; let userId: string | null = null; try { const rows = await query<{ email: string; name: string; user_id: string }>( `SELECT u.email, u.name, s.user_id FROM sessions s JOIN users u ON u.id = s.user_id WHERE s.session_token = $1 AND s.expires > NOW() LIMIT 1`, [sessionToken], ); if (rows.length > 0) { userEmail = rows[0].email; userName = rows[0].name; userId = rows[0].user_id; } } catch (err) { console.error('[theia-auth] DB error:', err); return redirectToLogin(request); } if (!userEmail || !userId) return redirectToLogin(request); // ── 3. Per-project ownership check for *.ide.vibnai.com ────────────────── const forwardedHost = request.headers.get('x-forwarded-host') ?? ''; if (forwardedHost.endsWith(IDE_SUFFIX)) { const slug = forwardedHost.slice(0, -IDE_SUFFIX.length); try { const rows = await query<{ user_id: string }>( `SELECT user_id FROM fs_projects WHERE slug = $1 LIMIT 1`, [slug], ); if (rows.length === 0) { // Unknown project slug — deny return new NextResponse('Workspace not found', { status: 403 }); } const ownerUserId = rows[0].user_id; if (ownerUserId !== userId) { // Authenticated but not the owner return new NextResponse('Access denied — this workspace belongs to another user', { status: 403 }); } } catch (err) { console.error('[theia-auth] project ownership check error:', err); return redirectToLogin(request); } } // ── 4. Allow — pass user identity headers to Theia ─────────────────────── return new NextResponse(null, { status: 200, headers: { 'X-Auth-Email': userEmail, 'X-Auth-Name': userName ?? '', }, }); } function redirectToLogin(request: NextRequest): NextResponse { // Use THEIA_URL as the callbackUrl so the user lands back on Theia after login. // (X-Forwarded-Host points to vibnai.com via Traefik, not the original Theia domain.) const loginUrl = `${APP_URL}/auth?callbackUrl=${encodeURIComponent(THEIA_URL)}`; return NextResponse.redirect(loginUrl, { status: 302 }); }