diff options
Diffstat (limited to 'app/api/client-logs/route.ts')
| -rw-r--r-- | app/api/client-logs/route.ts | 259 |
1 files changed, 259 insertions, 0 deletions
diff --git a/app/api/client-logs/route.ts b/app/api/client-logs/route.ts new file mode 100644 index 00000000..42e7db63 --- /dev/null +++ b/app/api/client-logs/route.ts @@ -0,0 +1,259 @@ +// 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 }); + } +}
\ No newline at end of file |
