import { Pool, QueryResult } from "pg"; import { getPool } from "../database/db"; import logger from "../utils/logger"; import { ExternalRestApiConnection, ExternalRestApiConnectionFilter, RestApiTestRequest, RestApiTestResult, AuthType, } from "../types/externalRestApiTypes"; import { ApiResponse } from "../types/common"; import crypto from "crypto"; const pool = getPool(); // 암호화 설정 const ENCRYPTION_KEY = process.env.DB_PASSWORD_SECRET || "default-secret-key-change-in-production"; const ALGORITHM = "aes-256-gcm"; export class ExternalRestApiConnectionService { /** * REST API 연결 목록 조회 */ static async getConnections( filter: ExternalRestApiConnectionFilter = {} ): Promise> { try { let query = ` SELECT id, connection_name, description, base_url, default_headers, auth_type, auth_config, timeout, retry_count, retry_delay, company_code, is_active, created_date, created_by, updated_date, updated_by, last_test_date, last_test_result, last_test_message FROM external_rest_api_connections WHERE 1=1 `; const params: any[] = []; let paramIndex = 1; // 회사 코드 필터 if (filter.company_code) { query += ` AND company_code = $${paramIndex}`; params.push(filter.company_code); paramIndex++; } // 활성 상태 필터 if (filter.is_active) { query += ` AND is_active = $${paramIndex}`; params.push(filter.is_active); paramIndex++; } // 인증 타입 필터 if (filter.auth_type) { query += ` AND auth_type = $${paramIndex}`; params.push(filter.auth_type); paramIndex++; } // 검색어 필터 (연결명, 설명, URL) if (filter.search) { query += ` AND ( connection_name ILIKE $${paramIndex} OR description ILIKE $${paramIndex} OR base_url ILIKE $${paramIndex} )`; params.push(`%${filter.search}%`); paramIndex++; } query += ` ORDER BY created_date DESC`; const result: QueryResult = await pool.query(query, params); // 민감 정보 복호화 const connections = result.rows.map((row: any) => ({ ...row, auth_config: row.auth_config ? this.decryptSensitiveData(row.auth_config) : null, })); return { success: true, data: connections, message: `${connections.length}개의 연결을 조회했습니다.`, }; } catch (error) { logger.error("REST API 연결 목록 조회 오류:", error); return { success: false, message: "연결 목록 조회에 실패했습니다.", error: { code: "FETCH_ERROR", details: error instanceof Error ? error.message : "알 수 없는 오류", }, }; } } /** * REST API 연결 상세 조회 */ static async getConnectionById( id: number ): Promise> { try { const query = ` SELECT id, connection_name, description, base_url, default_headers, auth_type, auth_config, timeout, retry_count, retry_delay, company_code, is_active, created_date, created_by, updated_date, updated_by, last_test_date, last_test_result, last_test_message FROM external_rest_api_connections WHERE id = $1 `; const result: QueryResult = await pool.query(query, [id]); if (result.rows.length === 0) { return { success: false, message: "연결을 찾을 수 없습니다.", }; } const connection = result.rows[0]; connection.auth_config = connection.auth_config ? this.decryptSensitiveData(connection.auth_config) : null; return { success: true, data: connection, message: "연결을 조회했습니다.", }; } catch (error) { logger.error("REST API 연결 상세 조회 오류:", error); return { success: false, message: "연결 조회에 실패했습니다.", error: { code: "FETCH_ERROR", details: error instanceof Error ? error.message : "알 수 없는 오류", }, }; } } /** * REST API 연결 생성 */ static async createConnection( data: ExternalRestApiConnection ): Promise> { try { // 유효성 검증 this.validateConnectionData(data); // 민감 정보 암호화 const encryptedAuthConfig = data.auth_config ? this.encryptSensitiveData(data.auth_config) : null; const query = ` INSERT INTO external_rest_api_connections ( connection_name, description, base_url, default_headers, auth_type, auth_config, timeout, retry_count, retry_delay, company_code, is_active, created_by ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING * `; const params = [ data.connection_name, data.description || null, data.base_url, JSON.stringify(data.default_headers || {}), data.auth_type, encryptedAuthConfig ? JSON.stringify(encryptedAuthConfig) : null, data.timeout || 30000, data.retry_count || 0, data.retry_delay || 1000, data.company_code || "*", data.is_active || "Y", data.created_by || "system", ]; const result: QueryResult = await pool.query(query, params); logger.info(`REST API 연결 생성 성공: ${data.connection_name}`); return { success: true, data: result.rows[0], message: "연결이 생성되었습니다.", }; } catch (error: any) { logger.error("REST API 연결 생성 오류:", error); // 중복 키 오류 처리 if (error.code === "23505") { return { success: false, message: "이미 존재하는 연결명입니다.", }; } return { success: false, message: "연결 생성에 실패했습니다.", error: { code: "CREATE_ERROR", details: error instanceof Error ? error.message : "알 수 없는 오류", }, }; } } /** * REST API 연결 수정 */ static async updateConnection( id: number, data: Partial ): Promise> { try { // 기존 연결 확인 const existing = await this.getConnectionById(id); if (!existing.success) { return existing; } // 민감 정보 암호화 const encryptedAuthConfig = data.auth_config ? this.encryptSensitiveData(data.auth_config) : undefined; const updateFields: string[] = []; const params: any[] = []; let paramIndex = 1; if (data.connection_name !== undefined) { updateFields.push(`connection_name = $${paramIndex}`); params.push(data.connection_name); paramIndex++; } if (data.description !== undefined) { updateFields.push(`description = $${paramIndex}`); params.push(data.description); paramIndex++; } if (data.base_url !== undefined) { updateFields.push(`base_url = $${paramIndex}`); params.push(data.base_url); paramIndex++; } if (data.default_headers !== undefined) { updateFields.push(`default_headers = $${paramIndex}`); params.push(JSON.stringify(data.default_headers)); paramIndex++; } if (data.auth_type !== undefined) { updateFields.push(`auth_type = $${paramIndex}`); params.push(data.auth_type); paramIndex++; } if (encryptedAuthConfig !== undefined) { updateFields.push(`auth_config = $${paramIndex}`); params.push(JSON.stringify(encryptedAuthConfig)); paramIndex++; } if (data.timeout !== undefined) { updateFields.push(`timeout = $${paramIndex}`); params.push(data.timeout); paramIndex++; } if (data.retry_count !== undefined) { updateFields.push(`retry_count = $${paramIndex}`); params.push(data.retry_count); paramIndex++; } if (data.retry_delay !== undefined) { updateFields.push(`retry_delay = $${paramIndex}`); params.push(data.retry_delay); paramIndex++; } if (data.is_active !== undefined) { updateFields.push(`is_active = $${paramIndex}`); params.push(data.is_active); paramIndex++; } if (data.updated_by !== undefined) { updateFields.push(`updated_by = $${paramIndex}`); params.push(data.updated_by); paramIndex++; } updateFields.push(`updated_date = NOW()`); params.push(id); const query = ` UPDATE external_rest_api_connections SET ${updateFields.join(", ")} WHERE id = $${paramIndex} RETURNING * `; const result: QueryResult = await pool.query(query, params); logger.info(`REST API 연결 수정 성공: ID ${id}`); return { success: true, data: result.rows[0], message: "연결이 수정되었습니다.", }; } catch (error: any) { logger.error("REST API 연결 수정 오류:", error); if (error.code === "23505") { return { success: false, message: "이미 존재하는 연결명입니다.", }; } return { success: false, message: "연결 수정에 실패했습니다.", error: { code: "UPDATE_ERROR", details: error instanceof Error ? error.message : "알 수 없는 오류", }, }; } } /** * REST API 연결 삭제 */ static async deleteConnection(id: number): Promise> { try { const query = ` DELETE FROM external_rest_api_connections WHERE id = $1 RETURNING connection_name `; const result: QueryResult = await pool.query(query, [id]); if (result.rows.length === 0) { return { success: false, message: "연결을 찾을 수 없습니다.", }; } logger.info(`REST API 연결 삭제 성공: ${result.rows[0].connection_name}`); return { success: true, message: "연결이 삭제되었습니다.", }; } catch (error) { logger.error("REST API 연결 삭제 오류:", error); return { success: false, message: "연결 삭제에 실패했습니다.", error: { code: "DELETE_ERROR", details: error instanceof Error ? error.message : "알 수 없는 오류", }, }; } } /** * REST API 연결 테스트 (테스트 요청 데이터 기반) */ static async testConnection( testRequest: RestApiTestRequest ): Promise { const startTime = Date.now(); try { // 헤더 구성 const headers = { ...testRequest.headers }; // 인증 헤더 추가 if ( testRequest.auth_type === "bearer" && testRequest.auth_config?.token ) { headers["Authorization"] = `Bearer ${testRequest.auth_config.token}`; } else if (testRequest.auth_type === "basic" && testRequest.auth_config) { const credentials = Buffer.from( `${testRequest.auth_config.username}:${testRequest.auth_config.password}` ).toString("base64"); headers["Authorization"] = `Basic ${credentials}`; } else if ( testRequest.auth_type === "api-key" && testRequest.auth_config ) { if (testRequest.auth_config.keyLocation === "header") { headers[testRequest.auth_config.keyName] = testRequest.auth_config.keyValue; } } // URL 구성 let url = testRequest.base_url; if (testRequest.endpoint) { url = testRequest.endpoint.startsWith("/") ? `${testRequest.base_url}${testRequest.endpoint}` : `${testRequest.base_url}/${testRequest.endpoint}`; } // API Key가 쿼리에 있는 경우 if ( testRequest.auth_type === "api-key" && testRequest.auth_config?.keyLocation === "query" && testRequest.auth_config?.keyName && testRequest.auth_config?.keyValue ) { const separator = url.includes("?") ? "&" : "?"; url = `${url}${separator}${testRequest.auth_config.keyName}=${testRequest.auth_config.keyValue}`; } logger.info( `REST API 연결 테스트: ${testRequest.method || "GET"} ${url}` ); // HTTP 요청 실행 const response = await fetch(url, { method: testRequest.method || "GET", headers, signal: AbortSignal.timeout(testRequest.timeout || 30000), }); const responseTime = Date.now() - startTime; let responseData = null; try { responseData = await response.json(); } catch { // JSON 파싱 실패는 무시 (텍스트 응답일 수 있음) } return { success: response.ok, message: response.ok ? "연결 성공" : `연결 실패 (${response.status} ${response.statusText})`, response_time: responseTime, status_code: response.status, response_data: responseData, }; } catch (error) { const responseTime = Date.now() - startTime; logger.error("REST API 연결 테스트 오류:", error); return { success: false, message: "연결 실패", response_time: responseTime, error_details: error instanceof Error ? error.message : "알 수 없는 오류", }; } } /** * REST API 연결 테스트 (ID 기반) */ static async testConnectionById( id: number, endpoint?: string ): Promise { try { const connectionResult = await this.getConnectionById(id); if (!connectionResult.success || !connectionResult.data) { return { success: false, message: "연결을 찾을 수 없습니다.", }; } const connection = connectionResult.data; const testRequest: RestApiTestRequest = { id: connection.id, base_url: connection.base_url, endpoint, headers: connection.default_headers, auth_type: connection.auth_type, auth_config: connection.auth_config, timeout: connection.timeout, }; const result = await this.testConnection(testRequest); // 테스트 결과 저장 await pool.query( ` UPDATE external_rest_api_connections SET last_test_date = NOW(), last_test_result = $1, last_test_message = $2 WHERE id = $3 `, [result.success ? "Y" : "N", result.message, id] ); return result; } catch (error) { logger.error("REST API 연결 테스트 (ID) 오류:", error); return { success: false, message: "연결 테스트에 실패했습니다.", error_details: error instanceof Error ? error.message : "알 수 없는 오류", }; } } /** * 민감 정보 암호화 */ private static encryptSensitiveData(authConfig: any): any { if (!authConfig) return null; const encrypted = { ...authConfig }; // 암호화 대상 필드 if (encrypted.keyValue) { encrypted.keyValue = this.encrypt(encrypted.keyValue); } if (encrypted.token) { encrypted.token = this.encrypt(encrypted.token); } if (encrypted.password) { encrypted.password = this.encrypt(encrypted.password); } if (encrypted.clientSecret) { encrypted.clientSecret = this.encrypt(encrypted.clientSecret); } return encrypted; } /** * 민감 정보 복호화 */ private static decryptSensitiveData(authConfig: any): any { if (!authConfig) return null; const decrypted = { ...authConfig }; // 복호화 대상 필드 try { if (decrypted.keyValue) { decrypted.keyValue = this.decrypt(decrypted.keyValue); } if (decrypted.token) { decrypted.token = this.decrypt(decrypted.token); } if (decrypted.password) { decrypted.password = this.decrypt(decrypted.password); } if (decrypted.clientSecret) { decrypted.clientSecret = this.decrypt(decrypted.clientSecret); } } catch (error) { logger.warn("민감 정보 복호화 실패 (암호화되지 않은 데이터일 수 있음)"); } return decrypted; } /** * 암호화 헬퍼 */ private static encrypt(text: string): string { const iv = crypto.randomBytes(16); const key = crypto.scryptSync(ENCRYPTION_KEY, "salt", 32); const cipher = crypto.createCipheriv(ALGORITHM, key, iv); let encrypted = cipher.update(text, "utf8", "hex"); encrypted += cipher.final("hex"); const authTag = cipher.getAuthTag(); return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`; } /** * 복호화 헬퍼 */ private static decrypt(text: string): string { const parts = text.split(":"); if (parts.length !== 3) { // 암호화되지 않은 데이터 return text; } const iv = Buffer.from(parts[0], "hex"); const authTag = Buffer.from(parts[1], "hex"); const encryptedText = parts[2]; const key = crypto.scryptSync(ENCRYPTION_KEY, "salt", 32); const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); decipher.setAuthTag(authTag); let decrypted = decipher.update(encryptedText, "hex", "utf8"); decrypted += decipher.final("utf8"); return decrypted; } /** * 연결 데이터 유효성 검증 */ private static validateConnectionData(data: ExternalRestApiConnection): void { if (!data.connection_name || data.connection_name.trim() === "") { throw new Error("연결명은 필수입니다."); } if (!data.base_url || data.base_url.trim() === "") { throw new Error("기본 URL은 필수입니다."); } // URL 형식 검증 try { new URL(data.base_url); } catch { throw new Error("올바른 URL 형식이 아닙니다."); } // 인증 타입 검증 const validAuthTypes: AuthType[] = [ "none", "api-key", "bearer", "basic", "oauth2", ]; if (!validAuthTypes.includes(data.auth_type)) { throw new Error("올바르지 않은 인증 타입입니다."); } } }