[feat]: 보호된 대시보드 및 API 라우트 추가

- 인증된 사용자용 대시보드 페이지 구현 ((protected) 라우트 그룹)
- API 엔드포인트 추가 (인증, 사용자 관리)
- 커스텀 훅 추가 (useAuth)
- 미들웨어 인증 로직 강화
- 환경변수 예제 업데이트
- 기존 dashboard 페이지 제거 후 보호된 라우트로 이동

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2025-11-10 09:38:59 +09:00
parent 56386e6d88
commit bf39fd22bd
14 changed files with 804 additions and 40 deletions

View File

@@ -0,0 +1,39 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
/**
* Auth Check Route Handler
*
* Purpose:
* - Check if user is authenticated (HttpOnly cookie validation)
* - Prevent browser back button cache issues
* - Real-time authentication validation
*/
export async function GET(request: NextRequest) {
try {
// Get token from HttpOnly cookie
const token = request.cookies.get('user_token')?.value;
if (!token) {
return NextResponse.json(
{ error: 'Not authenticated', authenticated: false },
{ status: 401 }
);
}
// Optional: Verify token with PHP backend
// (현재는 토큰 존재 여부만 확인, 필요시 PHP API 호출 추가 가능)
return NextResponse.json(
{ authenticated: true },
{ status: 200 }
);
} catch (error) {
console.error('Auth check error:', error);
return NextResponse.json(
{ error: 'Internal server error', authenticated: false },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,83 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
/**
* Login Proxy Route Handler
*
* Purpose:
* - Proxy login requests to PHP backend
* - Store token in HttpOnly cookie (XSS protection)
* - Never expose token to client JavaScript
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { user_id, user_pwd } = body;
// Validate input
if (!user_id || !user_pwd) {
return NextResponse.json(
{ error: 'User ID and password are required' },
{ status: 400 }
);
}
// Call PHP backend API
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
},
body: JSON.stringify({ user_id, user_pwd }),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
return NextResponse.json(
{
error: errorData.message || 'Login failed',
status: response.status
},
{ status: response.status }
);
}
const data = await response.json();
// Prepare response with user data (no token exposed)
const responseData = {
message: data.message,
user: data.user,
tenant: data.tenant,
menus: data.menus,
};
// Set HttpOnly cookie with token
const cookieOptions = [
`user_token=${data.user_token}`,
'HttpOnly', // ✅ JavaScript cannot access
'Secure', // ✅ HTTPS only (production)
'SameSite=Strict', // ✅ CSRF protection
'Path=/',
'Max-Age=604800', // 7 days
].join('; ');
console.log('✅ Login successful - Token stored in HttpOnly cookie');
return NextResponse.json(responseData, {
status: 200,
headers: {
'Set-Cookie': cookieOptions,
},
});
} catch (error) {
console.error('Login proxy error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,64 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
/**
* Logout Proxy Route Handler
*
* Purpose:
* - Call PHP backend logout API
* - Clear HttpOnly cookie
* - Ensure complete session cleanup
*/
export async function POST(request: NextRequest) {
try {
// Get token from HttpOnly cookie
const token = request.cookies.get('user_token')?.value;
if (token) {
// 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 ${token}`,
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
},
});
console.log('✅ Backend logout API called successfully');
} catch (error) {
console.warn('⚠️ Backend logout API failed (continuing with cookie deletion):', error);
}
}
// Clear HttpOnly cookie
const cookieOptions = [
'user_token=',
'HttpOnly',
'Secure',
'SameSite=Strict',
'Path=/',
'Max-Age=0', // Delete immediately
].join('; ');
console.log('✅ Logout complete - HttpOnly cookie cleared');
return NextResponse.json(
{ message: 'Logged out successfully' },
{
status: 200,
headers: {
'Set-Cookie': cookieOptions,
},
}
);
} catch (error) {
console.error('Logout proxy error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}