// app/api/client-logs/route.ts import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/app/api/auth/[...nextauth]/route'; import { createFileDownloadLog } from '@/lib/file-download-log/service'; import rateLimit from '@/lib/rate-limit'; import { z } from 'zod'; import { getRequestInfo } from '@/lib/network/get-client-ip'; // 클라이언트 로그 이벤트 스키마 const clientLogSchema = z.object({ event: z.enum(['download_attempt', 'download_success', 'download_error', 'security_violation']), data: z.object({ // 파일 관련 정보 filePath: z.string(), fileName: z.string(), fileExtension: z.string().optional(), fileSize: z.number().optional(), duration: z.number().optional(), // 액션 관련 정보 action: z.string().optional(), // 에러 관련 정보 error: z.string().optional(), // 보안 위반 관련 정보 type: z.string().optional(), // security_violation의 타입 // 클라이언트 환경 정보 timestamp: z.string(), userAgent: z.string(), url: z.string(), sessionId: z.string(), // 기타 정보 userInitiated: z.boolean().optional(), disableLogging: z.boolean().optional(), }), }); // 클라이언트 로그를 파일 다운로드 로그 형식으로 변환 function mapClientLogToDownloadLog(event: string, data: any, requestInfo: any, userId: string) { // 파일 ID는 클라이언트에서 알 수 없으므로 0으로 설정 (또는 파일 경로로 조회 시도) const fileId = 0; // 성공 여부 판단 const success = event === 'download_success'; // 에러 메시지 설정 let errorMessage = undefined; if (event === 'download_error') { errorMessage = data.error || 'Unknown client error'; } else if (event === 'security_violation') { errorMessage = `Security violation: ${data.type || 'unknown'} - ${data.error || ''}`; } // 요청 ID 생성 (클라이언트 세션 ID 활용) const requestId = data.sessionId || crypto.randomUUID(); return { fileId, success, errorMessage, requestId, downloadDurationMs: data.duration, fileInfo: { fileName: data.fileName, filePath: data.filePath, fileSize: data.fileSize || 0, }, // 클라이언트 로그 추가 정보 clientEventType: event, clientUserAgent: data.userAgent, clientUrl: data.url, clientTimestamp: data.timestamp, serverIp: requestInfo.ip, serverUserAgent: requestInfo.userAgent, }; } export async function POST(request: NextRequest) { const requestInfo = getRequestInfo(request); try { // Rate limiting 체크 const limiterResult = await rateLimit(request); if (!limiterResult.success) { console.warn('🚨 클라이언트 로그 Rate limit 초과:', { ip: requestInfo.ip, userAgent: requestInfo.userAgent }); return NextResponse.json( { error: "Too many requests" }, { status: 429 } ); } // 세션 확인 const session = await getServerSession(authOptions); if (!session?.user) { console.warn('🚨 인증되지 않은 클라이언트 로그 요청:', { ip: requestInfo.ip, userAgent: requestInfo.userAgent }); return NextResponse.json( { error: "Unauthorized" }, { status: 401 } ); } // 요청 본문 파싱 및 검증 const body = await request.json(); const validatedData = clientLogSchema.parse(body); const { event, data } = validatedData; // 로깅이 비활성화된 경우 스킵 (하지만 보안 위반은 항상 기록) if (data.disableLogging && event !== 'security_violation') { return NextResponse.json({ success: true, logged: false }); } // 클라이언트 로그를 서버 로그 형식으로 변환 const downloadLogData = mapClientLogToDownloadLog(event, data, requestInfo, session.user.id); // 기존 파일 다운로드 로그 서비스 사용 const result = await createFileDownloadLog(downloadLogData); if (!result.success) { console.error('클라이언트 로그 저장 실패:', result.error); return NextResponse.json( { error: "Failed to save log" }, { status: 500 } ); } // 성공 로그 (보안 위반은 경고 레벨로) if (event === 'security_violation') { console.warn('🚨 클라이언트 보안 위반 기록:', { type: data.type, filePath: data.filePath, fileName: data.fileName, userId: session.user.id, ip: requestInfo.ip, clientInfo: { userAgent: data.userAgent, url: data.url, sessionId: data.sessionId, } }); } else { console.log('📝 클라이언트 로그 기록:', { event, fileName: data.fileName, userId: session.user.id, success: event === 'download_success', logId: result.logId, }); } return NextResponse.json({ success: true, logged: true, logId: result.logId }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error('❌ 클라이언트 로그 API 오류:', { error: errorMessage, userId: (await getServerSession(authOptions))?.user?.id, ip: requestInfo.ip, userAgent: requestInfo.userAgent, }); // Zod 검증 에러 처리 if (error instanceof z.ZodError) { return NextResponse.json( { error: 'Invalid log data', details: error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ') }, { status: 400 } ); } return NextResponse.json( { error: 'Internal server error', details: process.env.NODE_ENV === 'development' ? errorMessage : undefined }, { status: 500 } ); } } // GET 요청으로 클라이언트 로그 조회 (관리자용) export async function GET(request: NextRequest) { try { const session = await getServerSession(authOptions); if (!session?.user || !session.user.roles?.includes('admin')) { return NextResponse.json( { error: "Admin access required" }, { status: 403 } ); } // 쿼리 파라미터 파싱 const searchParams = request.nextUrl.searchParams; const page = parseInt(searchParams.get('page') || '1', 10); const limit = parseInt(searchParams.get('limit') || '50', 10); const event = searchParams.get('event'); const userId = searchParams.get('userId'); const startDate = searchParams.get('startDate'); const endDate = searchParams.get('endDate'); // 여기서는 기존 로그 조회 함수를 사용하거나 // 클라이언트 로그 전용 조회 함수를 만들어야 합니다 // 현재는 간단한 응답으로 대체 return NextResponse.json({ message: "Client logs retrieval - implement with your existing log service", filters: { page, limit, event, userId, startDate, endDate } }); } catch (error) { console.error('❌ 클라이언트 로그 조회 오류:', error); return NextResponse.json( { error: 'Failed to retrieve logs' }, { status: 500 } ); } } // 클라이언트 로그 통계 조회 (관리자용) export async function HEAD(request: NextRequest) { try { const session = await getServerSession(authOptions); if (!session?.user || !session.user.roles?.includes('admin')) { return new NextResponse(null, { status: 403 }); } // 간단한 통계 정보를 헤더로 반환 const headers = new Headers(); headers.set('X-Total-Logs', '0'); // 실제 구현 필요 headers.set('X-Security-Violations', '0'); // 실제 구현 필요 headers.set('X-Success-Rate', '0%'); // 실제 구현 필요 return new NextResponse(null, { status: 200, headers }); } catch (error) { console.error('❌ 클라이언트 로그 통계 조회 오류:', error); return new NextResponse(null, { status: 500 }); } }