150 lines
4.9 KiB
TypeScript
150 lines
4.9 KiB
TypeScript
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<string, string> = {
|
|
"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");
|
|
}
|