@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
592 lines (528 loc) • 14 kB
text/typescript
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
);
}