ERP-node/backend-node/src/services/auditLogService.ts

297 lines
8.0 KiB
TypeScript
Raw Normal View History

import { query, pool } from "../database/db";
import logger from "../utils/logger";
export type AuditAction =
| "CREATE"
| "UPDATE"
| "DELETE"
| "COPY"
| "LOGIN"
| "STATUS_CHANGE"
| "BATCH_CREATE"
| "BATCH_UPDATE"
| "BATCH_DELETE";
export type AuditResourceType =
| "MENU"
| "SCREEN"
| "SCREEN_LAYOUT"
| "FLOW"
| "FLOW_STEP"
| "USER"
| "ROLE"
| "PERMISSION"
| "COMPANY"
| "CODE_CATEGORY"
| "CODE"
| "DATA"
| "TABLE"
| "NUMBERING_RULE"
| "BATCH";
export interface AuditLogParams {
companyCode: string;
userId: string;
userName?: string;
action: AuditAction;
resourceType: AuditResourceType;
resourceId?: string;
resourceName?: string;
tableName?: string;
summary?: string;
changes?: {
before?: Record<string, any>;
after?: Record<string, any>;
fields?: string[];
};
ipAddress?: string;
requestPath?: string;
}
export interface AuditLogEntry {
id: number;
company_code: string;
user_id: string;
user_name: string | null;
action: string;
resource_type: string;
resource_id: string | null;
resource_name: string | null;
table_name: string | null;
summary: string | null;
changes: any;
ip_address: string | null;
request_path: string | null;
created_at: string;
}
export interface AuditLogFilters {
companyCode?: string;
userId?: string;
resourceType?: string;
action?: string;
tableName?: string;
dateFrom?: string;
dateTo?: string;
search?: string;
page?: number;
limit?: number;
}
export interface AuditLogStats {
dailyCounts: Array<{ date: string; count: number }>;
resourceTypeCounts: Array<{ resource_type: string; count: number }>;
actionCounts: Array<{ action: string; count: number }>;
topUsers: Array<{ user_id: string; user_name: string; count: number }>;
}
class AuditLogService {
/**
* 1 (fire-and-forget)
*
*/
async log(params: AuditLogParams): Promise<void> {
try {
await query(
`INSERT INTO system_audit_log
(company_code, user_id, user_name, action, resource_type,
resource_id, resource_name, table_name, summary, changes,
ip_address, request_path)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
[
params.companyCode,
params.userId,
params.userName || null,
params.action,
params.resourceType,
params.resourceId || null,
params.resourceName || null,
params.tableName || null,
params.summary || null,
params.changes ? JSON.stringify(params.changes) : null,
params.ipAddress || null,
params.requestPath || null,
]
);
} catch (error) {
logger.error("감사 로그 기록 실패 (무시됨)", { error, params });
}
}
/**
* ()
*/
async logBatch(entries: AuditLogParams[]): Promise<void> {
if (entries.length === 0) return;
try {
const values = entries
.map(
(_, i) =>
`($${i * 12 + 1}, $${i * 12 + 2}, $${i * 12 + 3}, $${i * 12 + 4}, $${i * 12 + 5}, $${i * 12 + 6}, $${i * 12 + 7}, $${i * 12 + 8}, $${i * 12 + 9}, $${i * 12 + 10}, $${i * 12 + 11}, $${i * 12 + 12})`
)
.join(", ");
const params = entries.flatMap((e) => [
e.companyCode,
e.userId,
e.userName || null,
e.action,
e.resourceType,
e.resourceId || null,
e.resourceName || null,
e.tableName || null,
e.summary || null,
e.changes ? JSON.stringify(e.changes) : null,
e.ipAddress || null,
e.requestPath || null,
]);
await query(
`INSERT INTO system_audit_log
(company_code, user_id, user_name, action, resource_type,
resource_id, resource_name, table_name, summary, changes,
ip_address, request_path)
VALUES ${values}`,
params
);
} catch (error) {
logger.error("감사 로그 배치 기록 실패 (무시됨)", { error });
}
}
/**
* (, )
*/
async queryLogs(
filters: AuditLogFilters,
isSuperAdmin: boolean = false
): Promise<{ data: AuditLogEntry[]; total: number }> {
const conditions: string[] = [];
const params: any[] = [];
let paramIndex = 1;
if (!isSuperAdmin && filters.companyCode) {
conditions.push(`company_code = $${paramIndex++}`);
params.push(filters.companyCode);
} else if (isSuperAdmin && filters.companyCode) {
conditions.push(`company_code = $${paramIndex++}`);
params.push(filters.companyCode);
}
if (filters.userId) {
conditions.push(`user_id = $${paramIndex++}`);
params.push(filters.userId);
}
if (filters.resourceType) {
conditions.push(`resource_type = $${paramIndex++}`);
params.push(filters.resourceType);
}
if (filters.action) {
conditions.push(`action = $${paramIndex++}`);
params.push(filters.action);
}
if (filters.tableName) {
conditions.push(`table_name = $${paramIndex++}`);
params.push(filters.tableName);
}
if (filters.dateFrom) {
conditions.push(`created_at >= $${paramIndex++}::timestamptz`);
params.push(filters.dateFrom);
}
if (filters.dateTo) {
conditions.push(`created_at <= $${paramIndex++}::timestamptz`);
params.push(filters.dateTo);
}
if (filters.search) {
conditions.push(
`(summary ILIKE $${paramIndex} OR resource_name ILIKE $${paramIndex} OR user_name ILIKE $${paramIndex})`
);
params.push(`%${filters.search}%`);
paramIndex++;
}
const whereClause =
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
const page = filters.page || 1;
const limit = filters.limit || 50;
const offset = (page - 1) * limit;
const countResult = await query<{ count: string }>(
`SELECT COUNT(*) as count FROM system_audit_log ${whereClause}`,
params
);
const total = parseInt(countResult[0].count, 10);
const data = await query<AuditLogEntry>(
`SELECT * FROM system_audit_log ${whereClause}
ORDER BY created_at DESC
LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
[...params, limit, offset]
);
return { data, total };
}
/**
*
*/
async getStats(
companyCode?: string,
days: number = 30
): Promise<AuditLogStats> {
const companyFilter = companyCode
? "AND company_code = $1"
: "";
const params = companyCode ? [companyCode] : [];
const dailyCounts = await query<{ date: string; count: number }>(
`SELECT DATE(created_at) as date, COUNT(*)::int as count
FROM system_audit_log
WHERE created_at >= NOW() - INTERVAL '${days} days' ${companyFilter}
GROUP BY DATE(created_at)
ORDER BY date DESC`,
params
);
const resourceTypeCounts = await query<{
resource_type: string;
count: number;
}>(
`SELECT resource_type, COUNT(*)::int as count
FROM system_audit_log
WHERE created_at >= NOW() - INTERVAL '${days} days' ${companyFilter}
GROUP BY resource_type
ORDER BY count DESC`,
params
);
const actionCounts = await query<{ action: string; count: number }>(
`SELECT action, COUNT(*)::int as count
FROM system_audit_log
WHERE created_at >= NOW() - INTERVAL '${days} days' ${companyFilter}
GROUP BY action
ORDER BY count DESC`,
params
);
const topUsers = await query<{
user_id: string;
user_name: string;
count: number;
}>(
`SELECT user_id, COALESCE(MAX(user_name), user_id) as user_name, COUNT(*)::int as count
FROM system_audit_log
WHERE created_at >= NOW() - INTERVAL '${days} days' ${companyFilter}
GROUP BY user_id
ORDER BY count DESC
LIMIT 10`,
params
);
return { dailyCounts, resourceTypeCounts, actionCounts, topUsers };
}
}
export const auditLogService = new AuditLogService();