Files
sam-react-prod/src/app/api/auth/logout/route.ts
kent df51cf6852 fix(WEB): FCM 토큰 등록을 위한 is_authenticated 쿠키 추가
- HttpOnly 쿠키(access_token)는 JavaScript에서 읽을 수 없어 FCM 초기화 실패
- non-HttpOnly is_authenticated 쿠키 추가로 클라이언트에서 인증 상태 확인 가능
- login/logout/refresh/proxy 라우트에서 쿠키 설정/삭제 처리
- hasAuthToken()이 is_authenticated 쿠키 확인하도록 변경
2026-01-06 21:47:57 +09:00

102 lines
3.4 KiB
TypeScript

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
/**
* 🔵 Next.js 내부 API - 로그아웃 프록시 (PHP 백엔드로 전달)
*
* ⚡ 설계 목적:
* - 완전한 로그아웃: PHP 백엔드 토큰 무효화 + 쿠키 삭제
* - 보안: HttpOnly 쿠키에서 토큰을 읽어 백엔드로 전달
* - 세션 정리: 클라이언트와 서버 양쪽 모두 세션 종료
*
* 🔄 동작 흐름:
* 1. 클라이언트 → Next.js /api/auth/logout
* 2. Next.js: HttpOnly 쿠키에서 access_token 읽기
* 3. Next.js → PHP /api/v1/logout (토큰 무효화 요청)
* 4. Next.js: access_token, refresh_token 쿠키 삭제
* 5. Next.js → 클라이언트 (로그아웃 성공 응답)
*
* 🔐 보안 특징:
* - 백엔드에서 토큰 블랙리스트 처리 (재사용 방지)
* - 쿠키 완전 삭제 (Max-Age=0)
* - 로그아웃 실패해도 쿠키는 삭제 (클라이언트 보호)
*
* ⚠️ 주의:
* - 이 API는 PHP /api/v1/logout의 프록시입니다
* - 백엔드 호출 실패해도 쿠키는 삭제됩니다 (안전 우선)
*/
export async function POST(request: NextRequest) {
try {
// Get access_token from HttpOnly cookie
const accessToken = request.cookies.get('access_token')?.value;
if (accessToken) {
// Call PHP backend logout API
try {
await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/logout`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${accessToken}`,
'X-API-KEY': process.env.API_KEY || '',
},
});
console.log('✅ Backend logout API called successfully');
} catch (error) {
console.warn('⚠️ Backend logout API failed (continuing with cookie deletion):', error);
}
}
// Clear both HttpOnly cookies
// Safari compatibility: Must use same attributes as when setting cookies
const isProduction = process.env.NODE_ENV === 'production';
const clearAccessToken = [
'access_token=',
'HttpOnly',
...(isProduction ? ['Secure'] : []), // ✅ Match login/check cookie attributes
'SameSite=Lax', // ✅ Match login/check cookie attributes
'Path=/',
'Max-Age=0', // Delete immediately
].join('; ');
const clearRefreshToken = [
'refresh_token=',
'HttpOnly',
...(isProduction ? ['Secure'] : []), // ✅ Match login/check cookie attributes
'SameSite=Lax', // ✅ Match login/check cookie attributes
'Path=/',
'Max-Age=0', // Delete immediately
].join('; ');
// ✅ is_authenticated 쿠키도 삭제 (FCM 인증 상태 플래그)
const clearIsAuthenticated = [
'is_authenticated=',
...(isProduction ? ['Secure'] : []),
'SameSite=Lax',
'Path=/',
'Max-Age=0',
].join('; ');
console.log('✅ Logout complete - Access & Refresh tokens cleared');
const response = NextResponse.json(
{ message: 'Logged out successfully' },
{ status: 200 }
);
response.headers.append('Set-Cookie', clearAccessToken);
response.headers.append('Set-Cookie', clearRefreshToken);
response.headers.append('Set-Cookie', clearIsAuthenticated);
return response;
} catch (error) {
console.error('Logout proxy error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}