chore(WEB): 다수 컴포넌트 개선 및 CEO 대시보드 추가
- CEO 대시보드 컴포넌트 추가 - AuthenticatedLayout 개선 - 각 모듈 actions.ts 에러 핸들링 개선 - API fetch-wrapper, refresh-token 로직 개선 - ReceivablesStatus 컴포넌트 업데이트 - globals.css 스타일 업데이트 - 기타 다수 컴포넌트 수정 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { refreshAccessToken } from '@/lib/api/refresh-token';
|
||||
|
||||
/**
|
||||
* 🔵 Next.js 내부 API - 인증 상태 확인 (PHP 백엔드 X)
|
||||
@@ -49,72 +50,48 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
// Only has refresh token - try to refresh
|
||||
if (refreshToken && !accessToken) {
|
||||
console.log('🔄 Access token missing, attempting refresh...');
|
||||
console.log('🔍 Refresh token exists:', refreshToken.substring(0, 20) + '...');
|
||||
console.log('🔍 Backend URL:', process.env.NEXT_PUBLIC_API_URL);
|
||||
console.log('🔄 [auth/check] Access token missing, attempting refresh...');
|
||||
|
||||
// Attempt token refresh
|
||||
try {
|
||||
const refreshResponse = 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.API_KEY || '',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
refresh_token: refreshToken,
|
||||
}),
|
||||
});
|
||||
// 공유 캐시를 사용하는 refreshAccessToken 함수 사용
|
||||
const refreshResult = await refreshAccessToken(refreshToken, 'auth/check');
|
||||
|
||||
console.log('🔍 Refresh API response status:', refreshResponse.status);
|
||||
if (refreshResult.success && refreshResult.accessToken) {
|
||||
console.log('✅ [auth/check] Token refreshed successfully');
|
||||
|
||||
if (refreshResponse.ok) {
|
||||
const data = await refreshResponse.json();
|
||||
// Set new tokens with Safari-compatible configuration
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
// Set new tokens with Safari-compatible configuration
|
||||
// Safari compatibility: Secure only in production (HTTPS)
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const accessTokenCookie = [
|
||||
`access_token=${refreshResult.accessToken}`,
|
||||
'HttpOnly',
|
||||
...(isProduction ? ['Secure'] : []),
|
||||
'SameSite=Lax',
|
||||
'Path=/',
|
||||
`Max-Age=${refreshResult.expiresIn || 7200}`,
|
||||
].join('; ');
|
||||
|
||||
const accessTokenCookie = [
|
||||
`access_token=${data.access_token}`,
|
||||
'HttpOnly', // ✅ JavaScript cannot access
|
||||
...(isProduction ? ['Secure'] : []), // ✅ HTTPS only in production (Safari fix)
|
||||
'SameSite=Lax', // ✅ CSRF protection (Lax for better compatibility)
|
||||
'Path=/',
|
||||
`Max-Age=${data.expires_in || 7200}`,
|
||||
].join('; ');
|
||||
const refreshTokenCookie = [
|
||||
`refresh_token=${refreshResult.refreshToken}`,
|
||||
'HttpOnly',
|
||||
...(isProduction ? ['Secure'] : []),
|
||||
'SameSite=Lax',
|
||||
'Path=/',
|
||||
'Max-Age=604800',
|
||||
].join('; ');
|
||||
|
||||
const refreshTokenCookie = [
|
||||
`refresh_token=${data.refresh_token}`,
|
||||
'HttpOnly', // ✅ JavaScript cannot access
|
||||
...(isProduction ? ['Secure'] : []), // ✅ HTTPS only in production (Safari fix)
|
||||
'SameSite=Lax', // ✅ CSRF protection (Lax for better compatibility)
|
||||
'Path=/',
|
||||
'Max-Age=604800', // 7 days (longer for refresh token)
|
||||
].join('; ');
|
||||
const response = NextResponse.json(
|
||||
{ authenticated: true, refreshed: true },
|
||||
{ status: 200 }
|
||||
);
|
||||
|
||||
console.log('✅ Token auto-refreshed in auth check');
|
||||
response.headers.append('Set-Cookie', accessTokenCookie);
|
||||
response.headers.append('Set-Cookie', refreshTokenCookie);
|
||||
|
||||
const response = NextResponse.json(
|
||||
{ authenticated: true, refreshed: true },
|
||||
{ status: 200 }
|
||||
);
|
||||
|
||||
response.headers.append('Set-Cookie', accessTokenCookie);
|
||||
response.headers.append('Set-Cookie', refreshTokenCookie);
|
||||
|
||||
return response;
|
||||
} else {
|
||||
const errorData = await refreshResponse.text();
|
||||
console.error('❌ Refresh API failed:', refreshResponse.status, errorData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Token refresh failed in auth check:', error);
|
||||
return response;
|
||||
}
|
||||
|
||||
// Refresh failed - not authenticated
|
||||
console.log('⚠️ Returning 401 due to refresh failure');
|
||||
console.log('⚠️ [auth/check] Refresh failed, returning 401');
|
||||
return NextResponse.json(
|
||||
{ error: 'Token refresh failed' },
|
||||
{ status: 401 }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { refreshAccessToken } from '@/lib/api/refresh-token';
|
||||
|
||||
/**
|
||||
* 🔵 Next.js 내부 API - 토큰 갱신 프록시 (PHP 백엔드로 전달)
|
||||
@@ -43,24 +44,14 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// Call PHP backend refresh API
|
||||
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.API_KEY || '',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
refresh_token: refreshToken,
|
||||
}),
|
||||
});
|
||||
// 공유 캐시를 사용하는 refreshAccessToken 함수 사용
|
||||
const refreshResult = await refreshAccessToken(refreshToken, 'api/auth/refresh');
|
||||
|
||||
if (!response.ok) {
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
if (!refreshResult.success || !refreshResult.accessToken) {
|
||||
// Refresh token is invalid or expired
|
||||
console.warn('⚠️ Token refresh failed - user needs to re-login');
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
console.warn('⚠️ [api/auth/refresh] Token refresh failed - user needs to re-login');
|
||||
|
||||
// Clear all tokens
|
||||
const clearAccessToken = `access_token=; HttpOnly; ${isProduction ? 'Secure; ' : ''}SameSite=Lax; Path=/; Max-Age=0`;
|
||||
@@ -79,30 +70,24 @@ export async function POST(request: NextRequest) {
|
||||
return failResponse;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Prepare response
|
||||
const responseData = {
|
||||
message: 'Token refreshed successfully',
|
||||
token_type: data.token_type,
|
||||
expires_in: data.expires_in,
|
||||
expires_at: data.expires_at,
|
||||
expires_in: refreshResult.expiresIn,
|
||||
};
|
||||
|
||||
// Set new HttpOnly cookies
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
const accessTokenCookie = [
|
||||
`access_token=${data.access_token}`,
|
||||
`access_token=${refreshResult.accessToken}`,
|
||||
'HttpOnly',
|
||||
...(isProduction ? ['Secure'] : []),
|
||||
'SameSite=Lax',
|
||||
'Path=/',
|
||||
`Max-Age=${data.expires_in || 7200}`,
|
||||
`Max-Age=${refreshResult.expiresIn || 7200}`,
|
||||
].join('; ');
|
||||
|
||||
const refreshTokenCookie = [
|
||||
`refresh_token=${data.refresh_token}`,
|
||||
`refresh_token=${refreshResult.refreshToken}`,
|
||||
'HttpOnly',
|
||||
...(isProduction ? ['Secure'] : []),
|
||||
'SameSite=Lax',
|
||||
@@ -116,10 +101,10 @@ export async function POST(request: NextRequest) {
|
||||
...(isProduction ? ['Secure'] : []),
|
||||
'SameSite=Lax',
|
||||
'Path=/',
|
||||
`Max-Age=${data.expires_in || 7200}`,
|
||||
`Max-Age=${refreshResult.expiresIn || 7200}`,
|
||||
].join('; ');
|
||||
|
||||
console.log('✅ Token refresh successful - New tokens stored');
|
||||
console.log('✅ [api/auth/refresh] Token refresh successful');
|
||||
|
||||
const successResponse = NextResponse.json(responseData, { status: 200 });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user