summaryrefslogtreecommitdiff
path: root/app/api/client-logs
diff options
context:
space:
mode:
Diffstat (limited to 'app/api/client-logs')
-rw-r--r--app/api/client-logs/route.ts259
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