import { NextResponse, type NextRequest } from 'next/server'; import type { EmailOtpType } from '@supabase/supabase-js'; import { createSupabaseServerClient } from '@/lib/supabase/server'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; /** * Whitelist `next` to same-origin relative paths only (open-redirect guard). * Anything that isn't a single-slash-rooted path falls back to /dashboard. */ function safeNext(raw: string | null): string { if (!raw || !raw.startsWith('/') || raw.startsWith('//')) { return '/dashboard'; } return raw; } /** * Email confirmation / OTP verification landing route. * * Handles both GoTrue link styles defensively: * - token_hash template: /auth/confirm?token_hash=&type=signup -> verifyOtp * - PKCE/code redirect: /auth/confirm?code= -> exchangeCodeForSession * * On success the server client persists the session cookies and we redirect to * `next` (default /dashboard). On any failure we send the user to /login with a * verification_failed flag so they can resend a fresh confirmation email. */ export async function GET(request: NextRequest) { const url = request.nextUrl; const token_hash = url.searchParams.get('token_hash'); const type = url.searchParams.get('type') as EmailOtpType | null; const code = url.searchParams.get('code'); const next = safeNext(url.searchParams.get('next')); const base = process.env.NEXT_PUBLIC_APP_URL ?? url.origin; const supabase = createSupabaseServerClient(); if (token_hash && type) { const { error } = await supabase.auth.verifyOtp({ type, token_hash }); if (!error) { return NextResponse.redirect(new URL(next, base)); } } else if (code) { const { error } = await supabase.auth.exchangeCodeForSession(code); if (!error) { return NextResponse.redirect(new URL(next, base)); } } return NextResponse.redirect(new URL('/login?error=verification_failed', base)); }