ваше сообщение коммита

This commit is contained in:
2025-07-27 03:30:13 +03:00
parent 057fe6254c
commit 1835632be9
141 changed files with 32514 additions and 6661 deletions

View File

@@ -0,0 +1,440 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const fs = require('fs');
const path = require('path');
class EncryptedDataService {
constructor() {
this.encryptionKey = this.loadEncryptionKey();
this.isEncryptionEnabled = !!this.encryptionKey;
if (this.isEncryptionEnabled) {
console.log('🔐 Шифрование базы данных активировано');
console.log('📋 Автоматическое определение зашифрованных колонок');
} else {
console.log('⚠️ Шифрование базы данных отключено - ключ не найден');
}
}
loadEncryptionKey() {
try {
const keyPath = path.join(__dirname, '../../ssl/keys/full_db_encryption.key');
console.log(`[EncryptedDB] Trying key path: ${keyPath}`);
if (fs.existsSync(keyPath)) {
const key = fs.readFileSync(keyPath, 'utf8').trim();
console.log(`[EncryptedDB] Key loaded from: ${keyPath}, length: ${key.length}`);
return key;
}
// Попробуем альтернативный путь относительно корня приложения
const altKeyPath = '/app/ssl/keys/full_db_encryption.key';
console.log(`[EncryptedDB] Trying alternative key path: ${altKeyPath}`);
if (fs.existsSync(altKeyPath)) {
const key = fs.readFileSync(altKeyPath, 'utf8').trim();
console.log(`[EncryptedDB] Key loaded from: ${altKeyPath}, length: ${key.length}`);
return key;
}
console.log(`[EncryptedDB] No key file found, using default key`);
return 'default-key';
} catch (error) {
console.error('❌ Ошибка загрузки ключа шифрования:', error);
return 'default-key';
}
}
/**
* Получить данные из таблицы с автоматической расшифровкой
*/
async getData(tableName, conditions = {}, limit = null, orderBy = null) {
try {
// Проверяем, включено ли шифрование
if (!this.isEncryptionEnabled) {
return await this.executeUnencryptedQuery(tableName, conditions, limit, orderBy);
}
// Получаем информацию о колонках
const { rows: columns } = await db.getQuery()(`
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = $1
AND table_schema = 'public'
ORDER BY ordinal_position
`, [tableName]);
// Строим SELECT с расшифровкой
const selectFields = columns.map(col => {
if (col.column_name.endsWith('_encrypted')) {
const originalName = col.column_name.replace('_encrypted', '');
console.log(`🔓 Расшифровываем поле ${col.column_name} -> ${originalName}`);
if (col.data_type === 'jsonb') {
return `decrypt_json(${col.column_name}, $1) as "${originalName}"`;
} else {
return `decrypt_text(${col.column_name}, $1) as "${originalName}"`;
}
} else if (!col.column_name.includes('_encrypted')) {
// Проверяем, есть ли зашифрованная версия этой колонки
const hasEncryptedVersion = columns.some(encCol =>
encCol.column_name === `${col.column_name}_encrypted`
);
// Если есть зашифрованная версия, пропускаем незашифрованную
if (hasEncryptedVersion) {
console.log(`⚠️ Пропускаем незашифрованное поле ${col.column_name} (есть зашифрованная версия)`);
return null;
}
// Заключаем зарезервированные слова в кавычки
const reservedWords = ['order', 'group', 'user', 'index', 'table', 'column', 'key', 'foreign', 'primary', 'unique', 'check', 'constraint', 'default', 'null', 'not', 'and', 'or', 'as', 'on', 'in', 'is', 'like', 'between', 'exists', 'all', 'any', 'some', 'distinct', 'case', 'when', 'then', 'else', 'end', 'limit', 'offset', 'having', 'union', 'intersect', 'except', 'with', 'recursive'];
if (reservedWords.includes(col.column_name.toLowerCase())) {
return `"${col.column_name}"`;
}
return col.column_name;
}
return null;
}).filter(Boolean).join(', ');
let query = `SELECT ${selectFields} FROM ${tableName}`;
// Проверяем, есть ли зашифрованные поля в таблице
const hasEncryptedFields = columns.some(col => col.column_name.endsWith('_encrypted'));
const params = hasEncryptedFields ? [this.encryptionKey] : [];
let paramIndex = hasEncryptedFields ? 2 : 1;
// Список зарезервированных слов для WHERE-условий
const reservedWords = ['order', 'group', 'user', 'index', 'table', 'column', 'key', 'foreign', 'primary', 'unique', 'check', 'constraint', 'default', 'null', 'not', 'and', 'or', 'as', 'on', 'in', 'is', 'like', 'between', 'exists', 'all', 'any', 'some', 'distinct', 'case', 'when', 'then', 'else', 'end', 'limit', 'offset', 'having', 'union', 'intersect', 'except', 'with', 'recursive'];
if (Object.keys(conditions).length > 0) {
const whereClause = Object.keys(conditions)
.map(key => {
const value = conditions[key];
// Проверяем, есть ли зашифрованная версия колонки
const encryptedColumn = columns.find(col => col.column_name === `${key}_encrypted`);
// Обрабатываем оператор $in
if (value && typeof value === 'object' && value.$in && Array.isArray(value.$in)) {
const placeholders = value.$in.map(() => `$${paramIndex++}`).join(', ');
const columnName = encryptedColumn ? key :
(reservedWords.includes(key.toLowerCase()) ? `"${key}"` : key);
return `${columnName} IN (${placeholders})`;
}
// Обрабатываем оператор $ne
if (value && typeof value === 'object' && value.$ne !== undefined) {
const columnName = encryptedColumn ? key :
(reservedWords.includes(key.toLowerCase()) ? `"${key}"` : key);
return `${columnName} != $${paramIndex++}`;
}
if (encryptedColumn) {
// Для зашифрованных колонок используем прямое сравнение с зашифрованным значением
return `${key}_encrypted = encrypt_text($${paramIndex++}, ${hasEncryptedFields ? '$1' : 'NULL'})`;
} else {
// Для незашифрованных колонок используем обычное сравнение
// Заключаем зарезервированные слова в кавычки
const columnName = reservedWords.includes(key.toLowerCase()) ? `"${key}"` : key;
return `${columnName} = $${paramIndex++}`;
}
})
.join(' AND ');
query += ` WHERE ${whereClause}`;
// Добавляем параметры для $in операторов
const paramsToAdd = Object.values(conditions).map(value => {
if (value && typeof value === 'object' && value.$in && Array.isArray(value.$in)) {
return value.$in;
}
if (value && typeof value === 'object' && value.$ne !== undefined) {
return value.$ne;
}
return value;
}).flat();
params.push(...paramsToAdd);
}
if (orderBy) {
query += ` ORDER BY ${orderBy}`;
}
if (limit) {
query += ` LIMIT ${limit}`;
}
console.log(`🔍 [getData] Выполняем запрос:`, query);
console.log(`🔍 [getData] Параметры:`, params);
const { rows } = await db.getQuery()(query, params);
console.log(`📊 Результат запроса из ${tableName}:`, rows);
return rows;
} catch (error) {
console.error(`❌ Ошибка получения данных из ${tableName}:`, error);
throw error;
}
}
/**
* Сохранить данные в таблицу с автоматическим шифрованием
*/
async saveData(tableName, data, whereConditions = null) {
try {
// Проверяем, включено ли шифрование
if (!this.isEncryptionEnabled) {
return await this.executeUnencryptedSave(tableName, data, whereConditions);
}
// Для таблицы users используем обычные запросы, так как она содержит смешанные колонки
if (tableName === 'users') {
return await this.executeUnencryptedSave(tableName, data, whereConditions);
}
// Получаем информацию о колонках
const { rows: columns } = await db.getQuery()(`
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = $1
AND table_schema = 'public'
ORDER BY ordinal_position
`, [tableName]);
// Подготавливаем данные для шифрования
const encryptedData = {};
const unencryptedData = {};
const filteredData = {}; // Отфильтрованные данные для параметров
// Проверяем, есть ли зашифрованные поля в таблице
const hasEncryptedFields = columns.some(col => col.column_name.endsWith('_encrypted'));
let paramIndex = hasEncryptedFields ? 2 : 1; // Начинаем с 2, если есть зашифрованные поля, иначе с 1
for (const [key, value] of Object.entries(data)) {
// Проверяем, есть ли зашифрованная версия колонки
const encryptedColumn = columns.find(col => col.column_name === `${key}_encrypted`);
const unencryptedColumn = columns.find(col => col.column_name === key);
console.log(`🔍 Обрабатываем поле ${key} = "${value}" (тип: ${typeof value})`);
if (encryptedColumn) {
// Если есть зашифрованная колонка, шифруем данные
// Проверяем, что значение не пустое перед шифрованием
if (value === null || value === undefined || (typeof value === 'string' && value.trim() === '')) {
// Пропускаем пустые значения
console.log(`⚠️ Пропускаем пустое зашифрованное поле ${key}`);
continue;
}
const currentParamIndex = paramIndex++;
filteredData[key] = value; // Добавляем в отфильтрованные данные
console.log(`✅ Добавили зашифрованное поле ${key} в filteredData`);
if (encryptedColumn.data_type === 'jsonb') {
encryptedData[`${key}_encrypted`] = `encrypt_json($${currentParamIndex}, ${hasEncryptedFields ? '$1::text' : 'NULL'})`;
} else {
encryptedData[`${key}_encrypted`] = `encrypt_text($${currentParamIndex}, ${hasEncryptedFields ? '$1::text' : 'NULL'})`;
}
} else if (unencryptedColumn) {
// Если есть незашифрованная колонка, сохраняем как есть
// Проверяем, что значение не пустое перед сохранением (кроме role и sender_type)
if ((value === null || value === undefined || (typeof value === 'string' && value.trim() === '')) &&
key !== 'role' && key !== 'sender_type') {
// Пропускаем пустые значения, кроме role и sender_type
console.log(`⚠️ Пропускаем пустое незашифрованное поле ${key}`);
continue;
}
filteredData[key] = value; // Добавляем в отфильтрованные данные
unencryptedData[key] = `$${paramIndex++}`;
console.log(`✅ Добавили незашифрованное поле ${key} в filteredData и unencryptedData`);
} else {
// Если колонка не найдена, пропускаем
console.warn(`⚠️ Колонка ${key} не найдена в таблице ${tableName}`);
}
}
const allData = { ...unencryptedData, ...encryptedData };
// Проверяем, есть ли данные для сохранения
if (Object.keys(allData).length === 0) {
console.warn(`⚠️ Нет данных для сохранения в таблице ${tableName} - все значения пустые`);
console.warn(`⚠️ Исходные данные:`, data);
console.warn(`⚠️ Отфильтрованные данные:`, filteredData);
return null;
}
// Функция для заключения зарезервированных слов в кавычки
const quoteReservedWord = (word) => {
const reservedWords = ['order', 'group', 'user', 'index', 'table', 'column', 'key', 'foreign', 'primary', 'unique', 'check', 'constraint', 'default', 'null', 'not', 'and', 'or', 'as', 'on', 'in', 'is', 'like', 'between', 'exists', 'all', 'any', 'some', 'distinct', 'case', 'when', 'then', 'else', 'end', 'limit', 'offset', 'having', 'union', 'intersect', 'except', 'with', 'recursive'];
return reservedWords.includes(word.toLowerCase()) ? `"${word}"` : word;
};
if (whereConditions) {
// UPDATE
const setClause = Object.keys(allData)
.map((key, index) => `${quoteReservedWord(key)} = ${allData[key]}`)
.join(', ');
const whereClause = Object.keys(whereConditions)
.map((key, index) => `${quoteReservedWord(key)} = $${paramIndex + index}`)
.join(' AND ');
const query = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause} RETURNING *`;
const allParams = hasEncryptedFields ? [this.encryptionKey, ...Object.values(filteredData), ...Object.values(whereConditions)] : [...Object.values(filteredData), ...Object.values(whereConditions)];
const { rows } = await db.getQuery()(query, allParams);
return rows[0];
} else {
// INSERT
const columns = Object.keys(allData).map(key => quoteReservedWord(key));
const placeholders = Object.keys(allData).map(key => allData[key]).join(', ');
const query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders}) RETURNING *`;
const params = hasEncryptedFields ? [this.encryptionKey, ...Object.values(filteredData)] : [...Object.values(filteredData)];
const { rows } = await db.getQuery()(query, params);
return rows[0];
}
} catch (error) {
console.error(`❌ Ошибка сохранения данных в ${tableName}:`, error);
throw error;
}
}
/**
* Удалить данные из таблицы
*/
async deleteData(tableName, conditions) {
try {
// Функция для заключения зарезервированных слов в кавычки
const quoteReservedWord = (word) => {
const reservedWords = ['order', 'group', 'user', 'index', 'table', 'column', 'key', 'foreign', 'primary', 'unique', 'check', 'constraint', 'default', 'null', 'not', 'and', 'or', 'as', 'on', 'in', 'is', 'like', 'between', 'exists', 'all', 'any', 'some', 'distinct', 'case', 'when', 'then', 'else', 'end', 'limit', 'offset', 'having', 'union', 'intersect', 'except', 'with', 'recursive'];
return reservedWords.includes(word.toLowerCase()) ? `"${word}"` : word;
};
// Проверяем, включено ли шифрование
if (!this.isEncryptionEnabled) {
let query = `DELETE FROM ${tableName}`;
const params = [];
let paramIndex = 1;
if (Object.keys(conditions).length > 0) {
const whereClause = Object.keys(conditions)
.map(key => `${quoteReservedWord(key)} = $${paramIndex++}`)
.join(' AND ');
query += ` WHERE ${whereClause}`;
params.push(...Object.values(conditions));
}
const { rows } = await db.getQuery()(query, params);
return rows;
}
// Для зашифрованных таблиц - пока используем обычный DELETE
// TODO: Добавить логику для зашифрованных условий WHERE
let query = `DELETE FROM ${tableName}`;
const params = [];
let paramIndex = 1;
if (Object.keys(conditions).length > 0) {
const whereClause = Object.keys(conditions)
.map(key => `${quoteReservedWord(key)} = $${paramIndex++}`)
.join(' AND ');
query += ` WHERE ${whereClause}`;
params.push(...Object.values(conditions));
}
const { rows } = await db.getQuery()(query, params);
return rows;
} catch (error) {
console.error(`❌ Ошибка удаления данных из ${tableName}:`, error);
throw error;
}
}
/**
* Получить статус шифрования
*/
getEncryptionStatus() {
return {
enabled: this.isEncryptionEnabled,
keyExists: !!this.encryptionKey,
keyPath: path.join(__dirname, '../../ssl/keys/full_db_encryption.key')
};
}
/**
* Проверить, нужно ли шифровать колонку
*/
shouldEncryptColumn(column) {
const encryptableTypes = ['text', 'varchar', 'character varying', 'json', 'jsonb'];
return encryptableTypes.includes(column.data_type) &&
!column.column_name.includes('_encrypted') &&
!['created_at', 'updated_at', 'id'].includes(column.column_name);
}
/**
* Выполнить незашифрованный запрос (fallback)
*/
async executeUnencryptedQuery(tableName, conditions = {}, limit = null, orderBy = null) {
let query = `SELECT * FROM ${tableName}`;
const params = [];
let paramIndex = 1;
if (Object.keys(conditions).length > 0) {
const whereClause = Object.keys(conditions)
.map(key => `${key} = $${paramIndex++}`)
.join(' AND ');
query += ` WHERE ${whereClause}`;
params.push(...Object.values(conditions));
}
if (orderBy) {
query += ` ORDER BY ${orderBy}`;
}
if (limit) {
query += ` LIMIT ${limit}`;
}
const { rows } = await db.getQuery()(query, params);
return rows;
}
/**
* Выполнить незашифрованное сохранение (fallback)
*/
async executeUnencryptedSave(tableName, data, whereConditions = null) {
if (whereConditions) {
// UPDATE
const setClause = Object.keys(data)
.map((key, index) => `${key} = $${index + 1}`)
.join(', ');
const whereClause = Object.keys(whereConditions)
.map((key, index) => `${key} = $${Object.keys(data).length + index + 1}`)
.join(' AND ');
const query = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause} RETURNING *`;
const params = [...Object.values(data), ...Object.values(whereConditions)];
const { rows } = await db.getQuery()(query, params);
return rows[0];
} else {
// INSERT
const columns = Object.keys(data);
const values = Object.values(data);
const placeholders = values.map((_, index) => `$${index + 1}`).join(', ');
const query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders}) RETURNING *`;
const { rows } = await db.getQuery()(query, values);
return rows[0];
}
}
}
module.exports = new EncryptedDataService();