import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, REFRESH_TOKEN, SERVER_API_URL, SITE_ID, DEVICE_ID, } from "@/shared/config"; import { cookies } from "next/headers"; import { NextResponse } from "next/server"; async function handler( request: Request, params: { url: string[] }, method: "GET" | "POST" | "PUT" | "DELETE", ) { const cookieStore = await cookies(); const headers: Record = { "Content-Type": "application/json", "X-Site-Id": SITE_ID, }; let body = {}; const isRefreshHandler = params.url.includes("refresh"); const isLoginHandler = params.url.includes("login"); // ── Silent Refresh ────────────────────────────────────────────────────────── // HttpOnly 쿠키에서 refresh-token 꺼내 Authorization 헤더에 주입. // 쿠키에 저장된 device-id도 X-Device-Id 헤더로 함께 전달한다. // 백엔드: TokenProvider.refreshToken(token, request, response) // → userService.findByRefreshToken(refreshToken, deviceId) (Redis Hash 검증) // → userService.updateRefreshToken(userUnqId, newToken, deviceId) (RTR 갱신) if (isRefreshHandler) { const refreshToken = cookieStore.get(REFRESH_TOKEN); headers[CLAIM_NAME] = refreshToken?.value ?? ""; if (!headers[CLAIM_NAME]) { return NextResponse.json({ message: "Can't refresh" }, { status: 401 }); } // device-id 쿠키 → X-Device-Id 헤더 (Redis 다중 세션 식별) const deviceId = cookieStore.get(DEVICE_ID); if (deviceId?.value) { headers["X-Device-Id"] = deviceId.value; } } // ── Login ──────────────────────────────────────────────────────────────────── // 클라이언트(login-service.ts)가 fetch 헤더에 X-Device-Id 를 포함해서 전송한다. // BFF는 해당 헤더를 그대로 백엔드로 포워딩한다. // 백엔드: AuthenticationFilter.successfulAuthentication // → TokenProvider.createTokenAndAddHeader(request, ...) // → request.getHeader("X-Device-Id") 로 deviceId 추출 // → userService.updateRefreshToken(userUnqId, refreshToken, deviceId) // → Redis Hash[user:sessions:{userUnqId}][deviceId] = refreshToken (TTL 1일) if (isLoginHandler) { const deviceId = request.headers.get("X-Device-Id"); if (deviceId) { headers["X-Device-Id"] = deviceId; } } try { if (!isRefreshHandler) { body = await request.json(); } const result = await fetch(`${SERVER_API_URL}/${params.url.join("/")}`, { method, headers, body: JSON.stringify(body), }); if (!result.ok) { return NextResponse.json( { message: "Invalid Credentials" }, { status: 401 }, ); } const accessToken = result.headers.get(ACCESS_TOKEN); const refreshToken = result.headers.get(REFRESH_TOKEN); const userId = result.headers.get(AUTH_USER_ID); if (accessToken && userId) { const response = NextResponse.json({ [ACCESS_TOKEN]: accessToken, [AUTH_USER_ID]: userId, }); const forwardedHost = request.headers.get("x-forwarded-host"); const host = request.headers.get("host"); const rawHost = forwardedHost || host || new URL(request.url).hostname.toString(); // 포트 번호 제거 (예: "localhost:3000" → "localhost") const hostname = rawHost.split(":")[0]; // 쿠키 domain 결정 // - localhost / 127.0.0.1: domain 미지정 (undefined) → 브라우저 기본 동작 // - 운영(hasuon.or.kr): 서브도메인 공유를 위해 최상위 도메인 앞에 점(.) 붙임 // - 그 외(IP, 기타 도메인): 현재 hostname 그대로 사용 const isLocal = hostname === "localhost" || hostname === "127.0.0.1"; const domain = isLocal ? undefined : hostname.includes("hasuon.or.kr") ? ".hasuon.or.kr" : hostname; if (refreshToken) { cookieStore.set(REFRESH_TOKEN, refreshToken, { httpOnly: true, sameSite: "strict", ...(domain !== undefined && { domain }), path: "/", }); } return response; } return NextResponse.json( { message: "Invalid Credentials" }, { status: 401 }, ); } catch (error) { return NextResponse.json( { message: "Server Error", error }, { status: 500 }, ); } } export async function POST( request: Request, props: { params: Promise<{ url: string[] }> }, ) { const params = await props.params; return handler(request, params, "POST"); } export async function PUT( request: Request, props: { params: Promise<{ url: string[] }> }, ) { const params = await props.params; return handler(request, params, "PUT"); }