[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:
@@ -1,13 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher';
|
||||
import WelcomeMessage from '@/components/WelcomeMessage';
|
||||
import NavigationMenu from '@/components/NavigationMenu';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { LogOut } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* Dashboard Page with Internationalization
|
||||
*
|
||||
* Note: Authentication protection is handled by (protected)/layout.tsx
|
||||
*/
|
||||
export default function Dashboard() {
|
||||
const t = useTranslations('common');
|
||||
const router = useRouter();
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
// ✅ HttpOnly Cookie 방식: Next.js API Route로 프록시
|
||||
const response = await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('✅ 로그아웃 완료: HttpOnly 쿠키 삭제됨');
|
||||
}
|
||||
|
||||
// 로그인 페이지로 리다이렉트
|
||||
router.push('/login');
|
||||
} catch (error) {
|
||||
console.error('로그아웃 처리 중 오류:', error);
|
||||
// 에러가 나도 로그인 페이지로 이동
|
||||
router.push('/login');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-8">
|
||||
@@ -17,7 +45,17 @@ export default function Dashboard() {
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{t('appName')}
|
||||
</h1>
|
||||
<LanguageSwitcher />
|
||||
<div className="flex items-center gap-3">
|
||||
<LanguageSwitcher />
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleLogout}
|
||||
className="rounded-xl"
|
||||
>
|
||||
<LogOut className="w-4 h-4 mr-2" />
|
||||
Logout
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
29
src/app/[locale]/(protected)/layout.tsx
Normal file
29
src/app/[locale]/(protected)/layout.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||
|
||||
/**
|
||||
* Protected Layout
|
||||
*
|
||||
* Purpose:
|
||||
* - Apply authentication guard to all protected pages
|
||||
* - Prevent browser back button cache issues
|
||||
* - Centralized protection for all routes under (protected)
|
||||
*
|
||||
* Protected Routes:
|
||||
* - /dashboard
|
||||
* - /profile
|
||||
* - /settings
|
||||
* - /admin/*
|
||||
* - All other authenticated pages
|
||||
*/
|
||||
export default function ProtectedLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
// 🔒 모든 하위 페이지에 인증 보호 적용
|
||||
useAuthGuard();
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
39
src/app/api/auth/check/route.ts
Normal file
39
src/app/api/auth/check/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
83
src/app/api/auth/login/route.ts
Normal file
83
src/app/api/auth/login/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
64
src/app/api/auth/logout/route.ts
Normal file
64
src/app/api/auth/logout/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user