From fd90e3d761977e494b5bec21cc033c142f593874 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 12 Mar 2026 02:13:15 +0900 Subject: [PATCH] feat: add audit logging for node flow operations - Integrated audit logging for create, update, and delete actions in the node flows API. - Enhanced the logging service to capture relevant details such as user information, action type, resource details, and IP address. - Updated the audit log service to include NODE_FLOW as a resource type. - Improved the overall traceability of node flow changes within the system. Made-with: Cursor --- .../src/routes/dataflow/node-flows.ts | 68 ++++++++++++++++++- backend-node/src/services/auditLogService.ts | 3 +- frontend/app/(main)/admin/audit-log/page.tsx | 1 + 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/backend-node/src/routes/dataflow/node-flows.ts b/backend-node/src/routes/dataflow/node-flows.ts index 177b4304..30fffd7b 100644 --- a/backend-node/src/routes/dataflow/node-flows.ts +++ b/backend-node/src/routes/dataflow/node-flows.ts @@ -8,6 +8,7 @@ import { logger } from "../../utils/logger"; import { NodeFlowExecutionService } from "../../services/nodeFlowExecutionService"; import { AuthenticatedRequest } from "../../types/auth"; import { authenticateToken } from "../../middleware/authMiddleware"; +import { auditLogService, getClientIp } from "../../services/auditLogService"; const router = Router(); @@ -124,6 +125,21 @@ router.post("/", async (req: AuthenticatedRequest, res: Response) => { `플로우 저장 성공: ${result.flowId} (회사: ${userCompanyCode})` ); + auditLogService.log({ + companyCode: userCompanyCode, + userId: req.user?.userId || "", + userName: req.user?.userName, + action: "CREATE", + resourceType: "NODE_FLOW", + resourceId: String(result.flowId), + resourceName: flowName, + tableName: "node_flows", + summary: `노드 플로우 "${flowName}" 생성`, + changes: { after: { flowName, flowDescription } }, + ipAddress: getClientIp(req as any), + requestPath: req.originalUrl, + }); + return res.json({ success: true, message: "플로우가 저장되었습니다.", @@ -143,7 +159,7 @@ router.post("/", async (req: AuthenticatedRequest, res: Response) => { /** * 플로우 수정 */ -router.put("/", async (req: Request, res: Response) => { +router.put("/", async (req: AuthenticatedRequest, res: Response) => { try { const { flowId, flowName, flowDescription, flowData } = req.body; @@ -154,6 +170,11 @@ router.put("/", async (req: Request, res: Response) => { }); } + const oldFlow = await queryOne( + `SELECT flow_name, flow_description FROM node_flows WHERE flow_id = $1`, + [flowId] + ); + await query( ` UPDATE node_flows @@ -168,6 +189,25 @@ router.put("/", async (req: Request, res: Response) => { logger.info(`플로우 수정 성공: ${flowId}`); + const userCompanyCode = req.user?.companyCode || "*"; + auditLogService.log({ + companyCode: userCompanyCode, + userId: req.user?.userId || "", + userName: req.user?.userName, + action: "UPDATE", + resourceType: "NODE_FLOW", + resourceId: String(flowId), + resourceName: flowName, + tableName: "node_flows", + summary: `노드 플로우 "${flowName}" 수정`, + changes: { + before: oldFlow ? { flowName: (oldFlow as any).flow_name, flowDescription: (oldFlow as any).flow_description } : undefined, + after: { flowName, flowDescription }, + }, + ipAddress: getClientIp(req as any), + requestPath: req.originalUrl, + }); + return res.json({ success: true, message: "플로우가 수정되었습니다.", @@ -187,10 +227,15 @@ router.put("/", async (req: Request, res: Response) => { /** * 플로우 삭제 */ -router.delete("/:flowId", async (req: Request, res: Response) => { +router.delete("/:flowId", async (req: AuthenticatedRequest, res: Response) => { try { const { flowId } = req.params; + const oldFlow = await queryOne( + `SELECT flow_name, flow_description, company_code FROM node_flows WHERE flow_id = $1`, + [flowId] + ); + await query( ` DELETE FROM node_flows @@ -201,6 +246,25 @@ router.delete("/:flowId", async (req: Request, res: Response) => { logger.info(`플로우 삭제 성공: ${flowId}`); + const userCompanyCode = req.user?.companyCode || "*"; + const flowName = (oldFlow as any)?.flow_name || `ID:${flowId}`; + auditLogService.log({ + companyCode: userCompanyCode, + userId: req.user?.userId || "", + userName: req.user?.userName, + action: "DELETE", + resourceType: "NODE_FLOW", + resourceId: String(flowId), + resourceName: flowName, + tableName: "node_flows", + summary: `노드 플로우 "${flowName}" 삭제`, + changes: { + before: oldFlow ? { flowName: (oldFlow as any).flow_name, flowDescription: (oldFlow as any).flow_description } : undefined, + }, + ipAddress: getClientIp(req as any), + requestPath: req.originalUrl, + }); + return res.json({ success: true, message: "플로우가 삭제되었습니다.", diff --git a/backend-node/src/services/auditLogService.ts b/backend-node/src/services/auditLogService.ts index bc77be49..9ac3e35e 100644 --- a/backend-node/src/services/auditLogService.ts +++ b/backend-node/src/services/auditLogService.ts @@ -41,7 +41,8 @@ export type AuditResourceType = | "DATA" | "TABLE" | "NUMBERING_RULE" - | "BATCH"; + | "BATCH" + | "NODE_FLOW"; export interface AuditLogParams { companyCode: string; diff --git a/frontend/app/(main)/admin/audit-log/page.tsx b/frontend/app/(main)/admin/audit-log/page.tsx index 74cb550b..8fbe5e95 100644 --- a/frontend/app/(main)/admin/audit-log/page.tsx +++ b/frontend/app/(main)/admin/audit-log/page.tsx @@ -74,6 +74,7 @@ const RESOURCE_TYPE_CONFIG: Record< SCREEN_LAYOUT: { label: "레이아웃", icon: Monitor, color: "bg-purple-100 text-purple-700" }, FLOW: { label: "플로우", icon: GitBranch, color: "bg-emerald-100 text-emerald-700" }, FLOW_STEP: { label: "플로우 스텝", icon: GitBranch, color: "bg-emerald-100 text-emerald-700" }, + NODE_FLOW: { label: "플로우 제어", icon: GitBranch, color: "bg-teal-100 text-teal-700" }, USER: { label: "사용자", icon: User, color: "bg-amber-100 text-orange-700" }, ROLE: { label: "권한", icon: Shield, color: "bg-destructive/10 text-destructive" }, PERMISSION: { label: "권한", icon: Shield, color: "bg-destructive/10 text-destructive" },