카테고리 기능 구현
This commit is contained in:
@@ -1,3 +1,18 @@
|
||||
/**
|
||||
* 동적 데이터 서비스
|
||||
*
|
||||
* 주요 특징:
|
||||
* 1. 화이트리스트 제거 - 모든 테이블에 동적으로 접근 가능
|
||||
* 2. 블랙리스트 방식 - 시스템 중요 테이블만 접근 금지
|
||||
* 3. 자동 회사별 필터링 - company_code 컬럼 자동 감지 및 필터 적용
|
||||
* 4. SQL 인젝션 방지 - 정규식 기반 테이블명/컬럼명 검증
|
||||
*
|
||||
* 보안:
|
||||
* - 테이블명은 영문, 숫자, 언더스코어만 허용
|
||||
* - 시스템 테이블(pg_*, information_schema 등) 접근 금지
|
||||
* - company_code 컬럼이 있는 테이블은 자동으로 회사별 격리
|
||||
* - 최고 관리자(company_code = "*")만 전체 데이터 조회 가능
|
||||
*/
|
||||
import { query, queryOne } from "../database/db";
|
||||
|
||||
interface GetTableDataParams {
|
||||
@@ -17,65 +32,72 @@ interface ServiceResponse<T> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 안전한 테이블명 목록 (화이트리스트)
|
||||
* SQL 인젝션 방지를 위해 허용된 테이블만 접근 가능
|
||||
* 접근 금지 테이블 목록 (블랙리스트)
|
||||
* 시스템 중요 테이블 및 보안상 접근 금지할 테이블
|
||||
*/
|
||||
const ALLOWED_TABLES = [
|
||||
"company_mng",
|
||||
"user_info",
|
||||
"dept_info",
|
||||
"code_info",
|
||||
"code_category",
|
||||
"menu_info",
|
||||
"approval",
|
||||
"approval_kind",
|
||||
"board",
|
||||
"comm_code",
|
||||
"product_mng",
|
||||
"part_mng",
|
||||
"material_mng",
|
||||
"order_mng_master",
|
||||
"inventory_mng",
|
||||
"contract_mgmt",
|
||||
"project_mgmt",
|
||||
"screen_definitions",
|
||||
"screen_layouts",
|
||||
"layout_standards",
|
||||
"component_standards",
|
||||
"web_type_standards",
|
||||
"button_action_standards",
|
||||
"template_standards",
|
||||
"grid_standards",
|
||||
"style_templates",
|
||||
"multi_lang_key_master",
|
||||
"multi_lang_text",
|
||||
"language_master",
|
||||
"table_labels",
|
||||
"column_labels",
|
||||
"dynamic_form_data",
|
||||
"work_history", // 작업 이력 테이블
|
||||
"delivery_status", // 배송 현황 테이블
|
||||
const BLOCKED_TABLES = [
|
||||
"pg_catalog",
|
||||
"pg_statistic",
|
||||
"pg_database",
|
||||
"pg_user",
|
||||
"information_schema",
|
||||
"session_tokens", // 세션 토큰 테이블
|
||||
"password_history", // 패스워드 이력
|
||||
];
|
||||
|
||||
/**
|
||||
* 회사별 필터링이 필요한 테이블 목록
|
||||
* 테이블 이름 검증 정규식
|
||||
* SQL 인젝션 방지: 영문, 숫자, 언더스코어만 허용
|
||||
*/
|
||||
const COMPANY_FILTERED_TABLES = [
|
||||
"company_mng",
|
||||
"user_info",
|
||||
"dept_info",
|
||||
"approval",
|
||||
"board",
|
||||
"product_mng",
|
||||
"part_mng",
|
||||
"material_mng",
|
||||
"order_mng_master",
|
||||
"inventory_mng",
|
||||
"contract_mgmt",
|
||||
"project_mgmt",
|
||||
];
|
||||
const TABLE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
||||
|
||||
class DataService {
|
||||
/**
|
||||
* 테이블 접근 검증 (공통 메서드)
|
||||
*/
|
||||
private async validateTableAccess(
|
||||
tableName: string
|
||||
): Promise<{ valid: boolean; error?: ServiceResponse<any> }> {
|
||||
// 1. 테이블명 형식 검증 (SQL 인젝션 방지)
|
||||
if (!TABLE_NAME_REGEX.test(tableName)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: {
|
||||
success: false,
|
||||
message: `유효하지 않은 테이블명입니다: ${tableName}`,
|
||||
error: "INVALID_TABLE_NAME",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 블랙리스트 검증
|
||||
if (BLOCKED_TABLES.includes(tableName)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: {
|
||||
success: false,
|
||||
message: `접근이 금지된 테이블입니다: ${tableName}`,
|
||||
error: "TABLE_ACCESS_DENIED",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 3. 테이블 존재 여부 확인
|
||||
const tableExists = await this.checkTableExists(tableName);
|
||||
if (!tableExists) {
|
||||
return {
|
||||
valid: false,
|
||||
error: {
|
||||
success: false,
|
||||
message: `테이블을 찾을 수 없습니다: ${tableName}`,
|
||||
error: "TABLE_NOT_FOUND",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 데이터 조회
|
||||
*/
|
||||
@@ -92,23 +114,10 @@ class DataService {
|
||||
} = params;
|
||||
|
||||
try {
|
||||
// 테이블명 화이트리스트 검증
|
||||
if (!ALLOWED_TABLES.includes(tableName)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `접근이 허용되지 않은 테이블입니다: ${tableName}`,
|
||||
error: "TABLE_NOT_ALLOWED",
|
||||
};
|
||||
}
|
||||
|
||||
// 테이블 존재 여부 확인
|
||||
const tableExists = await this.checkTableExists(tableName);
|
||||
if (!tableExists) {
|
||||
return {
|
||||
success: false,
|
||||
message: `테이블을 찾을 수 없습니다: ${tableName}`,
|
||||
error: "TABLE_NOT_FOUND",
|
||||
};
|
||||
// 테이블 접근 검증
|
||||
const validation = await this.validateTableAccess(tableName);
|
||||
if (!validation.valid) {
|
||||
return validation.error!;
|
||||
}
|
||||
|
||||
// 동적 SQL 쿼리 생성
|
||||
@@ -119,13 +128,14 @@ class DataService {
|
||||
// WHERE 조건 생성
|
||||
const whereConditions: string[] = [];
|
||||
|
||||
// 회사별 필터링 추가
|
||||
if (COMPANY_FILTERED_TABLES.includes(tableName) && userCompany) {
|
||||
// 슈퍼관리자(*)가 아닌 경우에만 회사 필터 적용
|
||||
if (userCompany !== "*") {
|
||||
// 4. 회사별 필터링 자동 적용 (company_code 컬럼이 있는 경우)
|
||||
if (userCompany && userCompany !== "*") {
|
||||
const hasCompanyCode = await this.checkColumnExists(tableName, "company_code");
|
||||
if (hasCompanyCode) {
|
||||
whereConditions.push(`company_code = $${paramIndex}`);
|
||||
queryParams.push(userCompany);
|
||||
paramIndex++;
|
||||
console.log(`🏢 회사별 필터링 적용: ${tableName}.company_code = ${userCompany}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,13 +223,10 @@ class DataService {
|
||||
*/
|
||||
async getTableColumns(tableName: string): Promise<ServiceResponse<any[]>> {
|
||||
try {
|
||||
// 테이블명 화이트리스트 검증
|
||||
if (!ALLOWED_TABLES.includes(tableName)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `접근이 허용되지 않은 테이블입니다: ${tableName}`,
|
||||
error: "TABLE_NOT_ALLOWED",
|
||||
};
|
||||
// 테이블 접근 검증
|
||||
const validation = await this.validateTableAccess(tableName);
|
||||
if (!validation.valid) {
|
||||
return validation.error!;
|
||||
}
|
||||
|
||||
const columns = await this.getTableColumnsSimple(tableName);
|
||||
@@ -276,6 +283,31 @@ class DataService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 컬럼 존재 여부 확인
|
||||
*/
|
||||
private async checkColumnExists(
|
||||
tableName: string,
|
||||
columnName: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const result = await query<{ exists: boolean }>(
|
||||
`SELECT EXISTS (
|
||||
SELECT FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = $1
|
||||
AND column_name = $2
|
||||
)`,
|
||||
[tableName, columnName]
|
||||
);
|
||||
|
||||
return result[0]?.exists || false;
|
||||
} catch (error) {
|
||||
console.error("컬럼 존재 확인 오류:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 컬럼 정보 조회 (간단 버전)
|
||||
*/
|
||||
@@ -324,13 +356,10 @@ class DataService {
|
||||
id: string | number
|
||||
): Promise<ServiceResponse<any>> {
|
||||
try {
|
||||
// 테이블명 화이트리스트 검증
|
||||
if (!ALLOWED_TABLES.includes(tableName)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `접근이 허용되지 않은 테이블입니다: ${tableName}`,
|
||||
error: "TABLE_NOT_ALLOWED",
|
||||
};
|
||||
// 테이블 접근 검증
|
||||
const validation = await this.validateTableAccess(tableName);
|
||||
if (!validation.valid) {
|
||||
return validation.error!;
|
||||
}
|
||||
|
||||
// Primary Key 컬럼 찾기
|
||||
@@ -383,21 +412,16 @@ class DataService {
|
||||
leftValue?: string | number
|
||||
): Promise<ServiceResponse<any[]>> {
|
||||
try {
|
||||
// 테이블명 화이트리스트 검증
|
||||
if (!ALLOWED_TABLES.includes(leftTable)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `접근이 허용되지 않은 테이블입니다: ${leftTable}`,
|
||||
error: "TABLE_NOT_ALLOWED",
|
||||
};
|
||||
// 왼쪽 테이블 접근 검증
|
||||
const leftValidation = await this.validateTableAccess(leftTable);
|
||||
if (!leftValidation.valid) {
|
||||
return leftValidation.error!;
|
||||
}
|
||||
|
||||
if (!ALLOWED_TABLES.includes(rightTable)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `접근이 허용되지 않은 테이블입니다: ${rightTable}`,
|
||||
error: "TABLE_NOT_ALLOWED",
|
||||
};
|
||||
// 오른쪽 테이블 접근 검증
|
||||
const rightValidation = await this.validateTableAccess(rightTable);
|
||||
if (!rightValidation.valid) {
|
||||
return rightValidation.error!;
|
||||
}
|
||||
|
||||
let queryText = `
|
||||
@@ -440,13 +464,10 @@ class DataService {
|
||||
data: Record<string, any>
|
||||
): Promise<ServiceResponse<any>> {
|
||||
try {
|
||||
// 테이블명 화이트리스트 검증
|
||||
if (!ALLOWED_TABLES.includes(tableName)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `접근이 허용되지 않은 테이블입니다: ${tableName}`,
|
||||
error: "TABLE_NOT_ALLOWED",
|
||||
};
|
||||
// 테이블 접근 검증
|
||||
const validation = await this.validateTableAccess(tableName);
|
||||
if (!validation.valid) {
|
||||
return validation.error!;
|
||||
}
|
||||
|
||||
const columns = Object.keys(data);
|
||||
@@ -485,13 +506,10 @@ class DataService {
|
||||
data: Record<string, any>
|
||||
): Promise<ServiceResponse<any>> {
|
||||
try {
|
||||
// 테이블명 화이트리스트 검증
|
||||
if (!ALLOWED_TABLES.includes(tableName)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `접근이 허용되지 않은 테이블입니다: ${tableName}`,
|
||||
error: "TABLE_NOT_ALLOWED",
|
||||
};
|
||||
// 테이블 접근 검증
|
||||
const validation = await this.validateTableAccess(tableName);
|
||||
if (!validation.valid) {
|
||||
return validation.error!;
|
||||
}
|
||||
|
||||
// Primary Key 컬럼 찾기
|
||||
@@ -554,13 +572,10 @@ class DataService {
|
||||
id: string | number
|
||||
): Promise<ServiceResponse<void>> {
|
||||
try {
|
||||
// 테이블명 화이트리스트 검증
|
||||
if (!ALLOWED_TABLES.includes(tableName)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `접근이 허용되지 않은 테이블입니다: ${tableName}`,
|
||||
error: "TABLE_NOT_ALLOWED",
|
||||
};
|
||||
// 테이블 접근 검증
|
||||
const validation = await this.validateTableAccess(tableName);
|
||||
if (!validation.valid) {
|
||||
return validation.error!;
|
||||
}
|
||||
|
||||
// Primary Key 컬럼 찾기
|
||||
|
||||
Reference in New Issue
Block a user