rest api 관리 구현
This commit is contained in:
@@ -35,6 +35,7 @@ import mailReceiveBasicRoutes from "./routes/mailReceiveBasicRoutes";
|
||||
import dataRoutes from "./routes/dataRoutes";
|
||||
import testButtonDataflowRoutes from "./routes/testButtonDataflowRoutes";
|
||||
import externalDbConnectionRoutes from "./routes/externalDbConnectionRoutes";
|
||||
import externalRestApiConnectionRoutes from "./routes/externalRestApiConnectionRoutes";
|
||||
import multiConnectionRoutes from "./routes/multiConnectionRoutes";
|
||||
import screenFileRoutes from "./routes/screenFileRoutes";
|
||||
//import dbTypeCategoryRoutes from "./routes/dbTypeCategoryRoutes";
|
||||
@@ -190,6 +191,7 @@ app.use("/api/screen", screenStandardRoutes);
|
||||
app.use("/api/data", dataRoutes);
|
||||
app.use("/api/test-button-dataflow", testButtonDataflowRoutes);
|
||||
app.use("/api/external-db-connections", externalDbConnectionRoutes);
|
||||
app.use("/api/external-rest-api-connections", externalRestApiConnectionRoutes);
|
||||
app.use("/api/multi-connection", multiConnectionRoutes);
|
||||
app.use("/api/screen-files", screenFileRoutes);
|
||||
app.use("/api/batch-configs", batchRoutes);
|
||||
|
||||
252
backend-node/src/routes/externalRestApiConnectionRoutes.ts
Normal file
252
backend-node/src/routes/externalRestApiConnectionRoutes.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import {
|
||||
authenticateToken,
|
||||
AuthenticatedRequest,
|
||||
} from "../middleware/authMiddleware";
|
||||
import { ExternalRestApiConnectionService } from "../services/externalRestApiConnectionService";
|
||||
import {
|
||||
ExternalRestApiConnection,
|
||||
ExternalRestApiConnectionFilter,
|
||||
RestApiTestRequest,
|
||||
} from "../types/externalRestApiTypes";
|
||||
import logger from "../utils/logger";
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* GET /api/external-rest-api-connections
|
||||
* REST API 연결 목록 조회
|
||||
*/
|
||||
router.get(
|
||||
"/",
|
||||
authenticateToken,
|
||||
async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const filter: ExternalRestApiConnectionFilter = {
|
||||
search: req.query.search as string,
|
||||
auth_type: req.query.auth_type as string,
|
||||
is_active: req.query.is_active as string,
|
||||
company_code: req.query.company_code as string,
|
||||
};
|
||||
|
||||
const result =
|
||||
await ExternalRestApiConnectionService.getConnections(filter);
|
||||
|
||||
return res.status(result.success ? 200 : 400).json(result);
|
||||
} catch (error) {
|
||||
logger.error("REST API 연결 목록 조회 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "서버 내부 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /api/external-rest-api-connections/:id
|
||||
* REST API 연결 상세 조회
|
||||
*/
|
||||
router.get(
|
||||
"/:id",
|
||||
authenticateToken,
|
||||
async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
|
||||
if (isNaN(id)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "유효하지 않은 ID입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const result =
|
||||
await ExternalRestApiConnectionService.getConnectionById(id);
|
||||
|
||||
return res.status(result.success ? 200 : 404).json(result);
|
||||
} catch (error) {
|
||||
logger.error("REST API 연결 상세 조회 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "서버 내부 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* POST /api/external-rest-api-connections
|
||||
* REST API 연결 생성
|
||||
*/
|
||||
router.post(
|
||||
"/",
|
||||
authenticateToken,
|
||||
async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const data: ExternalRestApiConnection = {
|
||||
...req.body,
|
||||
created_by: req.user?.userId || "system",
|
||||
};
|
||||
|
||||
const result =
|
||||
await ExternalRestApiConnectionService.createConnection(data);
|
||||
|
||||
return res.status(result.success ? 201 : 400).json(result);
|
||||
} catch (error) {
|
||||
logger.error("REST API 연결 생성 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "서버 내부 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* PUT /api/external-rest-api-connections/:id
|
||||
* REST API 연결 수정
|
||||
*/
|
||||
router.put(
|
||||
"/:id",
|
||||
authenticateToken,
|
||||
async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
|
||||
if (isNaN(id)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "유효하지 않은 ID입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const data: Partial<ExternalRestApiConnection> = {
|
||||
...req.body,
|
||||
updated_by: req.user?.userId || "system",
|
||||
};
|
||||
|
||||
const result = await ExternalRestApiConnectionService.updateConnection(
|
||||
id,
|
||||
data
|
||||
);
|
||||
|
||||
return res.status(result.success ? 200 : 400).json(result);
|
||||
} catch (error) {
|
||||
logger.error("REST API 연결 수정 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "서버 내부 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* DELETE /api/external-rest-api-connections/:id
|
||||
* REST API 연결 삭제
|
||||
*/
|
||||
router.delete(
|
||||
"/:id",
|
||||
authenticateToken,
|
||||
async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
|
||||
if (isNaN(id)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "유효하지 않은 ID입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const result =
|
||||
await ExternalRestApiConnectionService.deleteConnection(id);
|
||||
|
||||
return res.status(result.success ? 200 : 404).json(result);
|
||||
} catch (error) {
|
||||
logger.error("REST API 연결 삭제 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "서버 내부 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* POST /api/external-rest-api-connections/test
|
||||
* REST API 연결 테스트 (테스트 데이터 기반)
|
||||
*/
|
||||
router.post(
|
||||
"/test",
|
||||
authenticateToken,
|
||||
async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const testRequest: RestApiTestRequest = req.body;
|
||||
|
||||
if (!testRequest.base_url) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "기본 URL은 필수입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const result =
|
||||
await ExternalRestApiConnectionService.testConnection(testRequest);
|
||||
|
||||
return res.status(200).json(result);
|
||||
} catch (error) {
|
||||
logger.error("REST API 연결 테스트 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "서버 내부 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* POST /api/external-rest-api-connections/:id/test
|
||||
* REST API 연결 테스트 (ID 기반)
|
||||
*/
|
||||
router.post(
|
||||
"/:id/test",
|
||||
authenticateToken,
|
||||
async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
|
||||
if (isNaN(id)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "유효하지 않은 ID입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const endpoint = req.body.endpoint as string | undefined;
|
||||
|
||||
const result = await ExternalRestApiConnectionService.testConnectionById(
|
||||
id,
|
||||
endpoint
|
||||
);
|
||||
|
||||
return res.status(200).json(result);
|
||||
} catch (error) {
|
||||
logger.error("REST API 연결 테스트 (ID) 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "서버 내부 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
669
backend-node/src/services/externalRestApiConnectionService.ts
Normal file
669
backend-node/src/services/externalRestApiConnectionService.ts
Normal file
@@ -0,0 +1,669 @@
|
||||
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<ApiResponse<ExternalRestApiConnection[]>> {
|
||||
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<any> = 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<ApiResponse<ExternalRestApiConnection>> {
|
||||
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<any> = 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<ApiResponse<ExternalRestApiConnection>> {
|
||||
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<any> = 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<ExternalRestApiConnection>
|
||||
): Promise<ApiResponse<ExternalRestApiConnection>> {
|
||||
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<any> = 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<ApiResponse<void>> {
|
||||
try {
|
||||
const query = `
|
||||
DELETE FROM external_rest_api_connections
|
||||
WHERE id = $1
|
||||
RETURNING connection_name
|
||||
`;
|
||||
|
||||
const result: QueryResult<any> = 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<RestApiTestResult> {
|
||||
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<RestApiTestResult> {
|
||||
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("올바르지 않은 인증 타입입니다.");
|
||||
}
|
||||
}
|
||||
}
|
||||
78
backend-node/src/types/externalRestApiTypes.ts
Normal file
78
backend-node/src/types/externalRestApiTypes.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
// 외부 REST API 연결 관리 타입 정의
|
||||
|
||||
export type AuthType = "none" | "api-key" | "bearer" | "basic" | "oauth2";
|
||||
|
||||
export interface ExternalRestApiConnection {
|
||||
id?: number;
|
||||
connection_name: string;
|
||||
description?: string;
|
||||
base_url: string;
|
||||
default_headers: Record<string, string>;
|
||||
auth_type: AuthType;
|
||||
auth_config?: {
|
||||
// API Key
|
||||
keyLocation?: "header" | "query";
|
||||
keyName?: string;
|
||||
keyValue?: string;
|
||||
|
||||
// Bearer Token
|
||||
token?: string;
|
||||
|
||||
// Basic Auth
|
||||
username?: string;
|
||||
password?: string;
|
||||
|
||||
// OAuth2
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
tokenUrl?: string;
|
||||
accessToken?: string;
|
||||
};
|
||||
timeout?: number;
|
||||
retry_count?: number;
|
||||
retry_delay?: number;
|
||||
company_code: string;
|
||||
is_active: string;
|
||||
created_date?: Date;
|
||||
created_by?: string;
|
||||
updated_date?: Date;
|
||||
updated_by?: string;
|
||||
last_test_date?: Date;
|
||||
last_test_result?: string;
|
||||
last_test_message?: string;
|
||||
}
|
||||
|
||||
export interface ExternalRestApiConnectionFilter {
|
||||
auth_type?: string;
|
||||
is_active?: string;
|
||||
company_code?: string;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export interface RestApiTestRequest {
|
||||
id?: number;
|
||||
base_url: string;
|
||||
endpoint?: string;
|
||||
method?: "GET" | "POST" | "PUT" | "DELETE";
|
||||
headers?: Record<string, string>;
|
||||
auth_type?: AuthType;
|
||||
auth_config?: any;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export interface RestApiTestResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
response_time?: number;
|
||||
status_code?: number;
|
||||
response_data?: any;
|
||||
error_details?: string;
|
||||
}
|
||||
|
||||
export const AUTH_TYPE_OPTIONS = [
|
||||
{ value: "none", label: "인증 없음" },
|
||||
{ value: "api-key", label: "API Key" },
|
||||
{ value: "bearer", label: "Bearer Token" },
|
||||
{ value: "basic", label: "Basic Auth" },
|
||||
{ value: "oauth2", label: "OAuth 2.0" },
|
||||
];
|
||||
Reference in New Issue
Block a user