import { createClient } from "@/utils/supabase/server"; import { type NextRequest, NextResponse } from "next/server"; // NextRequest 추가 import { AUTH_ERROR_MESSAGES, AUTH_ROUTES } from "@/features/auth/constants"; import { getAuthErrorMessage } from "@/features/auth/errors"; /** * OAuth/이메일 인증 콜백 처리 * * Supabase 인증 후 리다이렉트되는 라우트입니다. * - 인증 코드를 세션으로 교환합니다. * - 인증 에러를 처리합니다. * - 최종 목적지(Next URL)로 리다이렉트합니다. */ export async function GET(request: NextRequest) { // -------------------------------------------------------------------------- // 1. 요청 파라미터 및 URL 준비 // -------------------------------------------------------------------------- const requestUrl = request.nextUrl.clone(); // URL 조작을 위해 복제 const code = requestUrl.searchParams.get("code"); const next = requestUrl.searchParams.get("next") ?? AUTH_ROUTES.HOME; // 에러 파라미터 확인 const error = requestUrl.searchParams.get("error"); const error_code = requestUrl.searchParams.get("error_code"); const error_description = requestUrl.searchParams.get("error_description"); const origin = requestUrl.origin; // -------------------------------------------------------------------------- // 2. 초기 에러 처리 (Provider 레벨 에러) // -------------------------------------------------------------------------- if (error) { console.error("Auth callback error parameter:", { error, error_code, error_description, }); let message: string = AUTH_ERROR_MESSAGES.DEFAULT; if (error === "access_denied") { message = AUTH_ERROR_MESSAGES.OAUTH_ACCESS_DENIED; } else if (error === "server_error") { message = AUTH_ERROR_MESSAGES.OAUTH_SERVER_ERROR; } // 로그인 페이지로 에러와 함께 이동 return NextResponse.redirect( `${origin}${AUTH_ROUTES.LOGIN}?message=${encodeURIComponent(message)}`, ); } // -------------------------------------------------------------------------- // 3. 인증 코드 교환 (Supabase 공식 패턴 적용) // -------------------------------------------------------------------------- if (code) { const supabase = await createClient(); // 코드 교환 실행 const { error: exchangeError } = await supabase.auth.exchangeCodeForSession(code); if (!exchangeError) { // ---------------------------------------------------------------------- // 3-1. 교환 성공: 리다이렉트 처리 // code 교환으로 세션이 생성된 상태입니다. // ---------------------------------------------------------------------- const { data: { user }, } = await supabase.auth.getUser(); // 회원가입 직후 인증 여부 확인 (생성된 지 1분 이내) // 별도의 type 파라미터 없이 데이터 기반으로 판단합니다. const isNewUser = user && user.created_at && new Date(user.created_at).getTime() > Date.now() - 60 * 1000; // 1분 이내 생성 // 회원가입 인증인 경우: // 이메일 인증만 완료하고, 자동 로그인된 세션은 종료시킨 뒤 로그인 페이지로 보냅니다. if (isNewUser) { await supabase.auth.signOut(); return NextResponse.redirect( `${origin}${AUTH_ROUTES.LOGIN}?message=${encodeURIComponent( AUTH_ERROR_MESSAGES.EMAIL_VERIFIED_SUCCESS, )}`, ); } // 그 외 일반적인 로그인/인증인 경우: // 코드 파라미터 등을 제거하고 깨끗한 URL로 이동합니다. const forwardedHost = request.headers.get("x-forwarded-host"); // 로드밸런서 지원 const isLocalEnv = process.env.NODE_ENV === "development"; // 리다이렉트할 최종 URL 설정 if (isLocalEnv) { // 로컬 개발 환경 return NextResponse.redirect(`${origin}${next}`); } else if (forwardedHost) { // 프로덕션 환경 (Vercel 등 프록시 뒤) return NextResponse.redirect(`https://${forwardedHost}${next}`); } else { // 기본 return NextResponse.redirect(`${origin}${next}`); } } // ------------------------------------------------------------------------ // 3-2. 교환 실패: 에러 처리 // ------------------------------------------------------------------------ console.error("Auth exchange error:", exchangeError.message); const message = getAuthErrorMessage(exchangeError); return NextResponse.redirect( `${origin}${AUTH_ROUTES.LOGIN}?message=${encodeURIComponent(message)}`, ); } // -------------------------------------------------------------------------- // 4. 잘못된 접근 처리 // -------------------------------------------------------------------------- const errorMessage = encodeURIComponent( AUTH_ERROR_MESSAGES.INVALID_AUTH_LINK, ); return NextResponse.redirect( `${origin}${AUTH_ROUTES.LOGIN}?message=${errorMessage}`, ); }