feat: новая функция
This commit is contained in:
@@ -22,8 +22,7 @@ README*
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Зависимости (будут установлены в контейнере)
|
||||
node_modules/
|
||||
# Логи установки
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
@@ -16,6 +16,7 @@ const cors = require('cors');
|
||||
const session = require('express-session');
|
||||
const sessionConfig = require('./config/session');
|
||||
const logger = require('./utils/logger');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
// const csurf = require('csurf'); // Закомментировано, так как не используется
|
||||
const errorHandler = require('./middleware/errorHandler');
|
||||
// const { version } = require('./package.json'); // Закомментировано, так как не используется
|
||||
@@ -115,14 +116,21 @@ app.set('host', '0.0.0.0');
|
||||
app.set('port', process.env.PORT || 8000);
|
||||
|
||||
// Настройка CORS
|
||||
const corsOrigins = process.env.NODE_ENV === 'production'
|
||||
? [
|
||||
'http://localhost:9000', // Локальный Docker nginx
|
||||
'https://localhost:9443', // Локальный Docker nginx HTTPS
|
||||
// Добавьте ваш продакшн домен здесь для VDS
|
||||
// 'https://yourdomain.com',
|
||||
]
|
||||
: [
|
||||
'http://localhost:5173', // Vite dev server
|
||||
'http://127.0.0.1:5173',
|
||||
];
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
origin: [
|
||||
'http://localhost:5173',
|
||||
'http://127.0.0.1:5173',
|
||||
'https://hb3-accelerator.com',
|
||||
'https://www.hb3-accelerator.com',
|
||||
],
|
||||
origin: corsOrigins,
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'Cookie'],
|
||||
@@ -171,6 +179,36 @@ app.use((req, res, next) => {
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Определяем режим работы
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
// Rate limiting
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 минут
|
||||
max: isProduction ? 100 : 1000, // 100 запросов в продакшне, 1000 в dev
|
||||
message: {
|
||||
error: 'Слишком много запросов, попробуйте позже',
|
||||
retryAfter: '15 минут'
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
// Применяем rate limiting ко всем запросам
|
||||
app.use(limiter);
|
||||
|
||||
// Строгий rate limiting для чувствительных эндпоинтов
|
||||
const strictLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 минут
|
||||
max: 10, // 10 попыток
|
||||
message: {
|
||||
error: 'Превышен лимит попыток, попробуйте позже',
|
||||
retryAfter: '15 минут'
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
// Статическая раздача загруженных файлов (для dev и prod)
|
||||
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
|
||||
app.use('/api/uploads', express.static(path.join(__dirname, 'uploads')));
|
||||
@@ -178,14 +216,26 @@ app.use('/api/uploads', express.static(path.join(__dirname, 'uploads')));
|
||||
// Настройка безопасности
|
||||
app.use(
|
||||
helmet({
|
||||
contentSecurityPolicy: false, // Отключаем CSP для разработки
|
||||
contentSecurityPolicy: isProduction ? {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
scriptSrc: ["'self'"],
|
||||
imgSrc: ["'self'", "data:", "https:"],
|
||||
connectSrc: ["'self'", "ws:", "wss:"],
|
||||
fontSrc: ["'self'"],
|
||||
objectSrc: ["'none'"],
|
||||
mediaSrc: ["'self'"],
|
||||
frameSrc: ["'none'"],
|
||||
},
|
||||
} : false, // Отключаем CSP для разработки
|
||||
})
|
||||
);
|
||||
|
||||
// Маршруты API
|
||||
app.use('/api/tables', tablesRoutes); // ДОЛЖНО БЫТЬ ВЫШЕ!
|
||||
// app.use('/api', identitiesRoutes);
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/auth', strictLimiter, authRoutes); // Строгий rate limiting для аутентификации
|
||||
app.use('/api/users', usersRoutes);
|
||||
app.use('/api/chat', chatRoutes);
|
||||
app.use('/api/admin', adminRoutes);
|
||||
@@ -195,10 +245,10 @@ app.use('/api/kpp', kppRoutes); // Добавленное использован
|
||||
app.use('/api/geocoding', geocodingRoutes); // Добавленное использование роута
|
||||
|
||||
app.use('/api/dle-v2', dleV2Routes); // Добавляем маршрут DLE v2
|
||||
app.use('/api/settings', settingsRoutes); // Добавляем маршрут настроек
|
||||
app.use('/api/settings', strictLimiter, settingsRoutes); // Строгий rate limiting для настроек
|
||||
app.use('/api/countries', countriesRoutes); // Добавляем маршрут стран
|
||||
app.use('/api/russian-classifiers', russianClassifiersRoutes); // Добавляем маршрут российских классификаторов
|
||||
app.use('/api/ollama', ollamaRoutes); // Добавляем маршрут Ollama
|
||||
app.use('/api/ollama', strictLimiter, ollamaRoutes); // Строгий rate limiting для Ollama
|
||||
app.use('/api/ai-queue', aiQueueRoutes); // Добавляем маршрут AI Queue
|
||||
app.use('/api/tags', tagsRoutes); // Добавляем маршрут тегов
|
||||
app.use('/api/blockchain', blockchainRoutes); // Добавляем маршрут blockchain
|
||||
|
||||
@@ -24,6 +24,8 @@ function setPoolChangeCallback(cb) {
|
||||
let sessionMiddleware = createSessionMiddleware();
|
||||
|
||||
function createSessionMiddleware() {
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
return session({
|
||||
store: new pgSession({
|
||||
pool: db.getPool(),
|
||||
@@ -36,8 +38,8 @@ function createSessionMiddleware() {
|
||||
cookie: {
|
||||
maxAge: 30 * 24 * 60 * 60 * 1000,
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
sameSite: 'lax',
|
||||
secure: false, // false для локального Docker (HTTP)
|
||||
sameSite: 'lax', // lax для локального Docker
|
||||
path: '/',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -787,21 +787,25 @@ router.get('/encryption-key/status', requireAdmin, async (req, res) => {
|
||||
|
||||
const exists = fs.existsSync(keyPath);
|
||||
|
||||
let key = null;
|
||||
if (exists) {
|
||||
try {
|
||||
key = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
} catch (error) {
|
||||
logger.error('Ошибка чтения ключа:', error);
|
||||
}
|
||||
// Возвращаем только метаданные без содержимого ключа
|
||||
let checksum = null;
|
||||
if (exists) {
|
||||
try {
|
||||
const data = fs.readFileSync(keyPath);
|
||||
// лёгкая хэш-сумма для проверки целостности без раскрытия ключа
|
||||
const crypto = require('crypto');
|
||||
checksum = crypto.createHash('sha256').update(data).digest('hex');
|
||||
} catch (error) {
|
||||
logger.error('Ошибка чтения ключа для метаданных:', error);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
exists,
|
||||
path: keyPath,
|
||||
key: key
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
exists,
|
||||
path: keyPath,
|
||||
checksum
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Ошибка проверки статуса ключа шифрования:', error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
|
||||
@@ -1,48 +1,10 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const https = require('https');
|
||||
const { promisify } = require('util');
|
||||
const dns = require('dns');
|
||||
|
||||
const resolve4 = promisify(dns.resolve4);
|
||||
|
||||
const SSH_DIR = path.join(process.env.HOME || process.env.USERPROFILE, '.ssh');
|
||||
const DEFAULT_KEY_PATH = path.join(SSH_DIR, 'id_rsa');
|
||||
const DEFAULT_PUB_KEY_PATH = path.join(SSH_DIR, 'id_rsa.pub');
|
||||
|
||||
// Helper to read SSH key
|
||||
const readSshKey = (keyPath) => {
|
||||
try {
|
||||
return fs.readFileSync(keyPath, 'utf8');
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// GET /api/ssh-key - Get existing SSH private key
|
||||
router.get('/ssh-key', (req, res) => {
|
||||
const privateKey = readSshKey(DEFAULT_KEY_PATH);
|
||||
const publicKey = readSshKey(DEFAULT_PUB_KEY_PATH);
|
||||
|
||||
if (privateKey) {
|
||||
res.json({ success: true, sshKey: privateKey, publicKey: publicKey, keyType: 'rsa' });
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: 'SSH private key not found' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/ssh-key/public - Get existing SSH public key
|
||||
router.get('/ssh-key/public', (req, res) => {
|
||||
const publicKey = readSshKey(DEFAULT_PUB_KEY_PATH);
|
||||
|
||||
if (publicKey) {
|
||||
res.json({ success: true, publicKey: publicKey, keyType: 'rsa' });
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: 'SSH public key not found' });
|
||||
}
|
||||
});
|
||||
// Удалено: эндпоинты выдачи приватного/публичного SSH-ключа
|
||||
|
||||
// GET /api/dns-check/:domain - Check DNS and get IP address
|
||||
router.get('/dns-check/:domain', async (req, res) => {
|
||||
|
||||
279
backend/shared/permissions.js
Normal file
279
backend/shared/permissions.js
Normal file
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
/**
|
||||
* Единая матрица прав доступа для DLE
|
||||
* Используется и на backend, и на frontend
|
||||
*/
|
||||
|
||||
// Роли в системе
|
||||
const ROLES = {
|
||||
GUEST: 'guest', // Неавторизованный гость
|
||||
USER: 'user', // Авторизованный гость (0 токенов)
|
||||
READONLY: 'readonly', // Админ чтение (токены > 0 && < X)
|
||||
EDITOR: 'editor' // Админ редактор (токены >= X)
|
||||
};
|
||||
|
||||
// Список всех прав в системе
|
||||
const PERMISSIONS = {
|
||||
// Публичный доступ
|
||||
VIEW_HOME: 'view_home',
|
||||
CHAT_AI: 'chat_ai',
|
||||
|
||||
// Получение сообщений
|
||||
RECEIVE_MESSAGES: 'receive_messages',
|
||||
|
||||
// Просмотр данных
|
||||
VIEW_CRM: 'view_crm',
|
||||
VIEW_CONTACTS: 'view_contacts',
|
||||
VIEW_DATA: 'view_data',
|
||||
|
||||
// Отправка сообщений
|
||||
SEND_TO_USERS: 'send_to_users',
|
||||
CHAT_WITH_ADMINS: 'chat_with_admins',
|
||||
|
||||
// AI функции
|
||||
GENERATE_AI_REPLIES: 'generate_ai_replies',
|
||||
|
||||
// Редактирование
|
||||
EDIT_USER_DATA: 'edit_user_data',
|
||||
EDIT_CONTACTS: 'edit_contacts',
|
||||
|
||||
// Удаление
|
||||
DELETE_USER_DATA: 'delete_user_data',
|
||||
DELETE_MESSAGES: 'delete_messages',
|
||||
|
||||
// Массовые операции
|
||||
BROADCAST: 'broadcast',
|
||||
|
||||
// Управление тегами
|
||||
MANAGE_TAGS: 'manage_tags',
|
||||
|
||||
// Блокировка пользователей
|
||||
BLOCK_USERS: 'block_users',
|
||||
|
||||
// Управление настройками
|
||||
MANAGE_SETTINGS: 'manage_settings',
|
||||
|
||||
// Контент: юридические документы
|
||||
VIEW_BASIC_DOCS: 'view_basic_docs', // Базовые документы для пользователей
|
||||
VIEW_LEGAL_DOCS: 'view_legal_docs', // Юридические документы для читателей
|
||||
MANAGE_LEGAL_DOCS: 'manage_legal_docs' // Управление документами для редакторов
|
||||
};
|
||||
|
||||
// Матрица: какая роль имеет какие права
|
||||
const PERMISSIONS_MAP = {
|
||||
[ROLES.GUEST]: [
|
||||
PERMISSIONS.VIEW_HOME,
|
||||
PERMISSIONS.CHAT_AI
|
||||
],
|
||||
|
||||
[ROLES.USER]: [
|
||||
PERMISSIONS.VIEW_HOME,
|
||||
PERMISSIONS.CHAT_AI,
|
||||
PERMISSIONS.RECEIVE_MESSAGES,
|
||||
PERMISSIONS.VIEW_CONTACTS, // Пользователи могут видеть контакты для выбора
|
||||
PERMISSIONS.SEND_TO_USERS, // Пользователи могут отправлять сообщения
|
||||
PERMISSIONS.CHAT_WITH_ADMINS, // Авторизованные пользователи могут видеть личные сообщения
|
||||
PERMISSIONS.VIEW_BASIC_DOCS // Базовые документы для пользователей
|
||||
],
|
||||
|
||||
[ROLES.READONLY]: [
|
||||
PERMISSIONS.VIEW_HOME,
|
||||
PERMISSIONS.CHAT_AI,
|
||||
PERMISSIONS.RECEIVE_MESSAGES,
|
||||
PERMISSIONS.VIEW_CRM,
|
||||
PERMISSIONS.VIEW_CONTACTS,
|
||||
PERMISSIONS.VIEW_DATA,
|
||||
PERMISSIONS.SEND_TO_USERS,
|
||||
PERMISSIONS.CHAT_WITH_ADMINS,
|
||||
// Базовые документы для пользователей
|
||||
PERMISSIONS.VIEW_BASIC_DOCS,
|
||||
// Чтение внутренних юридических документов
|
||||
PERMISSIONS.VIEW_LEGAL_DOCS
|
||||
],
|
||||
|
||||
[ROLES.EDITOR]: [
|
||||
PERMISSIONS.VIEW_HOME,
|
||||
PERMISSIONS.CHAT_AI,
|
||||
PERMISSIONS.RECEIVE_MESSAGES,
|
||||
PERMISSIONS.VIEW_CRM,
|
||||
PERMISSIONS.VIEW_CONTACTS,
|
||||
PERMISSIONS.VIEW_DATA,
|
||||
PERMISSIONS.SEND_TO_USERS,
|
||||
PERMISSIONS.CHAT_WITH_ADMINS,
|
||||
PERMISSIONS.GENERATE_AI_REPLIES,
|
||||
PERMISSIONS.EDIT_USER_DATA,
|
||||
PERMISSIONS.EDIT_CONTACTS,
|
||||
PERMISSIONS.DELETE_USER_DATA,
|
||||
PERMISSIONS.DELETE_MESSAGES,
|
||||
PERMISSIONS.BROADCAST,
|
||||
PERMISSIONS.MANAGE_TAGS,
|
||||
PERMISSIONS.BLOCK_USERS,
|
||||
PERMISSIONS.MANAGE_SETTINGS,
|
||||
// Базовые документы для пользователей
|
||||
PERMISSIONS.VIEW_BASIC_DOCS,
|
||||
// Полный доступ к юридическим документам
|
||||
PERMISSIONS.VIEW_LEGAL_DOCS,
|
||||
PERMISSIONS.MANAGE_LEGAL_DOCS
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Проверяет, имеет ли роль определенное право
|
||||
* @param {string} role - Роль пользователя
|
||||
* @param {string} permission - Требуемое право
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasPermission(role, permission) {
|
||||
if (!role || !permission) return false;
|
||||
return PERMISSIONS_MAP[role]?.includes(permission) || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает все права для роли
|
||||
* @param {string} role - Роль пользователя
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
function getPermissionsForRole(role) {
|
||||
return PERMISSIONS_MAP[role] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, имеет ли роль ХОТЯ БЫ ОДНО из прав
|
||||
* @param {string} role - Роль пользователя
|
||||
* @param {Array<string>} permissions - Список прав
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasAnyPermission(role, permissions) {
|
||||
if (!Array.isArray(permissions)) return false;
|
||||
return permissions.some(p => hasPermission(role, p));
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, имеет ли роль ВСЕ указанные права
|
||||
* @param {string} role - Роль пользователя
|
||||
* @param {Array<string>} permissions - Список прав
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasAllPermissions(role, permissions) {
|
||||
if (!Array.isArray(permissions)) return false;
|
||||
return permissions.every(p => hasPermission(role, p));
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает описание роли
|
||||
* @param {string} role - Роль пользователя
|
||||
* @returns {string}
|
||||
*/
|
||||
function getRoleDescription(role) {
|
||||
const descriptions = {
|
||||
[ROLES.GUEST]: 'Неавторизованный гость',
|
||||
[ROLES.USER]: 'Авторизованный гость',
|
||||
[ROLES.READONLY]: 'Админ (только чтение)',
|
||||
[ROLES.EDITOR]: 'Админ (редактор)'
|
||||
};
|
||||
|
||||
return descriptions[role] || 'Неизвестная роль';
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, может ли отправитель отправлять сообщения получателю
|
||||
* @param {string} senderRole - Роль отправителя
|
||||
* @param {string} recipientRole - Роль получателя
|
||||
* @param {number} senderId - ID отправителя
|
||||
* @param {number} recipientId - ID получателя
|
||||
* @returns {Object} { canSend: boolean, errorMessage?: string }
|
||||
*/
|
||||
function canSendMessage(senderRole, recipientRole, senderId, recipientId) {
|
||||
// Проверяем базовое право на отправку сообщений
|
||||
if (!hasPermission(senderRole, PERMISSIONS.SEND_TO_USERS)) {
|
||||
return {
|
||||
canSend: false,
|
||||
errorMessage: 'У вас нет права на отправку сообщений'
|
||||
};
|
||||
}
|
||||
|
||||
// Собственный чат - всегда разрешен (для ИИ ассистента)
|
||||
if (senderId === recipientId) {
|
||||
return { canSend: true };
|
||||
}
|
||||
|
||||
// USER и READONLY могут писать только EDITOR
|
||||
if ((senderRole === 'user' || senderRole === 'readonly') && recipientRole === 'editor') {
|
||||
return { canSend: true };
|
||||
}
|
||||
|
||||
// EDITOR может писать всем (USER, READONLY, EDITOR)
|
||||
if (senderRole === 'editor') {
|
||||
return { canSend: true };
|
||||
}
|
||||
|
||||
// USER и READONLY НЕ могут писать друг другу
|
||||
if ((senderRole === 'user' || senderRole === 'readonly') &&
|
||||
(recipientRole === 'user' || recipientRole === 'readonly')) {
|
||||
return {
|
||||
canSend: false,
|
||||
errorMessage: 'Пользователи и читатели не могут отправлять сообщения друг другу'
|
||||
};
|
||||
}
|
||||
|
||||
// Остальные случаи запрещены
|
||||
return {
|
||||
canSend: false,
|
||||
errorMessage: `Роль ${senderRole} не может отправлять сообщения роли ${recipientRole}`
|
||||
};
|
||||
}
|
||||
|
||||
// Экспорты для CommonJS (Node.js)
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
ROLES,
|
||||
PERMISSIONS,
|
||||
PERMISSIONS_MAP,
|
||||
hasPermission,
|
||||
getPermissionsForRole,
|
||||
hasAnyPermission,
|
||||
hasAllPermissions,
|
||||
getRoleDescription,
|
||||
canSendMessage
|
||||
};
|
||||
}
|
||||
|
||||
// ES модули для Frontend
|
||||
export {
|
||||
ROLES,
|
||||
PERMISSIONS,
|
||||
PERMISSIONS_MAP,
|
||||
hasPermission,
|
||||
getPermissionsForRole,
|
||||
hasAnyPermission,
|
||||
hasAllPermissions,
|
||||
getRoleDescription,
|
||||
canSendMessage
|
||||
};
|
||||
|
||||
// CommonJS для Backend
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
ROLES,
|
||||
PERMISSIONS,
|
||||
PERMISSIONS_MAP,
|
||||
hasPermission,
|
||||
getPermissionsForRole,
|
||||
hasAnyPermission,
|
||||
hasAllPermissions,
|
||||
getRoleDescription,
|
||||
canSendMessage
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user