import { NextRequest, NextResponse } from 'next/server'; /** * ๐Ÿ”ต Catch-All API Proxy (HttpOnly Cookie Pattern) * * โšก ์„ค๊ณ„ ๋ชฉ์ : * - HttpOnly ์ฟ ํ‚ค ๋ณด์•ˆ ์œ ์ง€: JavaScript ์ ‘๊ทผ ์ฐจ๋‹จ * - ๋ชจ๋“  ๋ฐฑ์—”๋“œ API๋ฅผ ๋‹จ์ผ ํ”„๋ก์‹œ๋กœ ์ฒ˜๋ฆฌ * - ์„œ๋ฒ„์—์„œ ์ฟ ํ‚ค ์ฝ์–ด Authorization ํ—ค๋” ์ž๋™ ์ถ”๊ฐ€ * * ๐Ÿ”„ ๋™์ž‘ ํ๋ฆ„: * 1. ํด๋ผ์ด์–ธํŠธ โ†’ Next.js /api/proxy/* (ํ† ํฐ ์—†์ด) * 2. Next.js: HttpOnly ์ฟ ํ‚ค์—์„œ access_token ์ฝ๊ธฐ (์„œ๋ฒ„์—์„œ๋งŒ ๊ฐ€๋Šฅ) * 3. Next.js โ†’ PHP Backend /api/v1/* (Authorization ํ—ค๋” ํฌํ•จ) * 4. PHP Backend โ†’ Next.js (์‘๋‹ต) * 5. Next.js โ†’ ํด๋ผ์ด์–ธํŠธ (์‘๋‹ต ์ „๋‹ฌ) * * ๐Ÿ” ๋ณด์•ˆ ํŠน์ง•: * - HttpOnly ์ฟ ํ‚ค: JavaScript ์ ‘๊ทผ ๋ถˆ๊ฐ€ (XSS ๋ฐฉ์ง€) * - ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ํ† ํฐ ์ฒ˜๋ฆฌ: ๋ธŒ๋ผ์šฐ์ €์— ํ† ํฐ ๋…ธ์ถœ ์•ˆ๋จ * - ์ž๋™ ์ธ์ฆ ํ—ค๋” ์ถ”๊ฐ€: ํด๋ผ์ด์–ธํŠธ๋Š” ์‹ ๊ฒฝ ์“ธ ํ•„์š” ์—†์Œ * * ๐Ÿ“ ์‚ฌ์šฉ ์˜ˆ์‹œ: * - Frontend: fetch('/api/proxy/item-master/init') * - Backend: GET https://api.codebridge-x.com/api/v1/item-master/init * * โš ๏ธ ์ฃผ์˜: * - ๋กœ๊ทธ์•„์›ƒ API(/api/auth/logout)์™€ ๋™์ผํ•œ ํŒจํ„ด * - ๋ชจ๋“  HTTP ๋ฉ”์„œ๋“œ ์ง€์› (GET, POST, PUT, DELETE) * - ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ์™€ ์š”์ฒญ ๋ฐ”๋”” ๋ชจ๋‘ ์ „๋‹ฌ */ /** * ํ† ํฐ ๊ฐฑ์‹  ํ•จ์ˆ˜ (access_token ๋งŒ๋ฃŒ ์‹œ refresh_token์œผ๋กœ ๊ฐฑ์‹ ) */ async function refreshAccessToken(refreshToken: string): Promise<{ success: boolean; accessToken?: string; refreshToken?: string; expiresIn?: number; }> { try { const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '', }, body: JSON.stringify({ refresh_token: refreshToken, }), }); if (!response.ok) { console.warn('๐Ÿ”ด [PROXY] Token refresh failed'); return { success: false }; } const data = await response.json(); console.log('โœ… [PROXY] Token refreshed successfully'); return { success: true, accessToken: data.access_token, refreshToken: data.refresh_token, expiresIn: data.expires_in, }; } catch (error) { console.error('๐Ÿ”ด [PROXY] Token refresh error:', error); return { success: false }; } } /** * Catch-all proxy handler for all HTTP methods */ async function proxyRequest( request: NextRequest, params: { path: string[] }, method: string ) { try { // 1. HttpOnly ์ฟ ํ‚ค์—์„œ ํ† ํฐ ์ฝ๊ธฐ (์„œ๋ฒ„์—์„œ๋งŒ ๊ฐ€๋Šฅ!) let token = request.cookies.get('access_token')?.value; const refreshToken = request.cookies.get('refresh_token')?.value; // 1-1. access_token์ด ์—†๊ณ  refresh_token์ด ์žˆ์œผ๋ฉด ์ž๋™ ๊ฐฑ์‹  let newTokens: { accessToken?: string; refreshToken?: string; expiresIn?: number } | null = null; if (!token && refreshToken) { console.log('๐Ÿ”„ [PROXY] No access_token, attempting refresh...'); const refreshResult = await refreshAccessToken(refreshToken); if (refreshResult.success && refreshResult.accessToken) { token = refreshResult.accessToken; newTokens = refreshResult; } } // 2. ๋ฐฑ์—”๋“œ URL ๊ตฌ์„ฑ const backendUrl = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/${params.path.join('/')}`; // ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ const url = new URL(backendUrl); request.nextUrl.searchParams.forEach((value, key) => { url.searchParams.append(key, value); }); // 3. ์š”์ฒญ ๋ฐ”๋”” ์ฝ๊ธฐ (POST, PUT, DELETE) let body: string | undefined; if (['POST', 'PUT', 'DELETE'].includes(method)) { // Content-Type์— ๋”ฐ๋ผ ๋ฐ”๋”” ์ฒ˜๋ฆฌ const contentType = request.headers.get('content-type') || ''; if (contentType.includes('application/json')) { body = await request.text(); // ๐Ÿ” ๋””๋ฒ„๊น…: ์ „์†ก ๋ฐ์ดํ„ฐ ๋กœ๊ทธ console.log('๐Ÿ”ต [PROXY DEBUG] Request Details:'); console.log(' Method:', method); console.log(' URL:', url.toString()); console.log(' Body:', body); console.log(' Token:', token ? `${token.substring(0, 20)}...` : 'null'); } else if (contentType.includes('multipart/form-data')) { // FormData๋Š” ๊ทธ๋Œ€๋กœ ์ „๋‹ฌ const formData = await request.formData(); // FormData๋ฅผ ๋ฐฑ์—”๋“œ๋กœ ์ „๋‹ฌํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์‹œ ๋ณ€ํ™˜ body = await request.text(); } } // 4. ๋ฐฑ์—”๋“œ๋กœ ํ”„๋ก์‹œ ์š”์ฒญ const backendResponse = await fetch(url.toString(), { method, headers: { 'Content-Type': request.headers.get('content-type') || 'application/json', 'Accept': 'application/json', 'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '', 'Authorization': token ? `Bearer ${token}` : '', }, body, }); // 5. ์‘๋‹ต ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ const responseData = await backendResponse.text(); // ๐Ÿ” ๋””๋ฒ„๊น…: ๋ฐฑ์—”๋“œ ์‘๋‹ต ๋กœ๊ทธ console.log('๐Ÿ”ต [PROXY DEBUG] Backend Response:'); console.log(' Status:', backendResponse.status); console.log(' Response:', responseData.substring(0, 500)); // ์ฒ˜์Œ 500์ž๋งŒ // 6. ํด๋ผ์ด์–ธํŠธ๋กœ ์‘๋‹ต ์ „๋‹ฌ const clientResponse = new NextResponse(responseData, { status: backendResponse.status, headers: { 'Content-Type': backendResponse.headers.get('content-type') || 'application/json', }, }); // 6-1. ํ† ํฐ์ด ๊ฐฑ์‹ ๋˜์—ˆ์œผ๋ฉด ์ƒˆ ์ฟ ํ‚ค ์„ค์ • if (newTokens && newTokens.accessToken) { const accessTokenCookie = [ `access_token=${newTokens.accessToken}`, 'HttpOnly', 'Secure', 'SameSite=Strict', 'Path=/', `Max-Age=${newTokens.expiresIn || 7200}`, ].join('; '); clientResponse.headers.append('Set-Cookie', accessTokenCookie); if (newTokens.refreshToken) { const refreshTokenCookie = [ `refresh_token=${newTokens.refreshToken}`, 'HttpOnly', 'Secure', 'SameSite=Strict', 'Path=/', 'Max-Age=604800', // 7 days ].join('; '); clientResponse.headers.append('Set-Cookie', refreshTokenCookie); } console.log('๐Ÿช [PROXY] New tokens set in cookies'); } return clientResponse; } catch (error) { console.error('Proxy request error:', error); return NextResponse.json( { error: 'Proxy server error' }, { status: 500 } ); } } /** * GET ์š”์ฒญ ํ”„๋ก์‹œ * Next.js 15: params๋Š” Promise์ด๋ฏ€๋กœ await ํ•„์š” */ export async function GET( request: NextRequest, { params }: { params: Promise<{ path: string[] }> } ) { const resolvedParams = await params; return proxyRequest(request, resolvedParams, 'GET'); } /** * POST ์š”์ฒญ ํ”„๋ก์‹œ * Next.js 15: params๋Š” Promise์ด๋ฏ€๋กœ await ํ•„์š” */ export async function POST( request: NextRequest, { params }: { params: Promise<{ path: string[] }> } ) { const resolvedParams = await params; return proxyRequest(request, resolvedParams, 'POST'); } /** * PUT ์š”์ฒญ ํ”„๋ก์‹œ * Next.js 15: params๋Š” Promise์ด๋ฏ€๋กœ await ํ•„์š” */ export async function PUT( request: NextRequest, { params }: { params: Promise<{ path: string[] }> } ) { const resolvedParams = await params; return proxyRequest(request, resolvedParams, 'PUT'); } /** * DELETE ์š”์ฒญ ํ”„๋ก์‹œ * Next.js 15: params๋Š” Promise์ด๋ฏ€๋กœ await ํ•„์š” */ export async function DELETE( request: NextRequest, { params }: { params: Promise<{ path: string[] }> } ) { const resolvedParams = await params; return proxyRequest(request, resolvedParams, 'DELETE'); }