UNPKG

@restnfeel/agentc-starter-kit

Version:

한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템

592 lines (528 loc) 14 kB
export interface MediaAuditLog { id: string; timestamp: Date; userId: string; userRole: string; action: MediaAction; resourceType: MediaResourceType; resourceId: string; details: MediaActionDetails; metadata: MediaAuditMetadata; sessionId?: string; ipAddress?: string; userAgent?: string; success: boolean; errorMessage?: string; } export enum MediaAction { // 파일 작업 FILE_UPLOAD = "file_upload", FILE_DOWNLOAD = "file_download", FILE_VIEW = "file_view", FILE_DELETE = "file_delete", FILE_UPDATE = "file_update", FILE_MOVE = "file_move", FILE_COPY = "file_copy", // 폴더 작업 FOLDER_CREATE = "folder_create", FOLDER_DELETE = "folder_delete", FOLDER_UPDATE = "folder_update", FOLDER_MOVE = "folder_move", // 컬렉션 작업 COLLECTION_CREATE = "collection_create", COLLECTION_DELETE = "collection_delete", COLLECTION_UPDATE = "collection_update", COLLECTION_ADD_FILE = "collection_add_file", COLLECTION_REMOVE_FILE = "collection_remove_file", // 권한 작업 PERMISSION_GRANT = "permission_grant", PERMISSION_REVOKE = "permission_revoke", PERMISSION_UPDATE = "permission_update", // 보안 작업 ACCESS_DENIED = "access_denied", SIGNED_URL_GENERATE = "signed_url_generate", SIGNED_URL_ACCESS = "signed_url_access", WATERMARK_APPLY = "watermark_apply", WATERMARK_REMOVE = "watermark_remove", // CDN 작업 CDN_UPLOAD = "cdn_upload", CDN_DELETE = "cdn_delete", CDN_CACHE_INVALIDATE = "cdn_cache_invalidate", // 배치 작업 BATCH_UPLOAD = "batch_upload", BATCH_DELETE = "batch_delete", BATCH_PROCESS = "batch_process", } export enum MediaResourceType { FILE = "file", FOLDER = "folder", COLLECTION = "collection", PERMISSION = "permission", SIGNED_URL = "signed_url", CDN = "cdn", SYSTEM = "system", } export interface MediaActionDetails { fileName?: string; fileSize?: number; mimeType?: string; originalPath?: string; newPath?: string; collectionName?: string; folderName?: string; permissionType?: string; targetUserId?: string; signedUrlDuration?: number; watermarkOptions?: Record<string, unknown>; batchSize?: number; processingOptions?: Record<string, unknown>; } export interface MediaAuditMetadata { beforeState?: Record<string, unknown>; afterState?: Record<string, unknown>; additionalData?: Record<string, unknown>; correlationId?: string; parentLogId?: string; tags?: string[]; } export interface AuditLogQuery { userId?: string; action?: MediaAction[]; resourceType?: MediaResourceType[]; resourceId?: string; startDate?: Date; endDate?: Date; success?: boolean; ipAddress?: string; limit?: number; offset?: number; sortBy?: "timestamp" | "action" | "userId"; sortOrder?: "asc" | "desc"; } export interface AuditLogStats { totalLogs: number; actionCounts: Record<MediaAction, number>; resourceTypeCounts: Record<MediaResourceType, number>; successRate: number; topUsers: Array<{ userId: string; count: number }>; recentActivity: MediaAuditLog[]; } export class MediaAuditLogger { private logs: Map<string, MediaAuditLog> = new Map(); private logBuffer: MediaAuditLog[] = []; private bufferSize: number = 100; private flushInterval: number = 30000; // 30초 private flushTimer: NodeJS.Timeout | null = null; constructor(bufferSize?: number, flushInterval?: number) { this.bufferSize = bufferSize || 100; this.flushInterval = flushInterval || 30000; this.startAutoFlush(); } /** * 감사 로그 기록 */ async log( userId: string, userRole: string, action: MediaAction, resourceType: MediaResourceType, resourceId: string, details: MediaActionDetails = {}, metadata: MediaAuditMetadata = {}, context?: { sessionId?: string; ipAddress?: string; userAgent?: string; success?: boolean; errorMessage?: string; } ): Promise<string> { const logId = `log_${Date.now()}_${Math.random() .toString(36) .substr(2, 9)}`; const auditLog: MediaAuditLog = { id: logId, timestamp: new Date(), userId, userRole, action, resourceType, resourceId, details, metadata, sessionId: context?.sessionId, ipAddress: context?.ipAddress, userAgent: context?.userAgent, success: context?.success ?? true, errorMessage: context?.errorMessage, }; // 메모리에 저장 this.logs.set(logId, auditLog); // 버퍼에 추가 this.logBuffer.push(auditLog); // 버퍼가 가득 차면 즉시 플러시 if (this.logBuffer.length >= this.bufferSize) { await this.flushLogs(); } return logId; } /** * 파일 업로드 감사 로그 */ async logFileUpload( userId: string, userRole: string, fileId: string, fileName: string, fileSize: number, mimeType: string, success: boolean = true, errorMessage?: string, context?: { sessionId?: string; ipAddress?: string; userAgent?: string; } ): Promise<string> { return this.log( userId, userRole, MediaAction.FILE_UPLOAD, MediaResourceType.FILE, fileId, { fileName, fileSize, mimeType }, {}, { ...context, success, errorMessage } ); } /** * 파일 다운로드 감사 로그 */ async logFileDownload( userId: string, userRole: string, fileId: string, fileName: string, context?: { sessionId?: string; ipAddress?: string; userAgent?: string; } ): Promise<string> { return this.log( userId, userRole, MediaAction.FILE_DOWNLOAD, MediaResourceType.FILE, fileId, { fileName }, {}, context ); } /** * 권한 변경 감사 로그 */ async logPermissionChange( granterId: string, granterRole: string, targetUserId: string, resourceId: string, resourceType: MediaResourceType, permissionType: string, action: | MediaAction.PERMISSION_GRANT | MediaAction.PERMISSION_REVOKE | MediaAction.PERMISSION_UPDATE, beforeState?: Record<string, unknown>, afterState?: Record<string, unknown> ): Promise<string> { return this.log( granterId, granterRole, action, resourceType, resourceId, { targetUserId, permissionType }, { beforeState, afterState } ); } /** * 서명된 URL 생성 감사 로그 */ async logSignedUrlGeneration( userId: string, userRole: string, fileId: string, duration: number, permissions: string[], context?: { sessionId?: string; ipAddress?: string; userAgent?: string; } ): Promise<string> { return this.log( userId, userRole, MediaAction.SIGNED_URL_GENERATE, MediaResourceType.SIGNED_URL, fileId, { signedUrlDuration: duration }, { additionalData: { permissions } }, context ); } /** * 배치 작업 감사 로그 */ async logBatchOperation( userId: string, userRole: string, action: | MediaAction.BATCH_UPLOAD | MediaAction.BATCH_DELETE | MediaAction.BATCH_PROCESS, batchId: string, batchSize: number, success: boolean = true, processingOptions?: Record<string, unknown>, errorMessage?: string ): Promise<string> { return this.log( userId, userRole, action, MediaResourceType.SYSTEM, batchId, { batchSize, processingOptions }, {}, { success, errorMessage } ); } /** * 감사 로그 조회 */ async queryLogs(query: AuditLogQuery): Promise<{ logs: MediaAuditLog[]; total: number; hasMore: boolean; }> { let filteredLogs = Array.from(this.logs.values()); // 필터 적용 if (query.userId) { filteredLogs = filteredLogs.filter((log) => log.userId === query.userId); } if (query.action && query.action.length > 0) { filteredLogs = filteredLogs.filter((log) => query.action!.includes(log.action) ); } if (query.resourceType && query.resourceType.length > 0) { filteredLogs = filteredLogs.filter((log) => query.resourceType!.includes(log.resourceType) ); } if (query.resourceId) { filteredLogs = filteredLogs.filter( (log) => log.resourceId === query.resourceId ); } if (query.startDate) { filteredLogs = filteredLogs.filter( (log) => log.timestamp >= query.startDate! ); } if (query.endDate) { filteredLogs = filteredLogs.filter( (log) => log.timestamp <= query.endDate! ); } if (query.success !== undefined) { filteredLogs = filteredLogs.filter( (log) => log.success === query.success ); } if (query.ipAddress) { filteredLogs = filteredLogs.filter( (log) => log.ipAddress === query.ipAddress ); } // 정렬 const sortBy = query.sortBy || "timestamp"; const sortOrder = query.sortOrder || "desc"; filteredLogs.sort((a, b) => { let comparison = 0; switch (sortBy) { case "timestamp": comparison = a.timestamp.getTime() - b.timestamp.getTime(); break; case "action": comparison = a.action.localeCompare(b.action); break; case "userId": comparison = a.userId.localeCompare(b.userId); break; } return sortOrder === "desc" ? -comparison : comparison; }); // 페이지네이션 const total = filteredLogs.length; const offset = query.offset || 0; const limit = query.limit || 50; const paginatedLogs = filteredLogs.slice(offset, offset + limit); const hasMore = offset + limit < total; return { logs: paginatedLogs, total, hasMore, }; } /** * 감사 로그 통계 */ async getStats(startDate?: Date, endDate?: Date): Promise<AuditLogStats> { let logs = Array.from(this.logs.values()); if (startDate) { logs = logs.filter((log) => log.timestamp >= startDate); } if (endDate) { logs = logs.filter((log) => log.timestamp <= endDate); } const actionCounts = {} as Record<MediaAction, number>; const resourceTypeCounts = {} as Record<MediaResourceType, number>; const userCounts = new Map<string, number>(); let successfulLogs = 0; for (const log of logs) { // 액션 카운트 actionCounts[log.action] = (actionCounts[log.action] || 0) + 1; // 리소스 타입 카운트 resourceTypeCounts[log.resourceType] = (resourceTypeCounts[log.resourceType] || 0) + 1; // 사용자 카운트 userCounts.set(log.userId, (userCounts.get(log.userId) || 0) + 1); // 성공률 계산 if (log.success) { successfulLogs++; } } // 상위 사용자 const topUsers = Array.from(userCounts.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .map(([userId, count]) => ({ userId, count })); // 최근 활동 const recentActivity = logs .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) .slice(0, 20); return { totalLogs: logs.length, actionCounts, resourceTypeCounts, successRate: logs.length > 0 ? successfulLogs / logs.length : 0, topUsers, recentActivity, }; } /** * 특정 리소스의 감사 로그 조회 */ async getResourceLogs( resourceType: MediaResourceType, resourceId: string ): Promise<MediaAuditLog[]> { return Array.from(this.logs.values()) .filter( (log) => log.resourceType === resourceType && log.resourceId === resourceId ) .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); } /** * 사용자별 감사 로그 조회 */ async getUserLogs( userId: string, limit: number = 100 ): Promise<MediaAuditLog[]> { return Array.from(this.logs.values()) .filter((log) => log.userId === userId) .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) .slice(0, limit); } /** * 로그 버퍼 플러시 (데이터베이스 저장) */ private async flushLogs(): Promise<void> { if (this.logBuffer.length === 0) return; try { // 실제 구현에서는 데이터베이스에 저장 console.log(`Flushing ${this.logBuffer.length} audit logs to database`); // 버퍼 초기화 this.logBuffer = []; } catch (error) { console.error("Failed to flush audit logs:", error); } } /** * 자동 플러시 시작 */ private startAutoFlush(): void { this.flushTimer = setInterval(() => { this.flushLogs(); }, this.flushInterval); } /** * 자동 플러시 중지 */ stopAutoFlush(): void { if (this.flushTimer) { clearInterval(this.flushTimer); this.flushTimer = null; } } /** * 정리 (메모리 정리) */ async cleanup(): Promise<void> { this.stopAutoFlush(); await this.flushLogs(); this.logs.clear(); } } // 싱글톤 인스턴스 let auditLoggerInstance: MediaAuditLogger | null = null; export function getMediaAuditLogger(): MediaAuditLogger { if (!auditLoggerInstance) { auditLoggerInstance = new MediaAuditLogger(); } return auditLoggerInstance; } // 편의 함수들 export async function logMediaOperation( userId: string, userRole: string, action: MediaAction, resourceType: MediaResourceType, resourceId: string, details?: MediaActionDetails, context?: { sessionId?: string; ipAddress?: string; userAgent?: string; success?: boolean; errorMessage?: string; } ): Promise<string> { const logger = getMediaAuditLogger(); return logger.log( userId, userRole, action, resourceType, resourceId, details, {}, context ); }