- HttpOnly 쿠키(access_token)는 JavaScript에서 읽을 수 없어 FCM 초기화 실패 - non-HttpOnly is_authenticated 쿠키 추가로 클라이언트에서 인증 상태 확인 가능 - login/logout/refresh/proxy 라우트에서 쿠키 설정/삭제 처리 - hasAuthToken()이 is_authenticated 쿠키 확인하도록 변경
102 lines
3.4 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
} |