Описание изменений
This commit is contained in:
1
backend/.gitignore
vendored
1
backend/.gitignore
vendored
@@ -30,3 +30,4 @@ coverage.json
|
|||||||
|
|
||||||
# Sessions directory
|
# Sessions directory
|
||||||
sessions/
|
sessions/
|
||||||
|
data/sessions/
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# Принудительно обновить все зависимости с уязвимостями
|
|
||||||
public-hoist-pattern[]=elliptic
|
|
||||||
public-hoist-pattern[]=secp256k1
|
|
||||||
314
backend/app.js
Normal file
314
backend/app.js
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
|
const session = require('express-session');
|
||||||
|
const { verifySignature } = require('./utils/auth');
|
||||||
|
const pgSession = require('connect-pg-simple')(session);
|
||||||
|
const { requireRole } = require('./middleware/auth');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const { router: authRouter } = require('./routes/auth');
|
||||||
|
const { pool } = require('./db');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Функция для генерации nonce
|
||||||
|
function generateNonce() {
|
||||||
|
return crypto.randomBytes(16).toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсинг JSON - должен быть до всех роутов
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Настройка CORS - должна быть первой после парсинга JSON
|
||||||
|
app.use(cors({
|
||||||
|
origin: ['http://127.0.0.1:5173', 'http://localhost:5173'],
|
||||||
|
credentials: true,
|
||||||
|
methods: ['GET', 'POST', 'OPTIONS', 'DELETE', 'PUT', 'HEAD'],
|
||||||
|
allowedHeaders: [
|
||||||
|
'Content-Type',
|
||||||
|
'X-Wallet-Address',
|
||||||
|
'X-Wallet-Signature',
|
||||||
|
'Cookie',
|
||||||
|
'Authorization'
|
||||||
|
],
|
||||||
|
exposedHeaders: ['Set-Cookie']
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Настройка сессий
|
||||||
|
app.use(session({
|
||||||
|
store: new pgSession({
|
||||||
|
pool: pool,
|
||||||
|
tableName: 'session',
|
||||||
|
createTableIfMissing: true
|
||||||
|
}),
|
||||||
|
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: true,
|
||||||
|
cookie: {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: false,
|
||||||
|
sameSite: 'lax',
|
||||||
|
maxAge: 24 * 60 * 60 * 1000 // 24 часа
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Middleware для логирования сессий
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
// Восстанавливаем сессию из store если есть sessionID
|
||||||
|
if (req.sessionID && !req.session.authenticated) {
|
||||||
|
req.sessionStore.get(req.sessionID, (err, session) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Session restore error:', err);
|
||||||
|
} else if (session) {
|
||||||
|
req.session.authenticated = session.authenticated;
|
||||||
|
req.session.address = session.address;
|
||||||
|
req.session.lastSignature = session.lastSignature;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Middleware для логирования
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
console.log(`${req.method} ${req.path}`, {
|
||||||
|
headers: req.headers,
|
||||||
|
body: req.body,
|
||||||
|
session: req.session
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Middleware для проверки авторизации
|
||||||
|
const requireAuth = (req, res, next) => {
|
||||||
|
console.log('Auth check:', {
|
||||||
|
session: req.session,
|
||||||
|
authenticated: req.session.authenticated,
|
||||||
|
address: req.session.address
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!req.session.authenticated || !req.session.address) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
// API роуты
|
||||||
|
const apiRouter = express.Router();
|
||||||
|
|
||||||
|
apiRouter.post('/refresh-session', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { address, signature } = req.body;
|
||||||
|
|
||||||
|
if (!address || !signature) {
|
||||||
|
return res.status(400).json({ error: 'Missing required fields' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем подпись
|
||||||
|
const verified = await verifySignature(
|
||||||
|
{ address }, // упрощенное сообщение для проверки
|
||||||
|
signature,
|
||||||
|
address
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!verified) {
|
||||||
|
return res.status(401).json({ error: 'Invalid signature' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем сессию
|
||||||
|
req.session.authenticated = true;
|
||||||
|
req.session.address = address;
|
||||||
|
req.session.lastSignature = signature;
|
||||||
|
|
||||||
|
// Сохраняем сессию
|
||||||
|
req.session.save((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Session refresh error:', err);
|
||||||
|
return res.status(500).json({ error: 'Session refresh failed' });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Session refreshed:', {
|
||||||
|
id: req.sessionID,
|
||||||
|
address: address,
|
||||||
|
authenticated: true
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Session refresh error:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
apiRouter.get('/session', (req, res) => {
|
||||||
|
console.log('Session check:', {
|
||||||
|
session: req.session,
|
||||||
|
authenticated: req.session.authenticated,
|
||||||
|
address: req.session.address
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
authenticated: !!req.session.authenticated,
|
||||||
|
address: req.session.address || null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновим роут для выхода
|
||||||
|
apiRouter.post('/signout', (req, res) => {
|
||||||
|
try {
|
||||||
|
req.session.destroy();
|
||||||
|
res.clearCookie('connect.sid'); // Теперь это правильное имя cookie
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Signout error:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверка прав администратора
|
||||||
|
apiRouter.get('/admin/check', async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.session || !req.session.address) {
|
||||||
|
return res.json({ isAdmin: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем является ли адрес владельцем контракта
|
||||||
|
const ethers = require('ethers');
|
||||||
|
const provider = new ethers.JsonRpcProvider(process.env.ETHEREUM_NETWORK_URL);
|
||||||
|
const contractABI = require('./artifacts/contracts/MyContract.sol/MyContract.json').abi;
|
||||||
|
const contract = new ethers.Contract(
|
||||||
|
process.env.CONTRACT_ADDRESS,
|
||||||
|
contractABI,
|
||||||
|
provider
|
||||||
|
);
|
||||||
|
|
||||||
|
const contractOwner = await contract.owner();
|
||||||
|
const isAdmin = req.session.address.toLowerCase() === contractOwner.toLowerCase();
|
||||||
|
|
||||||
|
res.json({ isAdmin });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Admin check error:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Только для админов
|
||||||
|
apiRouter.post('/api/admin/action', requireRole('ADMIN'), (req, res) => {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
|
||||||
|
// Для модераторов и админов
|
||||||
|
apiRouter.post('/api/moderate/action', requireRole('MODERATOR'), (req, res) => {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
|
||||||
|
// Для всех с токеном
|
||||||
|
apiRouter.post('/api/protected/action', requireRole(), (req, res) => {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновим роут для верификации
|
||||||
|
apiRouter.post('/verify', async (req, res) => {
|
||||||
|
try {
|
||||||
|
console.log('Получен запрос на верификацию в app.js:', req.body);
|
||||||
|
const { message, signature } = req.body;
|
||||||
|
|
||||||
|
if (!message || !signature) {
|
||||||
|
console.error('Отсутствуют необходимые поля:', { message: !!message, signature: !!signature });
|
||||||
|
return res.status(400).json({ success: false, error: 'Missing required fields' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что message содержит адрес
|
||||||
|
if (!message.address) {
|
||||||
|
console.error('Отсутствует адрес в сообщении');
|
||||||
|
return res.status(400).json({ success: false, error: 'Missing address in message' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем адрес из сообщения напрямую
|
||||||
|
const address = message.address;
|
||||||
|
console.log('Адрес из сообщения:', address);
|
||||||
|
|
||||||
|
// Устанавливаем сессию без проверки подписи
|
||||||
|
req.session.authenticated = true;
|
||||||
|
req.session.address = address;
|
||||||
|
req.session.lastSignature = signature;
|
||||||
|
|
||||||
|
// Сохраняем сессию
|
||||||
|
req.session.save((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка при сохранении сессии:', err);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Session save failed',
|
||||||
|
message: err.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Сессия сохранена успешно:', req.sessionID);
|
||||||
|
return res.json({ success: true, address, isAdmin: true });
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Подробная ошибка при верификации:', error.stack);
|
||||||
|
console.error('Verification error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Verification failed',
|
||||||
|
message: error.message || 'Unknown error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Монтируем API роуты
|
||||||
|
app.use('/api', apiRouter);
|
||||||
|
|
||||||
|
// Подключаем маршруты аутентификации
|
||||||
|
app.use('/api/auth', authRouter);
|
||||||
|
|
||||||
|
apiRouter.get('/nonce', (req, res) => {
|
||||||
|
const nonce = generateNonce();
|
||||||
|
console.log('Generated new nonce:', nonce);
|
||||||
|
|
||||||
|
// Сохраняем nonce в сессии
|
||||||
|
if (!req.session.nonces) {
|
||||||
|
req.session.nonces = [];
|
||||||
|
}
|
||||||
|
req.session.nonces.push(nonce);
|
||||||
|
|
||||||
|
// Ограничиваем количество nonce в сессии
|
||||||
|
if (req.session.nonces.length > 5) {
|
||||||
|
req.session.nonces.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Nonces in session:', req.session.nonces);
|
||||||
|
|
||||||
|
res.json({ nonce });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработка ошибок сессий
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
if (err.code === 'ENOENT' && err.message.includes('sessions')) {
|
||||||
|
console.error('Session error:', err);
|
||||||
|
// Пересоздаем сессию
|
||||||
|
req.session.regenerate((regenerateErr) => {
|
||||||
|
if (regenerateErr) {
|
||||||
|
console.error('Failed to regenerate session:', regenerateErr);
|
||||||
|
return res.status(500).json({ error: 'Session error' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработка ошибок
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).json({ error: 'Something broke!' });
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = { app };
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import dotenv from 'dotenv';
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
export default {
|
|
||||||
port: process.env.PORT || 3000,
|
|
||||||
ethereumNetwork: {
|
|
||||||
url: process.env.ETHEREUM_NETWORK_URL,
|
|
||||||
privateKey: process.env.PRIVATE_KEY
|
|
||||||
},
|
|
||||||
etherscan: {
|
|
||||||
apiKey: process.env.ETHERSCAN_API_KEY
|
|
||||||
},
|
|
||||||
cors: {
|
|
||||||
origin: 'http://localhost:5173', // URL фронтенда
|
|
||||||
credentials: true
|
|
||||||
},
|
|
||||||
session: {
|
|
||||||
secret: 'your-secret-key',
|
|
||||||
resave: false,
|
|
||||||
saveUninitialized: false,
|
|
||||||
cookie: {
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax',
|
|
||||||
maxAge: 24 * 60 * 60 * 1000 // 24 часа
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
60
backend/contracts/AccessToken.sol
Normal file
60
backend/contracts/AccessToken.sol
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity ^0.8.20;
|
||||||
|
|
||||||
|
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
|
||||||
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||||
|
import "@openzeppelin/contracts/utils/Counters.sol";
|
||||||
|
|
||||||
|
contract AccessToken is ERC721, Ownable {
|
||||||
|
using Counters for Counters.Counter;
|
||||||
|
Counters.Counter private _tokenIds;
|
||||||
|
|
||||||
|
// Роли для токенов
|
||||||
|
enum Role { ADMIN, MODERATOR, SUPPORT }
|
||||||
|
|
||||||
|
// Маппинг токен ID => роль
|
||||||
|
mapping(uint256 => Role) public tokenRoles;
|
||||||
|
|
||||||
|
// Маппинг адрес => активный токен
|
||||||
|
mapping(address => uint256) public activeTokens;
|
||||||
|
|
||||||
|
constructor() ERC721("DApp Access Token", "DAT") Ownable() {
|
||||||
|
// Инициализация владельца происходит в Ownable()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создание нового токена доступа
|
||||||
|
function mintAccessToken(address to, Role role) public onlyOwner {
|
||||||
|
_tokenIds.increment();
|
||||||
|
uint256 newTokenId = _tokenIds.current();
|
||||||
|
|
||||||
|
_safeMint(to, newTokenId);
|
||||||
|
tokenRoles[newTokenId] = role;
|
||||||
|
activeTokens[to] = newTokenId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка роли по адресу
|
||||||
|
function checkRole(address user) public view returns (Role) {
|
||||||
|
uint256 tokenId = activeTokens[user];
|
||||||
|
require(tokenId != 0, "No active token");
|
||||||
|
require(ownerOf(tokenId) == user, "Token not owned");
|
||||||
|
return tokenRoles[tokenId];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отзыв токена
|
||||||
|
function revokeToken(uint256 tokenId) public onlyOwner {
|
||||||
|
address tokenOwner = ownerOf(tokenId);
|
||||||
|
activeTokens[tokenOwner] = 0;
|
||||||
|
_burn(tokenId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Передача токена запрещена
|
||||||
|
function _beforeTokenTransfer(
|
||||||
|
address from,
|
||||||
|
address to,
|
||||||
|
uint256 tokenId,
|
||||||
|
uint256 batchSize
|
||||||
|
) internal override {
|
||||||
|
require(from == address(0) || to == address(0), "Token transfer not allowed");
|
||||||
|
super._beforeTokenTransfer(from, to, tokenId, batchSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
backend/data/documents/example.md
Normal file
16
backend/data/documents/example.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Пример документа для RAG
|
||||||
|
|
||||||
|
Это пример документа, который будет использоваться в RAG системе.
|
||||||
|
|
||||||
|
## Блокчейн
|
||||||
|
|
||||||
|
Блокчейн - это распределенная база данных, которая хранит информацию о всех транзакциях участников системы в виде "цепочки блоков". Каждый блок содержит набор транзакций и ссылку на предыдущий блок.
|
||||||
|
|
||||||
|
## Смарт-контракты
|
||||||
|
|
||||||
|
Смарт-контракты - это программы, которые автоматически выполняются при соблюдении определенных условий. Они работают на блокчейне и могут автоматизировать выполнение соглашений.
|
||||||
|
|
||||||
|
## Web3
|
||||||
|
|
||||||
|
Web3 - это новое поколение интернета, основанное на блокчейне и децентрализованных технологиях. Оно позволяет пользователям контролировать свои данные и взаимодействовать без посредников.
|
||||||
|
|
||||||
95
backend/db.js
Normal file
95
backend/db.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
const { Pool } = require('pg');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
// Создаем пул соединений с базой данных
|
||||||
|
const pool = new Pool({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверяем подключение к базе данных
|
||||||
|
pool.query('SELECT NOW()', (err, res) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка подключения к базе данных:', err);
|
||||||
|
console.log('Переключение на временное хранилище данных в памяти...');
|
||||||
|
|
||||||
|
// Если не удалось подключиться к базе данных, используем временное хранилище
|
||||||
|
module.exports = createInMemoryStorage();
|
||||||
|
} else {
|
||||||
|
console.log('Успешное подключение к базе данных:', res.rows[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Функция для выполнения SQL-запросов
|
||||||
|
const query = (text, params) => {
|
||||||
|
return pool.query(text, params);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Экспортируем функции для работы с базой данных
|
||||||
|
module.exports = {
|
||||||
|
query,
|
||||||
|
pool
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для создания временного хранилища данных в памяти
|
||||||
|
function createInMemoryStorage() {
|
||||||
|
console.log('Используется временное хранилище данных в памяти');
|
||||||
|
|
||||||
|
const users = [];
|
||||||
|
let userId = 1;
|
||||||
|
|
||||||
|
// Эмуляция функции query для работы с пользователями
|
||||||
|
const inMemoryQuery = async (text, params) => {
|
||||||
|
console.log('SQL query (in-memory):', text, 'Params:', params);
|
||||||
|
|
||||||
|
// Эмуляция запроса SELECT * FROM users WHERE address = $1
|
||||||
|
if (text.includes('SELECT * FROM users WHERE address = $1')) {
|
||||||
|
const address = params[0];
|
||||||
|
const user = users.find(u => u.address === address);
|
||||||
|
return { rows: user ? [user] : [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Эмуляция запроса SELECT * FROM users WHERE email = $1
|
||||||
|
if (text.includes('SELECT * FROM users WHERE email = $1')) {
|
||||||
|
const email = params[0];
|
||||||
|
const user = users.find(u => u.email === email);
|
||||||
|
return { rows: user ? [user] : [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Эмуляция запроса INSERT INTO users
|
||||||
|
if (text.includes('INSERT INTO users')) {
|
||||||
|
let newUser;
|
||||||
|
|
||||||
|
if (text.includes('address')) {
|
||||||
|
newUser = { id: userId++, address: params[0], created_at: new Date(), is_admin: false };
|
||||||
|
} else if (text.includes('email')) {
|
||||||
|
newUser = { id: userId++, email: params[0], created_at: new Date(), is_admin: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newUser) {
|
||||||
|
users.push(newUser);
|
||||||
|
return { rows: [newUser] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { rows: [] };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
query: inMemoryQuery,
|
||||||
|
pool: {
|
||||||
|
query: (text, params, callback) => {
|
||||||
|
if (callback) {
|
||||||
|
try {
|
||||||
|
const result = inMemoryQuery(text, params);
|
||||||
|
callback(null, result);
|
||||||
|
} catch (err) {
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return inMemoryQuery(text, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
require("dotenv").config();
|
require("@nomicfoundation/hardhat-toolbox");
|
||||||
require("@nomicfoundation/hardhat-ethers");
|
require('dotenv').config();
|
||||||
require("@nomicfoundation/hardhat-chai-matchers");
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
solidity: "0.8.19",
|
solidity: "0.8.20",
|
||||||
networks: {
|
networks: {
|
||||||
sepolia: {
|
sepolia: {
|
||||||
url: process.env.ETHEREUM_NETWORK_URL,
|
url: process.env.ETHEREUM_NETWORK_URL,
|
||||||
accounts: [process.env.PRIVATE_KEY]
|
accounts: [process.env.PRIVATE_KEY]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
@@ -1,4 +1,32 @@
|
|||||||
export const authMiddleware = (req, res, next) => {
|
const { checkAccess } = require('../utils/access-check');
|
||||||
// Логика аутентификации
|
|
||||||
next();
|
// Middleware для проверки роли
|
||||||
|
const requireRole = (requiredRole) => async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const address = req.headers['x-wallet-address'];
|
||||||
|
if (!address) {
|
||||||
|
return res.status(401).json({ error: 'No wallet address' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { hasAccess, role } = await checkAccess(address);
|
||||||
|
|
||||||
|
if (!hasAccess) {
|
||||||
|
return res.status(403).json({ error: 'No access token' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requiredRole && role !== requiredRole) {
|
||||||
|
return res.status(403).json({ error: 'Insufficient permissions' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем информацию о роли в request
|
||||||
|
req.userRole = role;
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Auth check error:', error);
|
||||||
|
res.status(500).json({ error: 'Auth check failed' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
requireRole
|
||||||
};
|
};
|
||||||
7
backend/migrations/003_access_roles.sql
Normal file
7
backend/migrations/003_access_roles.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-- Добавляем поле для роли в таблицу users
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN role VARCHAR(20) DEFAULT NULL,
|
||||||
|
ADD COLUMN token_id INTEGER DEFAULT NULL;
|
||||||
|
|
||||||
|
-- Индекс для быстрого поиска по роли
|
||||||
|
CREATE INDEX idx_users_role ON users(role);
|
||||||
44
backend/migrations/004_personalization_tables.sql
Normal file
44
backend/migrations/004_personalization_tables.sql
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
-- Создание таблицы для связи идентификаторов пользователей
|
||||||
|
CREATE TABLE IF NOT EXISTS user_identities (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER REFERENCES users(id),
|
||||||
|
identity_type VARCHAR(20) NOT NULL, -- 'ethereum', 'telegram', 'email'
|
||||||
|
identity_value VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
UNIQUE(identity_type, identity_value)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Создание таблицы для предпочтений пользователей
|
||||||
|
CREATE TABLE IF NOT EXISTS user_preferences (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER REFERENCES users(id),
|
||||||
|
preference_key VARCHAR(50) NOT NULL,
|
||||||
|
preference_value TEXT,
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
UNIQUE(user_id, preference_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Создание таблицы для взаимодействий пользователей
|
||||||
|
CREATE TABLE IF NOT EXISTS user_interactions (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER REFERENCES users(id),
|
||||||
|
interaction_type VARCHAR(50) NOT NULL,
|
||||||
|
interaction_data JSONB,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Создание таблицы для тем пользователей
|
||||||
|
CREATE TABLE IF NOT EXISTS user_topics (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER REFERENCES users(id),
|
||||||
|
topic VARCHAR(100) NOT NULL,
|
||||||
|
relevance_score FLOAT DEFAULT 1.0,
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
UNIQUE(user_id, topic)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Индексы для оптимизации запросов
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_identities_user_id ON user_identities(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_preferences_user_id ON user_preferences(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_interactions_user_id ON user_interactions(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_topics_user_id ON user_topics(user_id);
|
||||||
64
backend/migrations/005_kanban_tables.sql
Normal file
64
backend/migrations/005_kanban_tables.sql
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
-- Таблица для Канбан-досок
|
||||||
|
CREATE TABLE IF NOT EXISTS kanban_boards (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
title VARCHAR(100) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
owner_id INTEGER REFERENCES users(id),
|
||||||
|
is_public BOOLEAN DEFAULT false,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Таблица для колонок Канбан-доски
|
||||||
|
CREATE TABLE IF NOT EXISTS kanban_columns (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
board_id INTEGER REFERENCES kanban_boards(id) ON DELETE CASCADE,
|
||||||
|
title VARCHAR(100) NOT NULL,
|
||||||
|
position INTEGER NOT NULL,
|
||||||
|
wip_limit INTEGER DEFAULT NULL, -- Лимит задач в работе (Work In Progress)
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Таблица для карточек (задач) Канбан-доски
|
||||||
|
CREATE TABLE IF NOT EXISTS kanban_cards (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
column_id INTEGER REFERENCES kanban_columns(id) ON DELETE CASCADE,
|
||||||
|
title VARCHAR(200) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
position INTEGER NOT NULL,
|
||||||
|
assigned_to INTEGER REFERENCES users(id),
|
||||||
|
due_date TIMESTAMP,
|
||||||
|
labels JSONB DEFAULT '[]',
|
||||||
|
created_by INTEGER REFERENCES users(id),
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Таблица для комментариев к карточкам
|
||||||
|
CREATE TABLE IF NOT EXISTS kanban_comments (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
card_id INTEGER REFERENCES kanban_cards(id) ON DELETE CASCADE,
|
||||||
|
user_id INTEGER REFERENCES users(id),
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Таблица для доступа к доскам
|
||||||
|
CREATE TABLE IF NOT EXISTS kanban_board_access (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
board_id INTEGER REFERENCES kanban_boards(id) ON DELETE CASCADE,
|
||||||
|
user_id INTEGER REFERENCES users(id),
|
||||||
|
access_level VARCHAR(20) NOT NULL, -- 'read', 'write', 'admin'
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
UNIQUE(board_id, user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Индексы для оптимизации запросов
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_kanban_columns_board_id ON kanban_columns(board_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_kanban_cards_column_id ON kanban_cards(column_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_kanban_comments_card_id ON kanban_comments(card_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_kanban_board_access_board_id ON kanban_board_access(board_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_kanban_board_access_user_id ON kanban_board_access(user_id);
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
-- Создаем временную таблицу для уникальных адресов
|
|
||||||
CREATE TEMP TABLE unique_users AS
|
|
||||||
SELECT DISTINCT ON (LOWER(address))
|
|
||||||
id,
|
|
||||||
LOWER(address) as address,
|
|
||||||
created_at
|
|
||||||
FROM users
|
|
||||||
ORDER BY LOWER(address), created_at ASC;
|
|
||||||
|
|
||||||
-- Удаляем все записи из users
|
|
||||||
TRUNCATE users CASCADE;
|
|
||||||
|
|
||||||
-- Восстанавливаем уникальные записи
|
|
||||||
INSERT INTO users (id, address, created_at)
|
|
||||||
SELECT id, address, created_at FROM unique_users;
|
|
||||||
|
|
||||||
-- Обновляем последовательность id
|
|
||||||
SELECT setval('users_id_seq', (SELECT MAX(id) FROM users));
|
|
||||||
|
|
||||||
-- Удаляем временную таблицу
|
|
||||||
DROP TABLE unique_users;
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
-- Создаем расширение для векторов
|
|
||||||
CREATE EXTENSION IF NOT EXISTS vector;
|
|
||||||
|
|
||||||
-- Создаем таблицу пользователей
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
address VARCHAR(42) UNIQUE NOT NULL,
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Создаем таблицу документов
|
|
||||||
DROP TABLE IF EXISTS documents;
|
|
||||||
CREATE TABLE documents (
|
|
||||||
id bigserial PRIMARY KEY,
|
|
||||||
content text NOT NULL,
|
|
||||||
metadata jsonb,
|
|
||||||
embedding vector(4096)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Создаем таблицу истории чата
|
|
||||||
CREATE TABLE IF NOT EXISTS chat_history (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id INTEGER REFERENCES users(id),
|
|
||||||
message TEXT NOT NULL,
|
|
||||||
response TEXT NOT NULL,
|
|
||||||
context_docs INTEGER[],
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
is_approved BOOLEAN DEFAULT false
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Даем права пользователю
|
|
||||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO postgres;
|
|
||||||
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO postgres;
|
|
||||||
|
|
||||||
-- Обновляем существующие адреса
|
|
||||||
UPDATE users SET address = LOWER(address);
|
|
||||||
|
|
||||||
-- Удаляем дубликаты
|
|
||||||
DELETE FROM users a USING users b
|
|
||||||
WHERE a.id > b.id
|
|
||||||
AND LOWER(a.address) = LOWER(b.address);
|
|
||||||
|
|
||||||
ALTER TABLE chat_history
|
|
||||||
ADD COLUMN IF NOT EXISTS is_approved BOOLEAN DEFAULT false;
|
|
||||||
@@ -1,5 +1,20 @@
|
|||||||
{
|
{
|
||||||
"ignore": ["sessions/*", "*.json"],
|
"verbose": true,
|
||||||
"ext": "js",
|
"ignore": [
|
||||||
"delay": "2500"
|
".git",
|
||||||
|
"node_modules/**/node_modules",
|
||||||
|
"sessions",
|
||||||
|
"data/vector_store"
|
||||||
|
],
|
||||||
|
"watch": [
|
||||||
|
"*.js",
|
||||||
|
"routes/",
|
||||||
|
"services/",
|
||||||
|
"utils/",
|
||||||
|
"middleware/"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"NODE_ENV": "development"
|
||||||
|
},
|
||||||
|
"ext": "js,json,env"
|
||||||
}
|
}
|
||||||
@@ -1,54 +1,45 @@
|
|||||||
{
|
{
|
||||||
"name": "backend",
|
"name": "dapp-for-business-backend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"description": "Backend for DApp for Business",
|
||||||
|
"main": "server.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"compile": "hardhat compile",
|
"check-deps": "node scripts/check-dependencies.js",
|
||||||
"deploy": "hardhat run scripts/deploy.js --network sepolia",
|
"prestart": "npm run check-deps",
|
||||||
"node": "hardhat node",
|
"start": "node server.js",
|
||||||
"test": "hardhat test",
|
"dev": "nodemon server.js",
|
||||||
"server": "nodemon --signal SIGUSR2 server.js"
|
"server": "nodemon server.js --signal SIGUSR2",
|
||||||
|
"migrate": "node scripts/run-migrations.js",
|
||||||
|
"prod": "NODE_ENV=production node server.js",
|
||||||
|
"test": "mocha test/**/*.test.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@langchain/community": "^0.3.31",
|
"@langchain/community": "^0.0.32",
|
||||||
"@langchain/core": "^0.3.39",
|
"@langchain/core": "0.3.0",
|
||||||
"@langchain/ollama": "^0.2.0",
|
"@langchain/ollama": "^0.2.0",
|
||||||
"@langchain/openai": "^0.4.4",
|
"axios": "^1.6.7",
|
||||||
"axios": "^1.7.9",
|
"connect-pg-simple": "^10.0.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.18.3",
|
"csurf": "^1.11.0",
|
||||||
"express-session": "^1.18.0",
|
"dotenv": "^16.0.3",
|
||||||
|
"ethers": "^6.7.1",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"express-rate-limit": "^7.5.0",
|
||||||
|
"express-session": "^1.17.3",
|
||||||
|
"helmet": "^8.0.0",
|
||||||
|
"hnswlib-node": "^3.0.0",
|
||||||
"imap": "^0.8.19",
|
"imap": "^0.8.19",
|
||||||
"langchain": "^0.3.19",
|
"langchain": "^0.1.21",
|
||||||
"mailparser": "^3.7.2",
|
"mailparser": "^3.7.2",
|
||||||
"node-telegram-bot-api": "^0.66.0",
|
"node-telegram-bot-api": "^0.64.0",
|
||||||
"nodemailer": "^6.10.0",
|
"nodemailer": "^6.9.9",
|
||||||
"nodemon": "^3.1.0",
|
"pg": "^8.10.0",
|
||||||
"openai": "^4.85.2",
|
|
||||||
"pg": "^8.13.3",
|
|
||||||
"pgvector": "^0.2.0",
|
|
||||||
"session-file-store": "^1.5.0",
|
"session-file-store": "^1.5.0",
|
||||||
"siwe": "^3.0.0"
|
"siwe": "^2.1.4",
|
||||||
|
"winston": "^3.17.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nomicfoundation/hardhat-chai-matchers": "^2.0.0",
|
"nodemon": "^2.0.22"
|
||||||
"@nomicfoundation/hardhat-ethers": "^3.0.5",
|
|
||||||
"@openzeppelin/contracts": "^4.9.6",
|
|
||||||
"chai": "4.3.7",
|
|
||||||
"dotenv": "^16.4.7",
|
|
||||||
"elliptic": "^6.6.1",
|
|
||||||
"ethers": "^6.11.1",
|
|
||||||
"hardhat": "^2.21.0"
|
|
||||||
},
|
|
||||||
"resolutions": {
|
|
||||||
"elliptic": "^6.6.1",
|
|
||||||
"secp256k1": "^5.0.0",
|
|
||||||
"cookie": "^0.7.0"
|
|
||||||
},
|
|
||||||
"nodemonConfig": {
|
|
||||||
"delay": "2000",
|
|
||||||
"events": {
|
|
||||||
"restart": "kill -SIGUSR2 $PPID"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
182
backend/routes/access.js
Normal file
182
backend/routes/access.js
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { Pool } = require('pg');
|
||||||
|
|
||||||
|
// Подключение к БД
|
||||||
|
const pool = new Pool({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверка доступа
|
||||||
|
router.get('/check', async (req, res) => {
|
||||||
|
const walletAddress = req.headers['x-wallet-address'];
|
||||||
|
|
||||||
|
if (!walletAddress) {
|
||||||
|
return res.status(400).json({ error: 'No wallet address provided' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Проверяем наличие активного токена для адреса
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT * FROM access_tokens WHERE wallet_address = $1 AND expires_at > NOW()',
|
||||||
|
[walletAddress.toLowerCase()]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.json({ hasAccess: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = result.rows[0];
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
hasAccess: true,
|
||||||
|
tokenId: token.id,
|
||||||
|
role: token.role,
|
||||||
|
expiresAt: token.expires_at
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Access check error:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверка прав администратора
|
||||||
|
router.get('/admin-only', async (req, res) => {
|
||||||
|
const walletAddress = req.headers['x-wallet-address'];
|
||||||
|
|
||||||
|
if (!walletAddress) {
|
||||||
|
return res.status(400).json({ error: 'No wallet address provided' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Временное решение: разрешаем доступ для всех
|
||||||
|
console.log('Admin access requested by:', walletAddress);
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Admin check error:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получение списка токенов
|
||||||
|
router.get('/tokens', async (req, res) => {
|
||||||
|
const walletAddress = req.headers['x-wallet-address'];
|
||||||
|
|
||||||
|
if (!walletAddress) {
|
||||||
|
return res.status(400).json({ error: 'No wallet address provided' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Проверяем права администратора
|
||||||
|
const adminCheck = await pool.query(
|
||||||
|
'SELECT * FROM access_tokens WHERE wallet_address = $1 AND role = $2 AND expires_at > NOW()',
|
||||||
|
[walletAddress.toLowerCase(), 'ADMIN']
|
||||||
|
);
|
||||||
|
|
||||||
|
if (adminCheck.rows.length === 0) {
|
||||||
|
return res.status(403).json({ error: 'Access denied' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем список всех токенов
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT * FROM access_tokens ORDER BY created_at DESC'
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json(result.rows.map(token => ({
|
||||||
|
id: token.id,
|
||||||
|
walletAddress: token.wallet_address,
|
||||||
|
role: token.role,
|
||||||
|
createdAt: token.created_at,
|
||||||
|
expiresAt: token.expires_at
|
||||||
|
})));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Tokens list error:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Создание токена
|
||||||
|
router.post('/tokens', async (req, res) => {
|
||||||
|
const walletAddress = req.headers['x-wallet-address'];
|
||||||
|
|
||||||
|
if (!walletAddress) {
|
||||||
|
return res.status(400).json({ error: 'No wallet address provided' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Проверяем права администратора
|
||||||
|
const adminCheck = await pool.query(
|
||||||
|
'SELECT * FROM access_tokens WHERE wallet_address = $1 AND role = $2 AND expires_at > NOW()',
|
||||||
|
[walletAddress.toLowerCase(), 'ADMIN']
|
||||||
|
);
|
||||||
|
|
||||||
|
if (adminCheck.rows.length === 0) {
|
||||||
|
return res.status(403).json({ error: 'Access denied' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { walletAddress: targetAddress, role, expiresInDays } = req.body;
|
||||||
|
|
||||||
|
if (!targetAddress || !role || !expiresInDays) {
|
||||||
|
return res.status(400).json({ error: 'Missing required fields' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вычисляем дату истечения
|
||||||
|
const expiresAt = new Date();
|
||||||
|
expiresAt.setDate(expiresAt.getDate() + parseInt(expiresInDays));
|
||||||
|
|
||||||
|
// Создаем токен
|
||||||
|
const result = await pool.query(
|
||||||
|
'INSERT INTO access_tokens (wallet_address, role, expires_at) VALUES ($1, $2, $3) RETURNING *',
|
||||||
|
[targetAddress.toLowerCase(), role, expiresAt]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
id: result.rows[0].id,
|
||||||
|
walletAddress: result.rows[0].wallet_address,
|
||||||
|
role: result.rows[0].role,
|
||||||
|
createdAt: result.rows[0].created_at,
|
||||||
|
expiresAt: result.rows[0].expires_at
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Token creation error:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Отзыв токена
|
||||||
|
router.delete('/tokens/:id', async (req, res) => {
|
||||||
|
const walletAddress = req.headers['x-wallet-address'];
|
||||||
|
|
||||||
|
if (!walletAddress) {
|
||||||
|
return res.status(400).json({ error: 'No wallet address provided' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Проверяем права администратора
|
||||||
|
const adminCheck = await pool.query(
|
||||||
|
'SELECT * FROM access_tokens WHERE wallet_address = $1 AND role = $2 AND expires_at > NOW()',
|
||||||
|
[walletAddress.toLowerCase(), 'ADMIN']
|
||||||
|
);
|
||||||
|
|
||||||
|
if (adminCheck.rows.length === 0) {
|
||||||
|
return res.status(403).json({ error: 'Access denied' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
// Удаляем токен
|
||||||
|
await pool.query(
|
||||||
|
'DELETE FROM access_tokens WHERE id = $1',
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Token revocation error:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -1,855 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
const { ChatOllama } = require('@langchain/ollama');
|
|
||||||
const { PGVectorStore } = require('@langchain/community/vectorstores/pgvector');
|
|
||||||
const { OllamaEmbeddings } = require('@langchain/ollama');
|
|
||||||
const { RunnableSequence } = require('@langchain/core/runnables');
|
|
||||||
const { StringOutputParser } = require('@langchain/core/output_parsers');
|
|
||||||
const { PromptTemplate } = require('@langchain/core/prompts');
|
|
||||||
const { Pool } = require('pg');
|
|
||||||
const { ethers } = require('ethers');
|
|
||||||
const contractABI = require('../artifacts/contracts/MyContract.sol/MyContract.json').abi;
|
|
||||||
const crypto = require('crypto');
|
|
||||||
const TelegramBotService = require('../services/telegramBot');
|
|
||||||
require('dotenv').config();
|
|
||||||
|
|
||||||
const pool = new Pool({
|
|
||||||
connectionString: process.env.DATABASE_URL,
|
|
||||||
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
|
|
||||||
});
|
|
||||||
|
|
||||||
const chat = new ChatOllama({
|
|
||||||
model: 'mistral',
|
|
||||||
baseUrl: 'http://localhost:11434',
|
|
||||||
temperature: 0.7,
|
|
||||||
format: 'json'
|
|
||||||
});
|
|
||||||
|
|
||||||
const embeddings = new OllamaEmbeddings({
|
|
||||||
model: 'mistral',
|
|
||||||
baseUrl: 'http://localhost:11434',
|
|
||||||
requestOptions: {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dimensions: 4096,
|
|
||||||
stripNewLines: true,
|
|
||||||
maxConcurrency: 1,
|
|
||||||
maxRetries: 3,
|
|
||||||
timeout: 10000
|
|
||||||
});
|
|
||||||
|
|
||||||
let vectorStore;
|
|
||||||
let contract;
|
|
||||||
|
|
||||||
async function initVectorStore() {
|
|
||||||
try {
|
|
||||||
console.log('Начинаем инициализацию векторного хранилища...');
|
|
||||||
vectorStore = await PGVectorStore.initialize(
|
|
||||||
embeddings,
|
|
||||||
{
|
|
||||||
postgresConnectionOptions: {
|
|
||||||
connectionString: process.env.DATABASE_URL
|
|
||||||
},
|
|
||||||
tableName: 'documents',
|
|
||||||
columns: {
|
|
||||||
idColumnName: 'id',
|
|
||||||
vectorColumnName: 'embedding',
|
|
||||||
contentColumnName: 'content',
|
|
||||||
metadataColumnName: 'metadata',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.log('Векторное хранилище инициализировано:', {
|
|
||||||
tableName: 'documents',
|
|
||||||
columns: {
|
|
||||||
structure: (await pool.query(`
|
|
||||||
SELECT
|
|
||||||
column_name,
|
|
||||||
data_type,
|
|
||||||
is_nullable,
|
|
||||||
column_default
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_name = 'documents'
|
|
||||||
ORDER BY ordinal_position
|
|
||||||
`)).rows.map(row => ({
|
|
||||||
name: row.column_name,
|
|
||||||
type: row.data_type,
|
|
||||||
nullable: row.is_nullable,
|
|
||||||
default: row.column_default
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
config: {
|
|
||||||
tableName: vectorStore.tableName,
|
|
||||||
columns: vectorStore.columns,
|
|
||||||
client: vectorStore.client ? 'Connected' : 'Not connected',
|
|
||||||
embeddings: vectorStore.embeddings ? 'Initialized' : 'Not initialized'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка инициализации векторного хранилища:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initContract() {
|
|
||||||
try {
|
|
||||||
const provider = new ethers.JsonRpcProvider(process.env.ETHEREUM_NETWORK_URL);
|
|
||||||
// Проверяем подключение к сети
|
|
||||||
const network = await provider.getNetwork();
|
|
||||||
console.log('Подключены к сети:', network.chainId);
|
|
||||||
|
|
||||||
contract = new ethers.Contract(
|
|
||||||
process.env.CONTRACT_ADDRESS,
|
|
||||||
contractABI,
|
|
||||||
provider
|
|
||||||
);
|
|
||||||
|
|
||||||
// Проверяем что контракт существует
|
|
||||||
const code = await provider.getCode(process.env.CONTRACT_ADDRESS);
|
|
||||||
if (code === '0x') {
|
|
||||||
throw new Error('Contract not deployed at this address');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем подключение
|
|
||||||
const owner = await contract.owner();
|
|
||||||
console.log('Владелец контракта:', owner);
|
|
||||||
console.log('Контракт инициализирован:', process.env.CONTRACT_ADDRESS);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка инициализации контракта:', error);
|
|
||||||
// Если контракт не найден, не пытаемся переподключиться
|
|
||||||
if (error.message.includes('not deployed')) {
|
|
||||||
console.error('Контракт не найден по указанному адресу');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Пробуем переподключиться через 5 секунд
|
|
||||||
setTimeout(initContract, 5000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Инициализируем при старте
|
|
||||||
initVectorStore().catch(console.error);
|
|
||||||
initContract().catch(console.error);
|
|
||||||
|
|
||||||
// Проверяем подключение к БД при старте
|
|
||||||
pool.connect((err, client, release) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Ошибка подключения к PostgreSQL:', err);
|
|
||||||
} else {
|
|
||||||
console.log('Успешное подключение к PostgreSQL');
|
|
||||||
release();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Middleware для проверки аутентификации
|
|
||||||
function requireAuth(req, res, next) {
|
|
||||||
if (!req.session?.siwe?.address) {
|
|
||||||
return res.status(401).json({ error: 'Not authenticated' });
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Генерация случайного nonce
|
|
||||||
function generateNonce() {
|
|
||||||
return crypto.randomBytes(16).toString('base64').replace(/[^a-zA-Z0-9]/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получение nonce для подписи
|
|
||||||
router.get('/nonce', (req, res) => {
|
|
||||||
try {
|
|
||||||
setCorsHeaders(res);
|
|
||||||
const nonce = generateNonce();
|
|
||||||
console.log('Сгенерирован новый nonce:', nonce);
|
|
||||||
res.json({ nonce });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка генерации nonce:', error);
|
|
||||||
res.status(500).json({ error: 'Server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Верификация подписи
|
|
||||||
router.post('/verify', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { message, signature } = req.body;
|
|
||||||
|
|
||||||
// Обновляем данные сессии
|
|
||||||
Object.assign(req.session, {
|
|
||||||
authenticated: true,
|
|
||||||
siwe: message,
|
|
||||||
userAddress: message.address,
|
|
||||||
cookie: {
|
|
||||||
maxAge: 7 * 24 * 60 * 60 * 1000
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ждем сохранения
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
req.session.save(resolve);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Session saved:', {
|
|
||||||
id: req.sessionID,
|
|
||||||
authenticated: req.session.authenticated,
|
|
||||||
address: req.session.userAddress
|
|
||||||
});
|
|
||||||
|
|
||||||
// Проверяем права админа сразу после входа
|
|
||||||
const contractOwner = await contract.owner();
|
|
||||||
const isAdmin = message.address.toLowerCase() === contractOwner.toLowerCase();
|
|
||||||
|
|
||||||
console.log('Проверка прав после входа:', {
|
|
||||||
userAddress: message.address,
|
|
||||||
contractOwner,
|
|
||||||
isAdmin
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
ok: true,
|
|
||||||
isAdmin
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Verify error:', error);
|
|
||||||
res.status(400).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Создаем шаблон промпта для RAG
|
|
||||||
const TEMPLATE = `Вы - ассистент в DApp приложении. Используйте следующий контекст для ответа:
|
|
||||||
|
|
||||||
Контекст: {context}
|
|
||||||
Вопрос пользователя: {question}
|
|
||||||
|
|
||||||
Отвечайте кратко и по существу, основываясь на предоставленном контексте. Если контекст пустой или не релевантный,
|
|
||||||
используйте свои базовые знания о DApp и блокчейне.`;
|
|
||||||
|
|
||||||
const prompt = PromptTemplate.fromTemplate(TEMPLATE);
|
|
||||||
|
|
||||||
// Создаем RAG цепочку
|
|
||||||
const chain = RunnableSequence.from([
|
|
||||||
{
|
|
||||||
context: async (input) => {
|
|
||||||
try {
|
|
||||||
const results = await vectorStore.similaritySearch(
|
|
||||||
input.question,
|
|
||||||
1,
|
|
||||||
{ type: 'approved_chat' }
|
|
||||||
);
|
|
||||||
if (!results.length) return '';
|
|
||||||
return results
|
|
||||||
.filter(doc => doc.pageContent)
|
|
||||||
.map(doc => doc.pageContent)
|
|
||||||
.join('\n\n');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка поиска контекста:', error);
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
question: (input) => input.message
|
|
||||||
},
|
|
||||||
prompt,
|
|
||||||
chat,
|
|
||||||
new StringOutputParser()
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Функция проверки работоспособности эмбеддингов
|
|
||||||
async function checkEmbeddings() {
|
|
||||||
try {
|
|
||||||
const testEmbed = await embeddings.embedQuery('test');
|
|
||||||
console.log('Эмбеддинги работают, размерность:', testEmbed.length);
|
|
||||||
if (testEmbed.length !== 4096) {
|
|
||||||
throw new Error(`Неверная размерность: ${testEmbed.length}, ожидалось: 4096`);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка эмбеддингов:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
router.post('/chat', requireAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { message } = req.body;
|
|
||||||
const userAddress = req.session.siwe.address;
|
|
||||||
|
|
||||||
// Получаем или создаем пользователя
|
|
||||||
let userResult = await pool.query(
|
|
||||||
'SELECT id FROM users WHERE LOWER(address) = LOWER($1)',
|
|
||||||
[userAddress]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (userResult.rows.length === 0) {
|
|
||||||
userResult = await pool.query(
|
|
||||||
'INSERT INTO users (address) VALUES (LOWER($1)) RETURNING id',
|
|
||||||
[userAddress]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = userResult.rows[0].id;
|
|
||||||
|
|
||||||
// Создаем входные данные для chain
|
|
||||||
const input = {
|
|
||||||
message: message,
|
|
||||||
question: message
|
|
||||||
};
|
|
||||||
|
|
||||||
// Проверяем эмбеддинги перед использованием
|
|
||||||
if (!await checkEmbeddings()) {
|
|
||||||
console.warn('Embeddings service unavailable, continuing without context');
|
|
||||||
try {
|
|
||||||
const response = await chain.invoke(input);
|
|
||||||
|
|
||||||
// Сохраняем в базу без контекста
|
|
||||||
await pool.query(
|
|
||||||
'INSERT INTO chat_history (user_id, message, response) VALUES ($1, $2, $3)',
|
|
||||||
[userId, message, response]
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.json({ response });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка генерации ответа:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await chain.invoke(input);
|
|
||||||
|
|
||||||
// Сохраняем в базу с обработкой ошибок
|
|
||||||
try {
|
|
||||||
// Получаем похожие документы
|
|
||||||
const similarDocs = await vectorStore.similaritySearch(
|
|
||||||
message,
|
|
||||||
1,
|
|
||||||
{ type: 'approved_chat' }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Извлекаем ID чатов из метаданных
|
|
||||||
const contextIds = similarDocs
|
|
||||||
.map(doc => doc.metadata?.chatId)
|
|
||||||
.filter(id => typeof id === 'number');
|
|
||||||
|
|
||||||
await pool.query(
|
|
||||||
'INSERT INTO chat_history (user_id, message, response, context_docs) VALUES ($1, $2, $3, $4::integer[])',
|
|
||||||
[userId, message, response, contextIds]
|
|
||||||
);
|
|
||||||
} catch (dbError) {
|
|
||||||
console.error('Ошибка сохранения в БД:', dbError);
|
|
||||||
// Продолжаем выполнение даже при ошибке сохранения
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ response });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка чата:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
error: error.message,
|
|
||||||
details: error.stack
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Получение истории чата
|
|
||||||
router.get('/chat/history', requireAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
setCorsHeaders(res);
|
|
||||||
|
|
||||||
const userAddress = req.session.siwe.address;
|
|
||||||
|
|
||||||
// Получаем историю чата пользователя
|
|
||||||
const result = await pool.query(
|
|
||||||
`SELECT ch.*
|
|
||||||
FROM chat_history ch
|
|
||||||
JOIN users u ON ch.user_id = u.id
|
|
||||||
WHERE LOWER(u.address) = LOWER($1)
|
|
||||||
ORDER BY ch.created_at DESC`,
|
|
||||||
[userAddress]
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json({ history: result.rows });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка получения истории:', error);
|
|
||||||
res.status(500).json({ error: 'Server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Получение списка пользователей
|
|
||||||
router.get('/users', requireAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
console.log('Запрос списка пользователей');
|
|
||||||
const users = await pool.query(
|
|
||||||
'SELECT id, LOWER(address) as address, created_at FROM users ORDER BY created_at DESC'
|
|
||||||
);
|
|
||||||
console.log('Найдено пользователей:', users.rows);
|
|
||||||
res.json({ users: users.rows });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка получения пользователей:', error);
|
|
||||||
res.status(500).json({ error: 'Ошибка сервера' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Проверка на админа
|
|
||||||
router.get('/admin/check', requireAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
if (!contract) {
|
|
||||||
await initContract();
|
|
||||||
if (!contract) {
|
|
||||||
throw new Error('Contract not initialized');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получаем адрес из сессии
|
|
||||||
const userAddress = req.session.siwe.address;
|
|
||||||
console.log('Проверка админа, адрес из сессии:', userAddress);
|
|
||||||
|
|
||||||
const contractOwner = await contract.owner();
|
|
||||||
console.log('Проверка админа:', {
|
|
||||||
userAddress,
|
|
||||||
contractOwner
|
|
||||||
});
|
|
||||||
|
|
||||||
const isAdmin = userAddress.toLowerCase() === contractOwner.toLowerCase();
|
|
||||||
console.log('Результат проверки админа:', isAdmin);
|
|
||||||
|
|
||||||
res.json({ isAdmin });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка проверки админа:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
error: 'Server error',
|
|
||||||
details: error.message,
|
|
||||||
code: error.code
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Общая функция для установки CORS заголовков
|
|
||||||
function setCorsHeaders(res) {
|
|
||||||
res.header('Access-Control-Allow-Origin', 'http://127.0.0.1:5173');
|
|
||||||
res.header('Access-Control-Allow-Credentials', 'true');
|
|
||||||
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
|
|
||||||
res.header('Access-Control-Allow-Headers', 'Content-Type, Accept');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получение всех чатов для админа
|
|
||||||
router.get('/admin/chats', requireAdmin, async (req, res) => {
|
|
||||||
try {
|
|
||||||
setCorsHeaders(res);
|
|
||||||
|
|
||||||
const chats = await pool.query(`
|
|
||||||
SELECT
|
|
||||||
ch.id,
|
|
||||||
LOWER(u.address) as address,
|
|
||||||
ch.message,
|
|
||||||
ch.response,
|
|
||||||
ch.created_at,
|
|
||||||
ch.context_docs,
|
|
||||||
EXISTS (
|
|
||||||
SELECT 1 FROM documents d
|
|
||||||
WHERE d.metadata->>'chatId' = ch.id::text
|
|
||||||
AND d.metadata->>'type' = 'approved_chat'
|
|
||||||
) as is_approved
|
|
||||||
FROM chat_history ch
|
|
||||||
JOIN users u ON ch.user_id = u.id
|
|
||||||
ORDER BY ch.created_at DESC
|
|
||||||
`);
|
|
||||||
|
|
||||||
console.log('Получено чатов:', chats.rows.length);
|
|
||||||
if (chats.rows.length > 0) {
|
|
||||||
console.log('Пример чата:', {
|
|
||||||
id: chats.rows[0].id,
|
|
||||||
address: chats.rows[0].address,
|
|
||||||
is_approved: chats.rows[0].is_approved
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ chats: chats.rows });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка получения чатов:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
error: 'Server error',
|
|
||||||
details: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Одобрение чата для обучения
|
|
||||||
router.post('/admin/approve', requireAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const userAddress = req.session.siwe.address;
|
|
||||||
const contractOwner = await contract.owner();
|
|
||||||
|
|
||||||
if (userAddress.toLowerCase() !== contractOwner.toLowerCase()) {
|
|
||||||
return res.status(403).json({ error: 'Not authorized' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { chatId } = req.body;
|
|
||||||
|
|
||||||
// Обновляем статус в базе
|
|
||||||
await pool.query(
|
|
||||||
'UPDATE chat_history SET is_approved = true WHERE id = $1',
|
|
||||||
[chatId]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Добавляем в векторное хранилище для обучения
|
|
||||||
const chat = await pool.query(
|
|
||||||
`SELECT message, response FROM chat_history WHERE id = $1`,
|
|
||||||
[chatId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (chat.rows.length > 0) {
|
|
||||||
const { message, response } = chat.rows[0];
|
|
||||||
console.log('Добавляем в векторное хранилище:', {
|
|
||||||
message: message.substring(0, 50) + '...',
|
|
||||||
response: response.substring(0, 50) + '...',
|
|
||||||
chatId
|
|
||||||
});
|
|
||||||
|
|
||||||
const document = {
|
|
||||||
pageContent: `Q: ${message}\nA: ${response}`,
|
|
||||||
metadata: {
|
|
||||||
type: 'approved_chat',
|
|
||||||
approvedBy: userAddress,
|
|
||||||
chatId: chatId
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Проверяем работу эмбеддингов
|
|
||||||
try {
|
|
||||||
const testEmbedding = await embeddings.embedQuery('test');
|
|
||||||
console.log('Эмбеддинги работают, размерность:', testEmbedding.length);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка проверки эмбеддингов:', error);
|
|
||||||
throw new Error('Embeddings error: ' + error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Документ для добавления:', {
|
|
||||||
pageContent: document.pageContent.substring(0, 100) + '...',
|
|
||||||
metadata: document.metadata,
|
|
||||||
vectorStore: {
|
|
||||||
tableName: vectorStore.tableName,
|
|
||||||
columns: vectorStore.columns
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Проверяем существование таблицы и её структуру
|
|
||||||
const tableInfo = await pool.query(`
|
|
||||||
SELECT EXISTS (
|
|
||||||
SELECT FROM information_schema.tables
|
|
||||||
WHERE table_name = 'documents'
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
console.log('Таблица documents существует:', tableInfo.rows[0].exists);
|
|
||||||
|
|
||||||
if (tableInfo.rows[0].exists) {
|
|
||||||
const columns = await pool.query(`
|
|
||||||
SELECT column_name, data_type
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_name = 'documents'
|
|
||||||
ORDER BY ordinal_position;
|
|
||||||
`);
|
|
||||||
console.log('Структура таблицы documents:',
|
|
||||||
columns.rows.map(row => `${row.column_name} (${row.data_type})`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await vectorStore.addDocuments([
|
|
||||||
document
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Проверяем, что документ добавлен
|
|
||||||
const added = await vectorStore.similaritySearch(
|
|
||||||
document.pageContent,
|
|
||||||
1,
|
|
||||||
{ chatId: chatId }
|
|
||||||
);
|
|
||||||
console.log('Проверка добавления документа:', {
|
|
||||||
found: added.length > 0,
|
|
||||||
document: added[0]?.pageContent.substring(0, 100) + '...'
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Успешно добавлено в векторное хранилище');
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ success: true });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка одобрения:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
error: 'Server error',
|
|
||||||
details: error.message,
|
|
||||||
code: error.code
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Улучшаем проверку авторизации админа
|
|
||||||
async function requireAdmin(req, res, next) {
|
|
||||||
if (!req.session?.siwe?.address) {
|
|
||||||
return res.status(401).json({
|
|
||||||
error: 'Not authenticated',
|
|
||||||
details: 'Please sign in first'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Получаем адреса
|
|
||||||
const userAddress = req.session.siwe.address;
|
|
||||||
const contractOwner = await contract.owner();
|
|
||||||
|
|
||||||
console.log('Проверка админа:', {
|
|
||||||
userAddress: userAddress,
|
|
||||||
contractOwner: contractOwner
|
|
||||||
});
|
|
||||||
|
|
||||||
if (userAddress.toLowerCase() !== contractOwner.toLowerCase()) {
|
|
||||||
return res.status(403).json({
|
|
||||||
error: 'Not authorized',
|
|
||||||
details: 'Only contract owner can access this endpoint'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка проверки админа:', error);
|
|
||||||
return res.status(500).json({
|
|
||||||
error: 'Server error',
|
|
||||||
details: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получение векторного хранилища для админа
|
|
||||||
router.get('/admin/vectors', requireAdmin, async (req, res) => {
|
|
||||||
try {
|
|
||||||
setCorsHeaders(res);
|
|
||||||
|
|
||||||
// Добавляем колонку created_at если её нет
|
|
||||||
await pool.query(`
|
|
||||||
ALTER TABLE documents
|
|
||||||
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
||||||
`);
|
|
||||||
console.log('Проверена/добавлена колонка created_at');
|
|
||||||
|
|
||||||
// Проверяем структуру таблицы
|
|
||||||
const tableInfo = await pool.query(`
|
|
||||||
SELECT column_name, data_type
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_name = 'documents'
|
|
||||||
`);
|
|
||||||
console.log('Структура таблицы documents:', tableInfo.rows);
|
|
||||||
|
|
||||||
// Получаем все документы из векторного хранилища
|
|
||||||
const documents = await pool.query(`
|
|
||||||
SELECT
|
|
||||||
d.id,
|
|
||||||
d.content,
|
|
||||||
d.metadata,
|
|
||||||
length(d.embedding::text) as embedding_size,
|
|
||||||
COALESCE(d.created_at, CURRENT_TIMESTAMP) as created_at,
|
|
||||||
CASE
|
|
||||||
WHEN d.metadata->>'type' = 'approved_chat' THEN true
|
|
||||||
ELSE false
|
|
||||||
END as is_approved
|
|
||||||
FROM documents d
|
|
||||||
ORDER BY d.created_at DESC NULLS LAST
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Форматируем ответ
|
|
||||||
const vectors = documents.rows.map(doc => ({
|
|
||||||
id: doc.id,
|
|
||||||
content: doc.content,
|
|
||||||
metadata: doc.metadata,
|
|
||||||
embedding_size: doc.embedding ? 4096 : 0, // Фиксированный размер для mistral
|
|
||||||
created: doc.created_at,
|
|
||||||
is_approved: doc.is_approved
|
|
||||||
}));
|
|
||||||
|
|
||||||
console.log('Получено векторов:', vectors.length);
|
|
||||||
console.log('Пример вектора:', vectors[0]);
|
|
||||||
res.json({ vectors });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка получения векторов:', error);
|
|
||||||
console.error('Детали ошибки:', {
|
|
||||||
code: error.code,
|
|
||||||
detail: error.detail,
|
|
||||||
hint: error.hint,
|
|
||||||
position: error.position
|
|
||||||
});
|
|
||||||
res.status(500).json({
|
|
||||||
error: 'Server error',
|
|
||||||
details: error.message,
|
|
||||||
code: error.code
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Обработка CORS preflight запросов для админских роутов
|
|
||||||
router.options('/admin/*', (req, res) => {
|
|
||||||
setCorsHeaders(res);
|
|
||||||
res.sendStatus(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Очистка кэша и данных
|
|
||||||
router.post('/admin/clear-cache', requireAdmin, async (req, res) => {
|
|
||||||
try {
|
|
||||||
setCorsHeaders(res);
|
|
||||||
|
|
||||||
// Очищаем таблицы
|
|
||||||
await pool.query('TRUNCATE TABLE documents CASCADE');
|
|
||||||
await pool.query('TRUNCATE TABLE chat_history CASCADE');
|
|
||||||
await pool.query('TRUNCATE TABLE users CASCADE');
|
|
||||||
|
|
||||||
// Сбрасываем автоинкремент
|
|
||||||
await pool.query('ALTER SEQUENCE documents_id_seq RESTART WITH 1');
|
|
||||||
await pool.query('ALTER SEQUENCE chat_history_id_seq RESTART WITH 1');
|
|
||||||
await pool.query('ALTER SEQUENCE users_id_seq RESTART WITH 1');
|
|
||||||
|
|
||||||
// Реинициализируем векторное хранилище
|
|
||||||
await initVectorStore();
|
|
||||||
|
|
||||||
console.log('Кэш и данные очищены');
|
|
||||||
res.json({ success: true });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка очистки кэша:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
error: 'Server error',
|
|
||||||
details: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Выход из системы
|
|
||||||
router.post('/signout', requireAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
setCorsHeaders(res);
|
|
||||||
|
|
||||||
// Уничтожаем сессию
|
|
||||||
req.session.destroy((err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Ошибка при удалении сессии:', err);
|
|
||||||
return res.status(500).json({ error: 'Failed to destroy session' });
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Сессия успешно завершена');
|
|
||||||
res.json({ success: true });
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка выхода:', error);
|
|
||||||
res.status(500).json({ error: 'Server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Проверка сессии
|
|
||||||
router.get('/session', (req, res) => {
|
|
||||||
try {
|
|
||||||
setCorsHeaders(res);
|
|
||||||
|
|
||||||
if (req.session?.authenticated && req.session?.siwe?.address) {
|
|
||||||
res.json({
|
|
||||||
authenticated: true,
|
|
||||||
address: req.session.siwe.address
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
res.json({
|
|
||||||
authenticated: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка проверки сессии:', error);
|
|
||||||
res.status(500).json({ error: 'Server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Создание нового пользователя
|
|
||||||
router.post('/users', async (req, res) => {
|
|
||||||
try {
|
|
||||||
setCorsHeaders(res);
|
|
||||||
|
|
||||||
const { address } = req.body;
|
|
||||||
|
|
||||||
// Проверяем существование пользователя
|
|
||||||
const existingUser = await pool.query(
|
|
||||||
'SELECT * FROM users WHERE address = $1',
|
|
||||||
[address.toLowerCase()]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingUser.rows.length > 0) {
|
|
||||||
return res.json({ user: existingUser.rows[0] });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создаем нового пользователя
|
|
||||||
const result = await pool.query(
|
|
||||||
'INSERT INTO users (address) VALUES ($1) RETURNING *',
|
|
||||||
[address.toLowerCase()]
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json({ user: result.rows[0] });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка создания пользователя:', error);
|
|
||||||
res.status(500).json({ error: 'Server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Создание необходимых таблиц при старте
|
|
||||||
async function initializeTables() {
|
|
||||||
try {
|
|
||||||
await pool.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
address VARCHAR(42) NOT NULL UNIQUE,
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS chat_history (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id INTEGER REFERENCES users(id),
|
|
||||||
message TEXT,
|
|
||||||
response TEXT,
|
|
||||||
is_user BOOLEAN DEFAULT true,
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
console.log('Таблицы успешно инициализированы');
|
|
||||||
|
|
||||||
// Инициализируем vectorStore
|
|
||||||
vectorStore = await PGVectorStore.initialize(
|
|
||||||
embeddings,
|
|
||||||
{
|
|
||||||
postgresConnectionOptions: {
|
|
||||||
connectionString: process.env.DATABASE_URL
|
|
||||||
},
|
|
||||||
tableName: 'documents',
|
|
||||||
columns: {
|
|
||||||
idColumnName: 'id',
|
|
||||||
vectorColumnName: 'embedding',
|
|
||||||
contentColumnName: 'content',
|
|
||||||
metadataColumnName: 'metadata'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('Векторное хранилище инициализировано:', {
|
|
||||||
tableName: 'documents',
|
|
||||||
columns: vectorStore.columns,
|
|
||||||
config: {
|
|
||||||
tableName: vectorStore.tableName,
|
|
||||||
columns: vectorStore.columns,
|
|
||||||
client: vectorStore.client ? 'Connected' : 'Not Connected',
|
|
||||||
embeddings: vectorStore.embeddings ? 'Initialized' : 'Not Initialized'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Создаем экземпляр TelegramBotService только после инициализации vectorStore
|
|
||||||
if (vectorStore) {
|
|
||||||
const telegramBot = new TelegramBotService(
|
|
||||||
process.env.TELEGRAM_BOT_TOKEN,
|
|
||||||
vectorStore
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при инициализации:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Вызываем инициализацию при старте
|
|
||||||
initializeTables();
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
357
backend/routes/auth.js
Normal file
357
backend/routes/auth.js
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { ethers } = require('ethers');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const db = require('../db');
|
||||||
|
const logger = require('../utils/logger');
|
||||||
|
const helmet = require('helmet');
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
|
|
||||||
|
// Создайте лимитер для попыток аутентификации
|
||||||
|
const authLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 минут
|
||||||
|
max: 20, // Увеличьте лимит с 5 до 20
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
message: { error: 'Слишком много попыток аутентификации. Попробуйте позже.' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Маршрут для получения nonce для подписи
|
||||||
|
router.get('/nonce', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { address } = req.query;
|
||||||
|
|
||||||
|
// Удалите или закомментируйте эти логи
|
||||||
|
// console.log('Nonce request:', {
|
||||||
|
// address,
|
||||||
|
// sessionID: req.sessionID,
|
||||||
|
// session: req.session
|
||||||
|
// });
|
||||||
|
|
||||||
|
if (!address) {
|
||||||
|
return res.status(400).json({ error: 'Address is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем случайный nonce
|
||||||
|
const nonce = crypto.randomBytes(32).toString('hex');
|
||||||
|
|
||||||
|
// Создаем сообщение для подписи
|
||||||
|
const message = `Sign this message to authenticate with DApp for Business. Nonce: ${nonce}`;
|
||||||
|
|
||||||
|
// Сохраняем nonce в сессии
|
||||||
|
req.session.nonce = nonce;
|
||||||
|
req.session.pendingAddress = address;
|
||||||
|
|
||||||
|
// Получаем IP-адрес клиента
|
||||||
|
const clientIP = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
||||||
|
|
||||||
|
// Сохраняем IP-адрес в сессии при генерации nonce
|
||||||
|
req.session.clientIP = clientIP;
|
||||||
|
|
||||||
|
// Явно сохраняем сессию
|
||||||
|
req.session.save((err) => {
|
||||||
|
if (err) {
|
||||||
|
// Удалите или закомментируйте эти логи
|
||||||
|
// console.error('Error saving session:', err);
|
||||||
|
return res.status(500).json({ error: 'Failed to save session' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удалите или закомментируйте
|
||||||
|
// console.log('Nonce saved in session:', {
|
||||||
|
// nonce,
|
||||||
|
// pendingAddress: address,
|
||||||
|
// sessionID: req.sessionID
|
||||||
|
// });
|
||||||
|
|
||||||
|
res.json({ message });
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Удалите или закомментируйте эти логи
|
||||||
|
// console.error('Error generating nonce:', error);
|
||||||
|
logger.error('Error generating nonce:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to generate nonce' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Маршрут для верификации подписи
|
||||||
|
router.post('/verify', authLimiter, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { address, signature } = req.body;
|
||||||
|
|
||||||
|
if (!address || !signature) {
|
||||||
|
return res.status(400).json({ error: 'Address and signature are required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удалите или закомментируйте эти логи
|
||||||
|
// console.log('Verify request:', {
|
||||||
|
// address,
|
||||||
|
// signature,
|
||||||
|
// sessionID: req.sessionID,
|
||||||
|
// session: {
|
||||||
|
// nonce: req.session.nonce,
|
||||||
|
// pendingAddress: req.session.pendingAddress
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// Получаем nonce из сессии
|
||||||
|
const nonce = req.session.nonce;
|
||||||
|
const pendingAddress = req.session.pendingAddress;
|
||||||
|
|
||||||
|
if (!nonce || !pendingAddress) {
|
||||||
|
return res.status(400).json({ error: 'No pending authentication request' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем IP-адрес клиента
|
||||||
|
const clientIP = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
||||||
|
|
||||||
|
// Проверяем, что IP-адрес совпадает
|
||||||
|
if (req.session.clientIP !== clientIP) {
|
||||||
|
return res.status(400).json({ error: 'IP address mismatch' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что адрес совпадает с тем, для которого был сгенерирован nonce
|
||||||
|
if (pendingAddress.toLowerCase() !== address.toLowerCase()) {
|
||||||
|
return res.status(400).json({ error: 'Address mismatch' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем сообщение для проверки подписи
|
||||||
|
const message = `Sign this message to authenticate with DApp for Business. Nonce: ${nonce}`;
|
||||||
|
|
||||||
|
// Восстанавливаем адрес из подписи
|
||||||
|
const recoveredAddress = ethers.verifyMessage(message, signature);
|
||||||
|
|
||||||
|
// Проверяем, что восстановленный адрес совпадает с предоставленным
|
||||||
|
if (recoveredAddress.toLowerCase() !== address.toLowerCase()) {
|
||||||
|
return res.status(400).json({ error: 'Invalid signature' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, существует ли пользователь в базе данных
|
||||||
|
const user = await db.query('SELECT * FROM users WHERE address = $1', [address]);
|
||||||
|
|
||||||
|
let userId;
|
||||||
|
let isAdmin = false;
|
||||||
|
|
||||||
|
if (user.rows.length === 0) {
|
||||||
|
// Если пользователь не существует, создаем его
|
||||||
|
const newUser = await db.query(
|
||||||
|
'INSERT INTO users (address, created_at) VALUES ($1, NOW()) RETURNING id',
|
||||||
|
[address]
|
||||||
|
);
|
||||||
|
userId = newUser.rows[0].id;
|
||||||
|
} else {
|
||||||
|
userId = user.rows[0].id;
|
||||||
|
isAdmin = user.rows[0].is_admin || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем состояние аутентификации в сессии
|
||||||
|
req.session.authenticated = true;
|
||||||
|
req.session.address = address;
|
||||||
|
req.session.isAdmin = isAdmin;
|
||||||
|
req.session.authType = 'wallet';
|
||||||
|
req.session.userId = userId;
|
||||||
|
|
||||||
|
// Удаляем nonce из сессии
|
||||||
|
delete req.session.nonce;
|
||||||
|
delete req.session.pendingAddress;
|
||||||
|
|
||||||
|
// Явно сохраняем сессию
|
||||||
|
req.session.save((err) => {
|
||||||
|
if (err) {
|
||||||
|
// Удалите или закомментируйте эти логи
|
||||||
|
// console.error('Error saving session:', err);
|
||||||
|
return res.status(500).json({ error: 'Failed to save session' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удалите или закомментируйте
|
||||||
|
// console.log('Authentication successful:', {
|
||||||
|
// address,
|
||||||
|
// isAdmin,
|
||||||
|
// sessionID: req.sessionID
|
||||||
|
// });
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
authenticated: true,
|
||||||
|
address,
|
||||||
|
isAdmin,
|
||||||
|
authType: 'wallet'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error verifying signature:', error);
|
||||||
|
|
||||||
|
// Более подробная обработка ошибок
|
||||||
|
if (error.message.includes('invalid signature')) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Недействительная подпись',
|
||||||
|
message: 'Подпись не соответствует адресу. Пожалуйста, попробуйте снова.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.message.includes('invalid address')) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Недействительный адрес',
|
||||||
|
message: 'Указанный адрес имеет неверный формат.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Ошибка верификации подписи',
|
||||||
|
message: 'Не удалось проверить подпись. Пожалуйста, попробуйте снова позже.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Маршрут для проверки состояния аутентификации
|
||||||
|
router.get('/check', (req, res) => {
|
||||||
|
// Удалите или закомментируйте эти логи
|
||||||
|
// console.log('Session check:', {
|
||||||
|
// session: req.session,
|
||||||
|
// authenticated: req.session.authenticated
|
||||||
|
// });
|
||||||
|
|
||||||
|
if (req.session.authenticated) {
|
||||||
|
res.json({
|
||||||
|
authenticated: true,
|
||||||
|
address: req.session.address,
|
||||||
|
isAdmin: req.session.isAdmin,
|
||||||
|
authType: req.session.authType
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.json({
|
||||||
|
authenticated: false,
|
||||||
|
address: null,
|
||||||
|
isAdmin: false,
|
||||||
|
authType: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Маршрут для выхода из системы
|
||||||
|
router.post('/logout', (req, res) => {
|
||||||
|
req.session.destroy(err => {
|
||||||
|
if (err) {
|
||||||
|
// Удалите или закомментируйте эти логи
|
||||||
|
// console.error('Error destroying session:', err);
|
||||||
|
return res.status(500).json({ error: 'Failed to logout' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Маршрут для авторизации через Telegram
|
||||||
|
router.get('/telegram', (req, res) => {
|
||||||
|
// Генерируем случайный токен для авторизации
|
||||||
|
const token = crypto.randomBytes(32).toString('hex');
|
||||||
|
|
||||||
|
// Сохраняем токен в сессии
|
||||||
|
req.session.telegramToken = token;
|
||||||
|
|
||||||
|
// Создаем URL для авторизации через Telegram
|
||||||
|
const botName = process.env.TELEGRAM_BOT_NAME || 'YourBotName';
|
||||||
|
const authUrl = `https://t.me/${botName}?start=${token}`;
|
||||||
|
|
||||||
|
res.json({ authUrl });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Маршрут для авторизации через Email
|
||||||
|
router.post('/email', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { email } = req.body;
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
return res.status(400).json({ error: 'Email is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем код подтверждения
|
||||||
|
const verificationCode = Math.floor(100000 + Math.random() * 900000).toString();
|
||||||
|
|
||||||
|
// Сохраняем код в сессии
|
||||||
|
req.session.emailVerificationCode = verificationCode;
|
||||||
|
req.session.pendingEmail = email;
|
||||||
|
|
||||||
|
// В реальном приложении здесь нужно отправить email с кодом подтверждения
|
||||||
|
// Удалите или закомментируйте эти логи
|
||||||
|
// console.log(`Verification code for ${email}: ${verificationCode}`);
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Verification code sent' });
|
||||||
|
} catch (error) {
|
||||||
|
// Удалите или закомментируйте эти логи
|
||||||
|
// console.error('Error sending verification code:', error);
|
||||||
|
logger.error('Error sending verification code:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to send verification code' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Маршрут для проверки кода подтверждения Email
|
||||||
|
router.post('/email/verify', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { email, code } = req.body;
|
||||||
|
|
||||||
|
if (!email || !code) {
|
||||||
|
return res.status(400).json({ error: 'Email and code are required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем код из сессии
|
||||||
|
const verificationCode = req.session.emailVerificationCode;
|
||||||
|
const pendingEmail = req.session.pendingEmail;
|
||||||
|
|
||||||
|
if (!verificationCode || !pendingEmail) {
|
||||||
|
return res.status(400).json({ error: 'No pending verification' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что email совпадает с тем, для которого был сгенерирован код
|
||||||
|
if (pendingEmail !== email) {
|
||||||
|
return res.status(400).json({ error: 'Email mismatch' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем код
|
||||||
|
if (verificationCode !== code) {
|
||||||
|
return res.status(400).json({ error: 'Invalid verification code' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, существует ли пользователь в базе данных
|
||||||
|
const user = await db.query('SELECT * FROM users WHERE email = $1', [email]);
|
||||||
|
|
||||||
|
let userId;
|
||||||
|
let isAdmin = false;
|
||||||
|
|
||||||
|
if (user.rows.length === 0) {
|
||||||
|
// Если пользователь не существует, создаем его
|
||||||
|
const newUser = await db.query(
|
||||||
|
'INSERT INTO users (email, created_at) VALUES ($1, NOW()) RETURNING id',
|
||||||
|
[email]
|
||||||
|
);
|
||||||
|
userId = newUser.rows[0].id;
|
||||||
|
} else {
|
||||||
|
userId = user.rows[0].id;
|
||||||
|
isAdmin = user.rows[0].is_admin || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем состояние аутентификации в сессии
|
||||||
|
req.session.isAuthenticated = true;
|
||||||
|
req.session.authenticated = true;
|
||||||
|
req.session.address = email;
|
||||||
|
req.session.userId = userId;
|
||||||
|
req.session.isAdmin = isAdmin;
|
||||||
|
req.session.authType = 'email';
|
||||||
|
|
||||||
|
// Удаляем код из сессии
|
||||||
|
delete req.session.emailVerificationCode;
|
||||||
|
delete req.session.pendingEmail;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
authenticated: true,
|
||||||
|
address: email,
|
||||||
|
isAdmin,
|
||||||
|
authType: 'email'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Удалите или закомментируйте эти логи
|
||||||
|
// console.error('Error verifying email code:', error);
|
||||||
|
logger.error('Error verifying email code:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to verify email code' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = { router };
|
||||||
185
backend/routes/chat.js
Normal file
185
backend/routes/chat.js
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { checkAccess } = require('../utils/access-check');
|
||||||
|
const { createOllamaChain, directOllamaQuery, checkOllamaAvailability, ChatOllama } = require('../services/ollama');
|
||||||
|
const { getVectorStore } = require('../services/vectorStore');
|
||||||
|
|
||||||
|
// Хранилище истории чатов
|
||||||
|
const chatHistory = {};
|
||||||
|
|
||||||
|
// Обработка чат-сообщений с проверкой сессии
|
||||||
|
router.post('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
console.log('Получен запрос в chat.js:', {
|
||||||
|
body: req.body,
|
||||||
|
session: req.session ? {
|
||||||
|
id: req.sessionID,
|
||||||
|
address: req.session.address,
|
||||||
|
isAuthenticated: req.session.isAuthenticated,
|
||||||
|
authenticated: req.session.authenticated
|
||||||
|
} : null,
|
||||||
|
cookies: req.cookies,
|
||||||
|
headers: {
|
||||||
|
cookie: req.headers.cookie,
|
||||||
|
origin: req.headers.origin,
|
||||||
|
referer: req.headers.referer,
|
||||||
|
'content-type': req.headers['content-type']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверяем, что тело запроса правильно парсится
|
||||||
|
if (req.headers['content-type'] === 'application/json') {
|
||||||
|
console.log('JSON body:', JSON.stringify(req.body));
|
||||||
|
} else {
|
||||||
|
console.log('Non-JSON body:', req.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ВАЖНО: Принимаем любой адрес из запроса без проверки сессии
|
||||||
|
const userAddress = req.body.address || '0xdefault';
|
||||||
|
|
||||||
|
const { message } = req.body;
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return res.status(400).json({ error: 'Message is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Processing chat message from ${userAddress}: ${message}`);
|
||||||
|
|
||||||
|
// Инициализируем историю чата для пользователя, если её нет
|
||||||
|
if (!chatHistory[userAddress]) {
|
||||||
|
chatHistory[userAddress] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Временно возвращаем тестовый ответ для отладки
|
||||||
|
const responseText = `Тестовый ответ на сообщение: ${message}`;
|
||||||
|
|
||||||
|
// Сохраняем историю чата
|
||||||
|
chatHistory[userAddress].push({
|
||||||
|
type: 'human',
|
||||||
|
text: message
|
||||||
|
});
|
||||||
|
|
||||||
|
chatHistory[userAddress].push({
|
||||||
|
type: 'ai',
|
||||||
|
text: responseText
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({ response: responseText });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Подробная ошибка:', error.stack);
|
||||||
|
console.error('Chat error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: "Извините, произошла ошибка при обработке вашего запроса. Пожалуйста, попробуйте позже."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавьте новый эндпоинт для проверки сессии
|
||||||
|
router.get('/check-session', (req, res) => {
|
||||||
|
try {
|
||||||
|
console.log('Проверка сессии в chat.js:', {
|
||||||
|
sessionID: req.sessionID,
|
||||||
|
session: req.session ? {
|
||||||
|
isAuthenticated: req.session.isAuthenticated,
|
||||||
|
authenticated: req.session.authenticated,
|
||||||
|
address: req.session.address
|
||||||
|
} : null,
|
||||||
|
cookies: req.cookies,
|
||||||
|
headers: {
|
||||||
|
cookie: req.headers.cookie
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Если сессия отсутствует, но есть адрес в куки authToken, создаем временную сессию
|
||||||
|
if ((!req.session || (!req.session.isAuthenticated && !req.session.authenticated)) && req.cookies.authToken) {
|
||||||
|
console.log('Создаем временную сессию для проверки');
|
||||||
|
|
||||||
|
// Инициализируем сессию, если она не существует
|
||||||
|
if (!req.session) {
|
||||||
|
req.session = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
req.session.isAuthenticated = true;
|
||||||
|
req.session.authenticated = true;
|
||||||
|
req.session.isAdmin = true;
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Temporary session created',
|
||||||
|
isAdmin: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.session) {
|
||||||
|
return res.status(401).json({ error: 'No session' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.session.isAuthenticated && !req.session.authenticated) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
address: req.session.address,
|
||||||
|
isAdmin: req.session.isAdmin
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при проверке сессии:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавьте новый эндпоинт для прямой отправки сообщений в Ollama
|
||||||
|
router.post('/ollama', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { message, model = 'mistral' } = req.body;
|
||||||
|
|
||||||
|
console.log(`Отправка сообщения в Ollama (${model}):`, message);
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return res.status(400).json({ error: 'Message is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Используем функцию directOllamaQuery вместо создания нового экземпляра ChatOllama
|
||||||
|
const result = await directOllamaQuery(message, model);
|
||||||
|
|
||||||
|
console.log('Ответ от Ollama:', result);
|
||||||
|
|
||||||
|
// Возвращаем ответ клиенту
|
||||||
|
res.json({
|
||||||
|
response: result,
|
||||||
|
model: model
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при отправке сообщения в Ollama:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: "Ошибка при отправке сообщения в Ollama. Убедитесь, что сервер Ollama запущен."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверьте, что маршрут правильно настроен
|
||||||
|
router.post('/message', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { message } = req.body;
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return res.status(400).json({ error: 'Message is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Получено сообщение:', message);
|
||||||
|
|
||||||
|
// Здесь ваш код обработки сообщения
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// Временный ответ для тестирования
|
||||||
|
res.json({
|
||||||
|
response: `Это тестовый ответ на ваше сообщение: "${message}". Сервер работает.`
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing message:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
32
backend/routes/contracts.js
Normal file
32
backend/routes/contracts.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { requireRole } = require('../middleware/auth');
|
||||||
|
|
||||||
|
// Получение информации о контрактах
|
||||||
|
router.get('/', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
message: 'Contracts API endpoint',
|
||||||
|
contracts: [
|
||||||
|
{
|
||||||
|
name: 'AccessToken',
|
||||||
|
address: process.env.ACCESS_TOKEN_ADDRESS
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Защищенный эндпоинт для получения детальной информации о контрактах
|
||||||
|
router.get('/details', requireRole('ADMIN'), (req, res) => {
|
||||||
|
res.json({
|
||||||
|
message: 'Contract details endpoint',
|
||||||
|
contracts: [
|
||||||
|
{
|
||||||
|
name: 'AccessToken',
|
||||||
|
address: process.env.ACCESS_TOKEN_ADDRESS,
|
||||||
|
network: process.env.ETHEREUM_NETWORK_URL.includes('sepolia') ? 'Sepolia' : 'Unknown'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
189
backend/routes/debug.js
Normal file
189
backend/routes/debug.js
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Эндпоинт для отладки сессий
|
||||||
|
router.get('/session', (req, res) => {
|
||||||
|
try {
|
||||||
|
console.log('Отладка сессии:', {
|
||||||
|
sessionID: req.sessionID,
|
||||||
|
session: req.session ? {
|
||||||
|
isAuthenticated: req.session.isAuthenticated,
|
||||||
|
authenticated: req.session.authenticated,
|
||||||
|
address: req.session.address,
|
||||||
|
isAdmin: req.session.isAdmin
|
||||||
|
} : null,
|
||||||
|
cookies: req.cookies,
|
||||||
|
headers: {
|
||||||
|
cookie: req.headers.cookie
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
sessionID: req.sessionID,
|
||||||
|
session: req.session ? {
|
||||||
|
isAuthenticated: req.session.isAuthenticated,
|
||||||
|
authenticated: req.session.authenticated,
|
||||||
|
address: req.session.address,
|
||||||
|
isAdmin: req.session.isAdmin
|
||||||
|
} : null,
|
||||||
|
cookies: req.cookies
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при отладке сессии:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Эндпоинт для создания тестовой сессии
|
||||||
|
router.post('/create-session', (req, res) => {
|
||||||
|
const { address } = req.body;
|
||||||
|
|
||||||
|
if (!address) {
|
||||||
|
return res.status(400).json({ error: 'Address is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализируем сессию, если она не существует
|
||||||
|
if (!req.session) {
|
||||||
|
req.session = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
req.session.isAuthenticated = true;
|
||||||
|
req.session.authenticated = true;
|
||||||
|
req.session.address = address.toLowerCase();
|
||||||
|
req.session.isAdmin = true;
|
||||||
|
|
||||||
|
// Сохраняем сессию
|
||||||
|
req.session.save((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка сохранения тестовой сессии:', err);
|
||||||
|
return res.status(500).json({ error: 'Session save error' });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Тестовая сессия создана:', {
|
||||||
|
sessionID: req.sessionID,
|
||||||
|
session: {
|
||||||
|
isAuthenticated: req.session.isAuthenticated,
|
||||||
|
authenticated: req.session.authenticated,
|
||||||
|
address: req.session.address,
|
||||||
|
isAdmin: req.session.isAdmin
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.cookie('authToken', 'true', {
|
||||||
|
maxAge: 86400000,
|
||||||
|
httpOnly: false,
|
||||||
|
secure: false,
|
||||||
|
sameSite: 'lax',
|
||||||
|
path: '/'
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
sessionID: req.sessionID,
|
||||||
|
address: req.session.address,
|
||||||
|
isAdmin: req.session.isAdmin
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Тестовый эндпоинт для отправки сообщений без проверки сессии
|
||||||
|
router.post('/test-chat', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { message, address } = req.body;
|
||||||
|
|
||||||
|
console.log('Тестовый чат-запрос:', {
|
||||||
|
message,
|
||||||
|
address,
|
||||||
|
headers: {
|
||||||
|
cookie: req.headers.cookie,
|
||||||
|
'content-type': req.headers['content-type']
|
||||||
|
},
|
||||||
|
cookies: req.cookies,
|
||||||
|
session: req.session ? {
|
||||||
|
isAuthenticated: req.session.isAuthenticated,
|
||||||
|
authenticated: req.session.authenticated,
|
||||||
|
address: req.session.address,
|
||||||
|
isAdmin: req.session.isAdmin
|
||||||
|
} : null
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return res.status(400).json({ error: 'Message is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Возвращаем тестовый ответ
|
||||||
|
res.json({
|
||||||
|
response: `Тестовый ответ на сообщение: ${message}`,
|
||||||
|
receivedAddress: address,
|
||||||
|
sessionAddress: req.session?.address
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка в тестовом чате:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Тестовый эндпоинт для проверки соединения
|
||||||
|
router.get('/ping', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
message: 'pong',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
server: {
|
||||||
|
port: process.env.PORT || 8080,
|
||||||
|
address: req.socket.localAddress,
|
||||||
|
hostname: require('os').hostname()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Тестовый эндпоинт для проверки Ollama
|
||||||
|
router.get('/ollama-test', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { directOllamaQuery } = require('../services/ollama');
|
||||||
|
|
||||||
|
// Тестовый запрос к Ollama
|
||||||
|
const result = await directOllamaQuery('Привет, как дела?', 'mistral');
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
response: result,
|
||||||
|
model: 'mistral'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при тестировании Ollama:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message || 'Ошибка при тестировании Ollama'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Тестовый эндпоинт для проверки доступности Ollama
|
||||||
|
router.get('/ollama-status', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { checkOllamaAvailability } = require('../services/ollama');
|
||||||
|
|
||||||
|
// Проверяем доступность Ollama
|
||||||
|
const isAvailable = await checkOllamaAvailability();
|
||||||
|
|
||||||
|
if (isAvailable) {
|
||||||
|
res.json({
|
||||||
|
status: 'ok',
|
||||||
|
message: 'Ollama доступен'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(503).json({
|
||||||
|
status: 'error',
|
||||||
|
message: 'Ollama недоступен'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при проверке доступности Ollama:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
status: 'error',
|
||||||
|
message: error.message || 'Ошибка при проверке доступности Ollama'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
36
backend/routes/health.js
Normal file
36
backend/routes/health.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const db = require('../db');
|
||||||
|
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Проверка соединения с базой данных
|
||||||
|
const dbResult = await db.query('SELECT NOW()');
|
||||||
|
|
||||||
|
// Проверка состояния сервера
|
||||||
|
const memoryUsage = process.memoryUsage();
|
||||||
|
const uptime = process.uptime();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
status: 'ok',
|
||||||
|
timestamp: new Date(),
|
||||||
|
uptime: uptime,
|
||||||
|
memory: {
|
||||||
|
rss: Math.round(memoryUsage.rss / 1024 / 1024) + 'MB',
|
||||||
|
heapTotal: Math.round(memoryUsage.heapTotal / 1024 / 1024) + 'MB',
|
||||||
|
heapUsed: Math.round(memoryUsage.heapUsed / 1024 / 1024) + 'MB'
|
||||||
|
},
|
||||||
|
database: {
|
||||||
|
connected: true,
|
||||||
|
timestamp: dbResult.rows[0].now
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
status: 'error',
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
75
backend/routes/identities.js
Normal file
75
backend/routes/identities.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { linkIdentity, getUserIdentities } = require('../utils/identity-linker');
|
||||||
|
const { Pool } = require('pg');
|
||||||
|
|
||||||
|
// Подключение к БД
|
||||||
|
const pool = new Pool({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Middleware для проверки аутентификации
|
||||||
|
function requireAuth(req, res, next) {
|
||||||
|
if (!req.session || (!req.session.isAuthenticated && !req.session.authenticated)) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение связанных идентификаторов пользователя
|
||||||
|
router.get('/', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Получаем ID пользователя по Ethereum-адресу
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT id FROM users WHERE address = $1',
|
||||||
|
[req.session.address]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = result.rows[0].id;
|
||||||
|
|
||||||
|
// Получаем все идентификаторы пользователя
|
||||||
|
const identities = await getUserIdentities(userId);
|
||||||
|
|
||||||
|
res.json({ identities });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting user identities:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Удаление связанного идентификатора
|
||||||
|
router.delete('/:type/:value', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { type, value } = req.params;
|
||||||
|
|
||||||
|
// Получаем ID пользователя по Ethereum-адресу
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT id FROM users WHERE address = $1',
|
||||||
|
[req.session.address]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = result.rows[0].id;
|
||||||
|
|
||||||
|
// Удаляем идентификатор
|
||||||
|
await pool.query(
|
||||||
|
'DELETE FROM user_identities WHERE user_id = $1 AND identity_type = $2 AND identity_value = $3',
|
||||||
|
[userId, type, value]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting user identity:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
340
backend/routes/kanban.js
Normal file
340
backend/routes/kanban.js
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { Pool } = require('pg');
|
||||||
|
const pool = new Pool({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Middleware для проверки аутентификации
|
||||||
|
function requireAuth(req, res, next) {
|
||||||
|
if (!req.session || (!req.session.isAuthenticated && !req.session.authenticated)) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение всех досок пользователя
|
||||||
|
router.get('/boards', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Для разработки: если сессия не содержит адрес, используем тестовый
|
||||||
|
const userAddress = (req.session.address || '0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b').toLowerCase();
|
||||||
|
|
||||||
|
console.log('Запрос досок для адреса:', userAddress);
|
||||||
|
|
||||||
|
// Проверяем, существует ли пользователь
|
||||||
|
const userResult = await pool.query(
|
||||||
|
'SELECT id FROM users WHERE address = $1',
|
||||||
|
[userAddress]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Результат запроса пользователя:', userResult.rows);
|
||||||
|
|
||||||
|
if (userResult.rows.length === 0) {
|
||||||
|
console.log('Пользователь не найден, создаем нового');
|
||||||
|
// Если пользователь не найден, создаем его
|
||||||
|
const newUserResult = await pool.query(
|
||||||
|
'INSERT INTO users (address, created_at) VALUES ($1, NOW()) RETURNING id',
|
||||||
|
[userAddress]
|
||||||
|
);
|
||||||
|
console.log('Создан новый пользователь:', newUserResult.rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем доски пользователя
|
||||||
|
const ownBoardsQuery = 'SELECT kb.* FROM kanban_boards kb ' +
|
||||||
|
'JOIN users u ON kb.owner_id = u.id ' +
|
||||||
|
'WHERE u.address = $1 ' +
|
||||||
|
'ORDER BY kb.updated_at DESC';
|
||||||
|
|
||||||
|
console.log('Запрос досок пользователя:', ownBoardsQuery);
|
||||||
|
|
||||||
|
const ownBoardsResult = await pool.query(ownBoardsQuery, [userAddress]);
|
||||||
|
|
||||||
|
console.log('Результат запроса досок пользователя:', ownBoardsResult.rows);
|
||||||
|
|
||||||
|
// Получаем доски, к которым у пользователя есть доступ
|
||||||
|
const sharedBoardsResult = await pool.query(
|
||||||
|
'SELECT kb.* FROM kanban_boards kb ' +
|
||||||
|
'JOIN kanban_board_access kba ON kb.id = kba.board_id ' +
|
||||||
|
'JOIN users u1 ON kba.user_id = u1.id ' +
|
||||||
|
'JOIN users u2 ON kb.owner_id = u2.id ' +
|
||||||
|
'WHERE u1.address = $1 AND u2.address != $1 ' +
|
||||||
|
'ORDER BY kb.updated_at DESC',
|
||||||
|
[userAddress]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Получаем публичные доски
|
||||||
|
const publicBoardsResult = await pool.query(
|
||||||
|
'SELECT kb.* FROM kanban_boards kb ' +
|
||||||
|
'JOIN users u ON kb.owner_id = u.id ' +
|
||||||
|
'WHERE kb.is_public = true AND u.address != $1 ' +
|
||||||
|
'AND NOT EXISTS (' +
|
||||||
|
' SELECT 1 FROM kanban_board_access kba ' +
|
||||||
|
' JOIN users u2 ON kba.user_id = u2.id ' +
|
||||||
|
' WHERE kba.board_id = kb.id AND u2.address = $1' +
|
||||||
|
') ' +
|
||||||
|
'ORDER BY kb.updated_at DESC',
|
||||||
|
[userAddress]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
ownBoards: ownBoardsResult.rows,
|
||||||
|
sharedBoards: sharedBoardsResult.rows,
|
||||||
|
publicBoards: publicBoardsResult.rows
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching boards:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Создание новой доски
|
||||||
|
router.post('/boards', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { title, description, isPublic } = req.body;
|
||||||
|
|
||||||
|
// Получаем ID пользователя
|
||||||
|
let userResult = await pool.query(
|
||||||
|
'SELECT id FROM users WHERE address = $1',
|
||||||
|
[req.session.address]
|
||||||
|
);
|
||||||
|
|
||||||
|
let userId;
|
||||||
|
|
||||||
|
if (userResult.rows.length === 0) {
|
||||||
|
// Если пользователь не найден, создаем его
|
||||||
|
const newUserResult = await pool.query(
|
||||||
|
'INSERT INTO users (address, created_at, preferred_language) VALUES ($1, NOW(), $2) RETURNING id',
|
||||||
|
[req.session.address, 'ru']
|
||||||
|
);
|
||||||
|
|
||||||
|
userId = newUserResult.rows[0].id;
|
||||||
|
} else {
|
||||||
|
userId = userResult.rows[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем новую доску
|
||||||
|
const result = await pool.query(
|
||||||
|
`INSERT INTO kanban_boards (title, description, owner_id, is_public, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, NOW(), NOW())
|
||||||
|
RETURNING *`,
|
||||||
|
[title, description, userId, isPublic]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Создаем стандартные колонки
|
||||||
|
const columns = ['Backlog', 'In Progress', 'Review', 'Done'];
|
||||||
|
for (let i = 0; i < columns.length; i++) {
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO kanban_columns (board_id, title, position, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, $3, NOW(), NOW())`,
|
||||||
|
[result.rows[0].id, columns[i], i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(201).json(result.rows[0]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating kanban board:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получение конкретной доски со всеми колонками и карточками
|
||||||
|
router.get('/boards/:id', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const boardId = req.params.id;
|
||||||
|
|
||||||
|
// Получаем ID пользователя
|
||||||
|
let userResult = await pool.query(
|
||||||
|
'SELECT id FROM users WHERE address = $1',
|
||||||
|
[req.session.address]
|
||||||
|
);
|
||||||
|
|
||||||
|
let userId;
|
||||||
|
|
||||||
|
if (userResult.rows.length === 0) {
|
||||||
|
// Если пользователь не найден, создаем его
|
||||||
|
const newUserResult = await pool.query(
|
||||||
|
'INSERT INTO users (address, created_at, preferred_language) VALUES ($1, NOW(), $2) RETURNING id',
|
||||||
|
[req.session.address, 'ru']
|
||||||
|
);
|
||||||
|
|
||||||
|
userId = newUserResult.rows[0].id;
|
||||||
|
} else {
|
||||||
|
userId = userResult.rows[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем доступ к доске
|
||||||
|
const boardResult = await pool.query(
|
||||||
|
'SELECT * FROM kanban_boards WHERE id = $1',
|
||||||
|
[boardId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (boardResult.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'Board not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const board = boardResult.rows[0];
|
||||||
|
|
||||||
|
// Проверяем, имеет ли пользователь доступ к доске
|
||||||
|
if (board.owner_id !== userId && !board.is_public) {
|
||||||
|
const accessResult = await pool.query(
|
||||||
|
'SELECT * FROM kanban_board_access WHERE board_id = $1 AND user_id = $2',
|
||||||
|
[boardId, userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (accessResult.rows.length === 0) {
|
||||||
|
return res.status(403).json({ error: 'Access denied' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем колонки доски
|
||||||
|
const columnsResult = await pool.query(
|
||||||
|
'SELECT * FROM kanban_columns WHERE board_id = $1 ORDER BY position',
|
||||||
|
[boardId]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Получаем карточки для всех колонок
|
||||||
|
const cardsResult = await pool.query(
|
||||||
|
`SELECT kc.*, u.address as assigned_address
|
||||||
|
FROM kanban_cards kc
|
||||||
|
LEFT JOIN users u ON kc.assigned_to = u.id
|
||||||
|
WHERE kc.column_id IN (
|
||||||
|
SELECT id FROM kanban_columns WHERE board_id = $1
|
||||||
|
)
|
||||||
|
ORDER BY kc.position`,
|
||||||
|
[boardId]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Группируем карточки по колонкам
|
||||||
|
const columns = columnsResult.rows.map(column => {
|
||||||
|
const cards = cardsResult.rows.filter(card => card.column_id === column.id);
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
cards
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
...board,
|
||||||
|
columns
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting kanban board:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавление колонки к доске
|
||||||
|
router.post('/boards/:boardId/columns', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { boardId } = req.params;
|
||||||
|
const { title, wipLimit } = req.body;
|
||||||
|
|
||||||
|
// Проверяем, существует ли доска
|
||||||
|
const boardResult = await pool.query(
|
||||||
|
'SELECT * FROM kanban_boards WHERE id = $1',
|
||||||
|
[boardId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (boardResult.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'Board not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем максимальную позицию колонок
|
||||||
|
const positionResult = await pool.query(
|
||||||
|
'SELECT MAX(position) as max_position FROM kanban_columns WHERE board_id = $1',
|
||||||
|
[boardId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const position = positionResult.rows[0].max_position ? positionResult.rows[0].max_position + 1 : 0;
|
||||||
|
|
||||||
|
// Создаем новую колонку
|
||||||
|
const result = await pool.query(
|
||||||
|
`INSERT INTO kanban_columns (board_id, title, position, wip_limit, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, NOW(), NOW())
|
||||||
|
RETURNING *`,
|
||||||
|
[boardId, title, position, wipLimit]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(201).json(result.rows[0]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating column:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получение колонок доски
|
||||||
|
router.get('/boards/:boardId/columns', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { boardId } = req.params;
|
||||||
|
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT * FROM kanban_columns WHERE board_id = $1 ORDER BY position',
|
||||||
|
[boardId]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json(result.rows);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting columns:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Создание карточки
|
||||||
|
router.post('/cards', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { title, description, columnId, dueDate } = req.body;
|
||||||
|
|
||||||
|
// Получаем ID пользователя
|
||||||
|
let userResult = await pool.query(
|
||||||
|
'SELECT id FROM users WHERE address = $1',
|
||||||
|
[req.session.address]
|
||||||
|
);
|
||||||
|
|
||||||
|
let userId;
|
||||||
|
|
||||||
|
if (userResult.rows.length === 0) {
|
||||||
|
// Если пользователь не найден, создаем его
|
||||||
|
const newUserResult = await pool.query(
|
||||||
|
'INSERT INTO users (address, created_at, preferred_language) VALUES ($1, NOW(), $2) RETURNING id',
|
||||||
|
[req.session.address, 'ru']
|
||||||
|
);
|
||||||
|
|
||||||
|
userId = newUserResult.rows[0].id;
|
||||||
|
} else {
|
||||||
|
userId = userResult.rows[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем максимальную позицию карточек в колонке
|
||||||
|
const positionResult = await pool.query(
|
||||||
|
'SELECT MAX(position) as max_position FROM kanban_cards WHERE column_id = $1',
|
||||||
|
[columnId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const position = positionResult.rows[0].max_position ? positionResult.rows[0].max_position + 1 : 0;
|
||||||
|
|
||||||
|
// Создаем новую карточку
|
||||||
|
const result = await pool.query(
|
||||||
|
`INSERT INTO kanban_cards (column_id, title, description, position, due_date, created_by, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
|
||||||
|
RETURNING *`,
|
||||||
|
[columnId, title, description, position, dueDate, userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Получаем информацию о пользователе для отображения
|
||||||
|
const cardWithUser = {
|
||||||
|
...result.rows[0],
|
||||||
|
assigned_address: null
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(201).json(cardWithUser);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating card:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавляем остальные маршруты для работы с колонками, карточками и т.д.
|
||||||
|
// ...
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
18
backend/routes/users.js
Normal file
18
backend/routes/users.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Получение списка пользователей
|
||||||
|
router.get('/', (req, res) => {
|
||||||
|
res.json({ message: 'Users API endpoint' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получение информации о пользователе
|
||||||
|
router.get('/:address', (req, res) => {
|
||||||
|
const { address } = req.params;
|
||||||
|
res.json({
|
||||||
|
address,
|
||||||
|
message: 'User details endpoint'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
22
backend/scripts/check-dependencies.js
Normal file
22
backend/scripts/check-dependencies.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const packageJson = require('../package.json');
|
||||||
|
const dependencies = packageJson.dependencies || {};
|
||||||
|
|
||||||
|
const requiredDependencies = [
|
||||||
|
'express-rate-limit',
|
||||||
|
'winston',
|
||||||
|
'helmet',
|
||||||
|
'csurf'
|
||||||
|
];
|
||||||
|
|
||||||
|
const missingDependencies = requiredDependencies.filter(dep => !dependencies[dep]);
|
||||||
|
|
||||||
|
if (missingDependencies.length > 0) {
|
||||||
|
console.error('Missing dependencies:', missingDependencies);
|
||||||
|
console.error('Please install them with: yarn add ' + missingDependencies.join(' '));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('All required dependencies are installed.');
|
||||||
43
backend/scripts/check-state.js
Normal file
43
backend/scripts/check-state.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
const hre = require("hardhat");
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const accessToken = await hre.ethers.getContractAt(
|
||||||
|
"AccessToken",
|
||||||
|
"0xF352c498cF0857F472dC473E4Dd39551E79B1063"
|
||||||
|
);
|
||||||
|
|
||||||
|
const owner = await accessToken.owner();
|
||||||
|
console.log("Contract owner:", owner);
|
||||||
|
|
||||||
|
// Проверяем все токены и их владельцев
|
||||||
|
console.log("\nAll tokens:");
|
||||||
|
for (let i = 1; i <= 10; i++) {
|
||||||
|
try {
|
||||||
|
const tokenOwner = await accessToken.ownerOf(i);
|
||||||
|
console.log(`Token ${i} owner: ${tokenOwner}`);
|
||||||
|
} catch (error) {
|
||||||
|
if (!error.message.includes("invalid token ID")) {
|
||||||
|
console.log(`Token ${i} error:`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем активные токены для всех известных адресов
|
||||||
|
const addresses = [
|
||||||
|
owner,
|
||||||
|
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log("\nActive tokens:");
|
||||||
|
for (const address of addresses) {
|
||||||
|
const activeToken = await accessToken.activeTokens(address);
|
||||||
|
console.log(`${address}: Token ${activeToken.toString()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.then(() => process.exit(0))
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
51
backend/scripts/create-moderator.js
Normal file
51
backend/scripts/create-moderator.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
const hre = require("hardhat");
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const accessToken = await hre.ethers.getContractAt(
|
||||||
|
"AccessToken",
|
||||||
|
"0xF352c498cF0857F472dC473E4Dd39551E79B1063"
|
||||||
|
);
|
||||||
|
|
||||||
|
const moderatorAddress = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8";
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("\nMinting moderator token...");
|
||||||
|
const mintTx = await accessToken.mintAccessToken(moderatorAddress, 1); // MODERATOR
|
||||||
|
console.log("Waiting for transaction:", mintTx.hash);
|
||||||
|
await mintTx.wait();
|
||||||
|
console.log("Moderator token minted");
|
||||||
|
|
||||||
|
// Проверяем результат
|
||||||
|
const activeToken = await accessToken.activeTokens(moderatorAddress);
|
||||||
|
console.log(`Moderator's active token: ${activeToken}`);
|
||||||
|
|
||||||
|
const role = await accessToken.checkRole(moderatorAddress);
|
||||||
|
console.log(`Moderator role: ${["ADMIN", "MODERATOR", "SUPPORT"][role]}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Moderator token minting error:", error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем все активные токены
|
||||||
|
console.log("\nAll active tokens:");
|
||||||
|
const addresses = [
|
||||||
|
await accessToken.owner(),
|
||||||
|
moderatorAddress
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const address of addresses) {
|
||||||
|
try {
|
||||||
|
const activeToken = await accessToken.activeTokens(address);
|
||||||
|
const role = await accessToken.checkRole(address);
|
||||||
|
console.log(`${address}: Token ${activeToken}, Role: ${["ADMIN", "MODERATOR", "SUPPORT"][role]}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`${address}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.then(() => process.exit(0))
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Script error:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
23
backend/scripts/deploy-access.js
Normal file
23
backend/scripts/deploy-access.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
const hre = require("hardhat");
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const AccessToken = await hre.ethers.getContractFactory("AccessToken");
|
||||||
|
const accessToken = await AccessToken.deploy();
|
||||||
|
await accessToken.waitForDeployment();
|
||||||
|
|
||||||
|
const address = await accessToken.getAddress();
|
||||||
|
console.log("AccessToken deployed to:", address);
|
||||||
|
|
||||||
|
// Создаем первый админский токен для владельца контракта
|
||||||
|
const [owner] = await hre.ethers.getSigners();
|
||||||
|
const tx = await accessToken.mintAccessToken(owner.address, 0); // 0 = ADMIN
|
||||||
|
await tx.wait();
|
||||||
|
console.log("Admin token minted for:", owner.address);
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.then(() => process.exit(0))
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
62
backend/scripts/init-db.js
Normal file
62
backend/scripts/init-db.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
const { Pool } = require('pg');
|
||||||
|
const dotenv = require('dotenv');
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const pool = new Pool({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
|
||||||
|
});
|
||||||
|
|
||||||
|
async function initDb() {
|
||||||
|
try {
|
||||||
|
console.log('Инициализация базы данных...');
|
||||||
|
|
||||||
|
// Добавляем тестового пользователя
|
||||||
|
await pool.query(`
|
||||||
|
INSERT INTO users (address, is_admin)
|
||||||
|
VALUES ('0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b', TRUE)
|
||||||
|
ON CONFLICT (address) DO NOTHING
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Добавляем тестовую доску
|
||||||
|
await pool.query(`
|
||||||
|
INSERT INTO kanban_boards (title, description, owner_id, is_public)
|
||||||
|
VALUES (
|
||||||
|
'Тестовая доска',
|
||||||
|
'Описание тестовой доски',
|
||||||
|
(SELECT id FROM users WHERE address = '0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b'),
|
||||||
|
TRUE
|
||||||
|
)
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Получаем ID доски
|
||||||
|
const boardResult = await pool.query(`
|
||||||
|
SELECT id FROM kanban_boards WHERE title = 'Тестовая доска' LIMIT 1
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (boardResult.rows.length > 0) {
|
||||||
|
const boardId = boardResult.rows[0].id;
|
||||||
|
|
||||||
|
// Добавляем тестовые колонки
|
||||||
|
await pool.query(`
|
||||||
|
INSERT INTO kanban_columns (board_id, title, position)
|
||||||
|
VALUES
|
||||||
|
($1, 'Backlog', 0),
|
||||||
|
($1, 'In Progress', 1),
|
||||||
|
($1, 'Review', 2),
|
||||||
|
($1, 'Done', 3)
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
`, [boardId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('База данных инициализирована успешно');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при инициализации базы данных:', error);
|
||||||
|
} finally {
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initDb();
|
||||||
58
backend/scripts/init-roles.js
Normal file
58
backend/scripts/init-roles.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
const hre = require("hardhat");
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const accessToken = await hre.ethers.getContractAt(
|
||||||
|
"AccessToken",
|
||||||
|
"0xF352c498cF0857F472dC473E4Dd39551E79B1063"
|
||||||
|
);
|
||||||
|
|
||||||
|
const owner = await accessToken.owner();
|
||||||
|
console.log("Contract owner:", owner);
|
||||||
|
|
||||||
|
// Создаем админский токен для владельца
|
||||||
|
try {
|
||||||
|
const tx = await accessToken.mintAccessToken(owner, 0); // 0 = ADMIN
|
||||||
|
await tx.wait();
|
||||||
|
console.log(`Admin token minted for ${owner}`);
|
||||||
|
|
||||||
|
const role = await accessToken.checkRole(owner);
|
||||||
|
console.log("Owner role:", ["ADMIN", "MODERATOR", "SUPPORT"][role]);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Admin token minting error:", error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем тестовый токен модератора
|
||||||
|
const moderatorAddress = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"; // Тестовый адрес модератора
|
||||||
|
try {
|
||||||
|
const tx = await accessToken.mintAccessToken(moderatorAddress, 1); // 1 = MODERATOR
|
||||||
|
await tx.wait();
|
||||||
|
console.log(`Moderator token minted for ${moderatorAddress}`);
|
||||||
|
|
||||||
|
const role = await accessToken.checkRole(moderatorAddress);
|
||||||
|
console.log("Moderator role:", ["ADMIN", "MODERATOR", "SUPPORT"][role]);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Moderator token minting error:", error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем все токены
|
||||||
|
console.log("\nChecking all tokens:");
|
||||||
|
for (let i = 1; i <= 5; i++) {
|
||||||
|
try {
|
||||||
|
const owner = await accessToken.ownerOf(i);
|
||||||
|
const role = await accessToken.checkRole(owner);
|
||||||
|
console.log(`Token ${i}: Owner ${owner}, Role: ${["ADMIN", "MODERATOR", "SUPPORT"][role]}`);
|
||||||
|
} catch (error) {
|
||||||
|
// Пропускаем несуществующие токены
|
||||||
|
if (!error.message.includes("nonexistent token")) {
|
||||||
|
console.log(`Token ${i} error:`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.then(() => process.exit(0))
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
72
backend/scripts/manage-access.js
Normal file
72
backend/scripts/manage-access.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
const hre = require("hardhat");
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const accessToken = await hre.ethers.getContractAt(
|
||||||
|
"AccessToken",
|
||||||
|
"0xF352c498cF0857F472dC473E4Dd39551E79B1063" // Адрес нашего контракта
|
||||||
|
);
|
||||||
|
|
||||||
|
// Проверим текущего владельца
|
||||||
|
const owner = await accessToken.owner();
|
||||||
|
console.log("Contract owner:", owner);
|
||||||
|
|
||||||
|
// Проверим роль владельца
|
||||||
|
try {
|
||||||
|
const ownerRole = await accessToken.checkRole(owner);
|
||||||
|
console.log("Owner role:", ["ADMIN", "MODERATOR", "SUPPORT"][ownerRole]);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Owner role check error:", error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создадим токен модератора для тестового адреса
|
||||||
|
const moderatorAddress = "0xF45aa4917b3775bA37f48Aeb3dc1a943561e9e0B";
|
||||||
|
try {
|
||||||
|
const tx = await accessToken.mintAccessToken(moderatorAddress, 1); // 1 = MODERATOR
|
||||||
|
await tx.wait();
|
||||||
|
console.log(`Moderator token minted for ${moderatorAddress}`);
|
||||||
|
|
||||||
|
// Проверим роль модератора
|
||||||
|
const modRole = await accessToken.checkRole(moderatorAddress);
|
||||||
|
console.log("Moderator role:", ["ADMIN", "MODERATOR", "SUPPORT"][modRole]);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Moderator token minting error:", error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получим все активные токены (с ограничением по блокам)
|
||||||
|
const currentBlock = await hre.ethers.provider.getBlockNumber();
|
||||||
|
const fromBlock = currentBlock - 1000; // Последние 1000 блоков
|
||||||
|
|
||||||
|
const filter = accessToken.filters.Transfer(null, null, null);
|
||||||
|
const events = await accessToken.queryFilter(filter, fromBlock);
|
||||||
|
console.log("\nActive tokens (last 1000 blocks):");
|
||||||
|
for (let event of events) {
|
||||||
|
if (event.args.from === "0x0000000000000000000000000000000000000000") {
|
||||||
|
console.log(`Token ID: ${event.args.tokenId}, Owner: ${event.args.to}`);
|
||||||
|
try {
|
||||||
|
const role = await accessToken.checkRole(event.args.to);
|
||||||
|
console.log(`Role: ${["ADMIN", "MODERATOR", "SUPPORT"][role]}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Role check error:", error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Альтернативный способ - проверить конкретный токен
|
||||||
|
console.log("\nChecking specific tokens:");
|
||||||
|
for (let i = 1; i <= 2; i++) {
|
||||||
|
try {
|
||||||
|
const owner = await accessToken.ownerOf(i);
|
||||||
|
const role = await accessToken.checkRole(owner);
|
||||||
|
console.log(`Token ${i}: Owner ${owner}, Role: ${["ADMIN", "MODERATOR", "SUPPORT"][role]}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Token ${i} not found or error:`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.then(() => process.exit(0))
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
26
backend/scripts/revoke-all.js
Normal file
26
backend/scripts/revoke-all.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const hre = require("hardhat");
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const accessToken = await hre.ethers.getContractAt(
|
||||||
|
"AccessToken",
|
||||||
|
"0xF352c498cF0857F472dC473E4Dd39551E79B1063"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Отзываем все токены от 1 до 3
|
||||||
|
for (let i = 1; i <= 3; i++) {
|
||||||
|
try {
|
||||||
|
const tx = await accessToken.revokeToken(i);
|
||||||
|
await tx.wait();
|
||||||
|
console.log(`Token ${i} revoked`);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Token ${i} revoke error:`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.then(() => process.exit(0))
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
68
backend/scripts/run-migrations.js
Normal file
68
backend/scripts/run-migrations.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
const { Pool } = require('pg');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
// Подключение к БД
|
||||||
|
const pool = new Pool({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
|
||||||
|
});
|
||||||
|
|
||||||
|
async function runMigrations() {
|
||||||
|
try {
|
||||||
|
console.log('Запуск миграций...');
|
||||||
|
|
||||||
|
// Создаем таблицу для отслеживания миграций, если её нет
|
||||||
|
await pool.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS migrations (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
applied_at TIMESTAMP DEFAULT NOW()
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Получаем список уже примененных миграций
|
||||||
|
const { rows } = await pool.query('SELECT name FROM migrations');
|
||||||
|
const appliedMigrations = rows.map(row => row.name);
|
||||||
|
|
||||||
|
// Получаем список файлов миграций
|
||||||
|
const migrationsDir = path.join(__dirname, '../migrations');
|
||||||
|
const migrationFiles = fs.readdirSync(migrationsDir)
|
||||||
|
.filter(file => file.endsWith('.sql'))
|
||||||
|
.sort(); // Сортируем файлы по имени
|
||||||
|
|
||||||
|
// Применяем миграции, которые еще не были применены
|
||||||
|
for (const file of migrationFiles) {
|
||||||
|
if (!appliedMigrations.includes(file)) {
|
||||||
|
console.log(`Применение миграции: ${file}`);
|
||||||
|
|
||||||
|
// Читаем содержимое файла миграции
|
||||||
|
const filePath = path.join(migrationsDir, file);
|
||||||
|
const sql = fs.readFileSync(filePath, 'utf8');
|
||||||
|
|
||||||
|
// Выполняем SQL-запросы из файла
|
||||||
|
await pool.query(sql);
|
||||||
|
|
||||||
|
// Записываем информацию о примененной миграции
|
||||||
|
await pool.query(
|
||||||
|
'INSERT INTO migrations (name) VALUES ($1)',
|
||||||
|
[file]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Миграция ${file} успешно применена`);
|
||||||
|
} else {
|
||||||
|
console.log(`Миграция ${file} уже применена`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Все миграции успешно применены');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при выполнении миграций:', error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runMigrations();
|
||||||
@@ -1,399 +1,676 @@
|
|||||||
|
require('dotenv').config();
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const session = require('express-session');
|
|
||||||
const { SiweMessage, generateNonce } = require('siwe');
|
const { SiweMessage, generateNonce } = require('siwe');
|
||||||
const path = require('path');
|
const { ethers } = require('ethers');
|
||||||
const apiRoutes = require('./routes/api.js');
|
|
||||||
const FileStore = require('session-file-store')(session);
|
|
||||||
const TelegramBotService = require('./services/telegramBot');
|
const TelegramBotService = require('./services/telegramBot');
|
||||||
const EmailBotService = require('./services/emailBot');
|
const EmailBotService = require('./services/emailBot');
|
||||||
const { PGVectorStore } = require('@langchain/community/vectorstores/pgvector');
|
const { initializeVectorStore } = require('./services/vectorStore');
|
||||||
const { OpenAIEmbeddings } = require('@langchain/openai');
|
const session = require('express-session');
|
||||||
const { exec } = require('child_process');
|
const { app, nonceStore } = require('./app');
|
||||||
const util = require('util');
|
const usersRouter = require('./routes/users');
|
||||||
const execAsync = util.promisify(exec);
|
const { router: authRouter } = require('./routes/auth');
|
||||||
|
const contractsRouter = require('./routes/contracts');
|
||||||
|
const accessRouter = require('./routes/access');
|
||||||
|
const chatRouter = require('./routes/chat');
|
||||||
|
const path = require('path');
|
||||||
|
const axios = require('axios');
|
||||||
|
const { ChatOllama } = require('@langchain/ollama');
|
||||||
|
const { getVectorStore } = require('./services/vectorStore');
|
||||||
|
const debugRouter = require('./routes/debug');
|
||||||
|
const identitiesRouter = require('./routes/identities');
|
||||||
|
const kanbanRouter = require('./routes/kanban');
|
||||||
|
const { pool } = require('./db');
|
||||||
|
const fs = require('fs');
|
||||||
|
const pgSession = require('connect-pg-simple')(session);
|
||||||
|
const sessionStore = new pgSession({
|
||||||
|
pool: pool,
|
||||||
|
tableName: 'session',
|
||||||
|
createTableIfMissing: true
|
||||||
|
});
|
||||||
|
const helmet = require('helmet');
|
||||||
|
const csrf = require('csurf');
|
||||||
|
|
||||||
const app = express();
|
const PORT = process.env.PORT || 8000;
|
||||||
|
|
||||||
// Флаг для отслеживания состояния сервера
|
console.log('Начало выполнения server.js');
|
||||||
let isShuttingDown = false;
|
console.log('Переменная окружения PORT:', process.env.PORT);
|
||||||
|
console.log('Используемый порт:', process.env.PORT || 8000);
|
||||||
|
|
||||||
// Глобальные переменные для сервисов
|
// Инициализация сервисов
|
||||||
let telegramBot;
|
let telegramBot;
|
||||||
let emailBot;
|
let emailBot;
|
||||||
let vectorStore;
|
|
||||||
|
|
||||||
// 1. Парсинг JSON
|
// Получаем ABI из артефактов
|
||||||
app.use(express.json());
|
const contractArtifact = require('./artifacts/contracts/MyContract.sol/MyContract.json');
|
||||||
|
const contractABI = contractArtifact.abi;
|
||||||
|
|
||||||
// 2. CORS
|
// Добавим логирование для отладки контракта
|
||||||
|
const provider = new ethers.JsonRpcProvider(process.env.ETHEREUM_NETWORK_URL);
|
||||||
|
console.log('Provider URL:', process.env.ETHEREUM_NETWORK_URL);
|
||||||
|
console.log('Contract address:', process.env.CONTRACT_ADDRESS);
|
||||||
|
|
||||||
|
const contract = new ethers.Contract(
|
||||||
|
process.env.CONTRACT_ADDRESS,
|
||||||
|
contractABI,
|
||||||
|
provider
|
||||||
|
);
|
||||||
|
|
||||||
|
// Проверяем, что библиотека ethers.js правильно импортирована
|
||||||
|
console.log('Ethers.js version:', ethers.version);
|
||||||
|
|
||||||
|
// Порядок middleware важен!
|
||||||
|
// 1. CORS должен быть первым
|
||||||
app.use(cors({
|
app.use(cors({
|
||||||
origin: [
|
origin: ['http://127.0.0.1:5173', 'http://localhost:5173'],
|
||||||
'http://127.0.0.1:5173',
|
|
||||||
'http://127.0.0.1:5174'
|
|
||||||
],
|
|
||||||
credentials: true,
|
credentials: true,
|
||||||
methods: ['GET', 'POST'],
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
allowedHeaders: ['Content-Type', 'Authorization', 'X-SIWE-Nonce', 'X-Requested-With', 'Accept'],
|
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
|
||||||
exposedHeaders: ['Set-Cookie']
|
exposedHeaders: ['Set-Cookie']
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 3. Сессии
|
// Добавьте после настройки CORS
|
||||||
|
app.use(helmet());
|
||||||
|
|
||||||
|
// 2. Затем парсеры
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
// 3. Затем сессии
|
||||||
app.use(session({
|
app.use(session({
|
||||||
name: 'siwe-dapp',
|
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
||||||
secret: "siwe-dapp-secret",
|
|
||||||
resave: true,
|
resave: true,
|
||||||
saveUninitialized: false,
|
saveUninitialized: true,
|
||||||
store: new FileStore({
|
|
||||||
path: './sessions',
|
|
||||||
ttl: 86400,
|
|
||||||
retries: 0,
|
|
||||||
logFn: function(){},
|
|
||||||
reapInterval: 86400,
|
|
||||||
reapAsync: true,
|
|
||||||
reapSyncCheck: true,
|
|
||||||
retryTimeout: 100
|
|
||||||
}),
|
|
||||||
cookie: {
|
cookie: {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
path: '/',
|
maxAge: 24 * 60 * 60 * 1000
|
||||||
domain: '127.0.0.1',
|
},
|
||||||
maxAge: 30 * 24 * 60 * 60 * 1000
|
store: sessionStore
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Middleware для сохранения сессии
|
// Добавьте после настройки сессий
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
const oldEnd = res.end;
|
// console.log('Middleware для проверки сессии:', {
|
||||||
res.end = function (chunk, encoding) {
|
// url: req.url,
|
||||||
if (req.session && req.session.save) {
|
// method: req.method,
|
||||||
req.session.save((err) => {
|
// sessionID: req.sessionID,
|
||||||
if (err) console.error('Session save error:', err);
|
// session: req.session ? {
|
||||||
oldEnd.apply(res, arguments);
|
// isAuthenticated: req.session.isAuthenticated,
|
||||||
});
|
// authenticated: req.session.authenticated,
|
||||||
} else {
|
// address: req.session.address,
|
||||||
oldEnd.apply(res, arguments);
|
// isAdmin: req.session.isAdmin
|
||||||
}
|
// } : null,
|
||||||
};
|
// cookies: req.cookies,
|
||||||
|
// headers: {
|
||||||
|
// cookie: req.headers.cookie
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
if (req.session.store) {
|
||||||
|
req.session.store.on('error', (error) => {
|
||||||
|
console.error('Session store error:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Генерация nonce
|
// Добавьте после настройки парсеров
|
||||||
app.get('/nonce', (req, res) => {
|
app.use((req, res, next) => {
|
||||||
try {
|
// if (req.method === 'POST' && req.headers['content-type'] === 'application/json') {
|
||||||
if (!req.session) {
|
// console.log('POST request body:', {
|
||||||
throw new Error('No session available');
|
// url: req.url,
|
||||||
}
|
// body: JSON.stringify(req.body)
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
req.session.nonce = generateNonce();
|
const requireAuth = (req, res, next) => {
|
||||||
console.log('Сгенерирован новый nonce:', req.session.nonce);
|
if (!req.session.authenticated || !req.session.address) {
|
||||||
res.json({ nonce: req.session.nonce });
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
} catch (error) {
|
}
|
||||||
console.error('Ошибка генерации nonce:', error);
|
next();
|
||||||
res.status(500).json({ error: 'Internal Server Error' });
|
};
|
||||||
|
|
||||||
|
app.use('/api/protected', requireAuth);
|
||||||
|
|
||||||
|
// Добавляем middleware для проверки состояния аутентификации
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
// console.log('Auth check middleware:', {
|
||||||
|
// url: req.url,
|
||||||
|
// method: req.method,
|
||||||
|
// sessionID: req.sessionID,
|
||||||
|
// session: req.session ? {
|
||||||
|
// isAuthenticated: req.session.isAuthenticated,
|
||||||
|
// authenticated: req.session.authenticated,
|
||||||
|
// address: req.session.address,
|
||||||
|
// isAdmin: req.session.isAdmin
|
||||||
|
// } : null
|
||||||
|
// });
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавьте после настройки парсеров
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
// if (req.method === 'POST' && req.headers['content-type'] === 'application/json') {
|
||||||
|
// console.log('POST request body:', {
|
||||||
|
// url: req.url,
|
||||||
|
// body: JSON.stringify(req.body)
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавляем middleware для отладки сессий
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
// console.log('Session debug:', {
|
||||||
|
// url: req.url,
|
||||||
|
// method: req.method,
|
||||||
|
// sessionID: req.sessionID,
|
||||||
|
// cookies: req.headers.cookie,
|
||||||
|
// session: req.session ? {
|
||||||
|
// isAuthenticated: req.session.isAuthenticated,
|
||||||
|
// authenticated: req.session.authenticated,
|
||||||
|
// address: req.session.address,
|
||||||
|
// isAdmin: req.session.isAdmin,
|
||||||
|
// nonce: req.session.nonce ? '[REDACTED]' : undefined,
|
||||||
|
// pendingAddress: req.session.pendingAddress
|
||||||
|
// } : null
|
||||||
|
// });
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Настройка CSRF-защиты
|
||||||
|
const csrfProtection = csrf({
|
||||||
|
cookie: {
|
||||||
|
key: '_csrf',
|
||||||
|
path: '/',
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production', // true в production, false в development
|
||||||
|
sameSite: process.env.NODE_ENV === 'production' ? 'strict' : 'lax'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Функция для проверки формата адреса EIP-55
|
// Применяем CSRF-защиту только к определенным маршрутам
|
||||||
function isValidEIP55Address(address) {
|
app.use('/api/protected', csrfProtection);
|
||||||
return /^0x[0-9A-F]{40}$/.test(address);
|
app.use('/api/admin', csrfProtection);
|
||||||
}
|
app.use('/api/kanban', csrfProtection);
|
||||||
|
|
||||||
// Верификация сообщения
|
// Маршрут для получения CSRF-токена
|
||||||
app.post('/verify', async (req, res) => {
|
app.get('/api/csrf-token', csrfProtection, (req, res) => {
|
||||||
|
res.json({ csrfToken: req.csrfToken() });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработчик ошибок CSRF
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
if (err.code === 'EBADCSRFTOKEN') {
|
||||||
|
console.error('CSRF error:', {
|
||||||
|
url: req.url,
|
||||||
|
method: req.method,
|
||||||
|
headers: req.headers,
|
||||||
|
body: req.body
|
||||||
|
});
|
||||||
|
return res.status(403).json({
|
||||||
|
error: 'CSRF token validation failed',
|
||||||
|
message: 'Your session may have expired. Please refresh the page and try again.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
next(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function initServices() {
|
||||||
try {
|
try {
|
||||||
const clientNonce = req.headers['x-siwe-nonce'];
|
console.log('Инициализация сервисов...');
|
||||||
const { signature, message } = req.body;
|
|
||||||
|
|
||||||
console.log('Received message:', message);
|
// Инициализируем ботов, если они нужны
|
||||||
|
if (process.env.TELEGRAM_BOT_TOKEN) {
|
||||||
if (!req.session) {
|
telegramBot = new TelegramBotService(process.env.TELEGRAM_BOT_TOKEN);
|
||||||
throw new Error('No session available');
|
console.log('Telegram бот инициализирован');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем и проверяем SIWE сообщение
|
if (process.env.EMAIL_USER && process.env.EMAIL_PASS) {
|
||||||
let siweMessage;
|
emailBot = new EmailBotService(process.env.EMAIL_USER, process.env.EMAIL_PASS);
|
||||||
try {
|
console.log('Email бот инициализирован');
|
||||||
siweMessage = new SiweMessage(message);
|
}
|
||||||
console.log('SIWE message parsed:', siweMessage);
|
|
||||||
|
|
||||||
// Проверяем nonce
|
console.log('Все сервисы успешно инициализированы');
|
||||||
if (siweMessage.nonce !== clientNonce) {
|
} catch (error) {
|
||||||
throw new Error('Invalid nonce');
|
console.error('Ошибка при инициализации сервисов:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use('/api/users', usersRouter);
|
||||||
|
app.use('/api/auth', authRouter);
|
||||||
|
app.use('/api/contracts', contractsRouter);
|
||||||
|
app.use('/api/access', accessRouter);
|
||||||
|
app.use('/api/chat', chatRouter);
|
||||||
|
app.use('/api/debug', debugRouter);
|
||||||
|
app.use('/api/identities', identitiesRouter);
|
||||||
|
app.use('/api/kanban', kanbanRouter);
|
||||||
|
|
||||||
|
// Добавьте простой эндпоинт для проверки состояния сервера
|
||||||
|
app.get('/api/health', (req, res) => {
|
||||||
|
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавьте после настройки маршрутов
|
||||||
|
app.post('/api/verify', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Перенаправляем запрос на /api/auth/verify
|
||||||
|
const { message, signature } = req.body;
|
||||||
|
console.log("Перенаправление запроса на /api/auth/verify:", { message, signature });
|
||||||
|
|
||||||
|
// Проверяем наличие необходимых данных
|
||||||
|
if (!message || !message.address || !signature) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Отсутствуют необходимые данные для верификации'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const address = message.address.toLowerCase();
|
||||||
|
console.log("Адрес из сообщения:", address);
|
||||||
|
|
||||||
|
// Проверяем, является ли пользователь администратором
|
||||||
|
const isAdmin = true; // Для примера всегда true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const siweMessage = new SiweMessage(message);
|
||||||
|
const fields = await siweMessage.validate(signature);
|
||||||
|
|
||||||
|
if (fields.address.toLowerCase() !== address.toLowerCase()) {
|
||||||
|
return res.status(401).json({ success: false, error: 'Invalid signature' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем подпись
|
// Только после проверки устанавливаем сессию
|
||||||
const fields = await siweMessage.verify({
|
req.session.authenticated = true;
|
||||||
signature: signature,
|
req.session.address = fields.address;
|
||||||
domain: siweMessage.domain,
|
req.session.lastSignature = signature;
|
||||||
nonce: clientNonce
|
|
||||||
|
// Сохраняем сессию
|
||||||
|
req.session.save();
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(401).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем данные в сессии
|
||||||
|
req.session.isAuthenticated = true;
|
||||||
|
req.session.isAdmin = isAdmin;
|
||||||
|
|
||||||
|
// Явно сохраняем сессию
|
||||||
|
req.session.save((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка сохранения сессии:', err);
|
||||||
|
return res.status(500).json({ error: 'Session save error' });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Сессия успешно сохранена:', {
|
||||||
|
sessionID: req.sessionID,
|
||||||
|
session: {
|
||||||
|
isAuthenticated: req.session.isAuthenticated,
|
||||||
|
authenticated: req.session.authenticated,
|
||||||
|
address: req.session.address,
|
||||||
|
isAdmin: req.session.isAdmin
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('SIWE validation successful:', fields);
|
res.cookie('authToken', 'true', {
|
||||||
console.log('Сообщение успешно верифицировано');
|
maxAge: 86400000,
|
||||||
|
httpOnly: false,
|
||||||
// Сохраняем данные в сессии
|
secure: false,
|
||||||
req.session.siwe = fields.data;
|
sameSite: 'lax',
|
||||||
req.session.authenticated = true;
|
path: '/'
|
||||||
req.session.nonce = null;
|
|
||||||
|
|
||||||
// Принудительно сохраняем сессию
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
req.session.save((err) => {
|
|
||||||
if (err) reject(err);
|
|
||||||
else resolve();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
address: fields.data.address
|
address: address,
|
||||||
|
isAdmin: isAdmin
|
||||||
});
|
});
|
||||||
} catch (error) {
|
});
|
||||||
console.error('Ошибка валидации сообщения:', error);
|
|
||||||
req.session.authenticated = false;
|
|
||||||
req.session.siwe = null;
|
|
||||||
req.session.nonce = null;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка верификации:', error);
|
console.error("Ошибка верификации:", error);
|
||||||
res.status(400).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message,
|
error: error.message || 'Внутренняя ошибка сервера'
|
||||||
details: error.stack
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Получение сессии
|
// Добавьте после настройки маршрутов
|
||||||
app.get('/session', (req, res) => {
|
app.get('/api/session', (req, res) => {
|
||||||
try {
|
console.log('Запрос сессии в server.js:', {
|
||||||
if (!req.session) {
|
sessionExists: !!req.session,
|
||||||
return res.status(401).json({
|
sessionID: req.sessionID,
|
||||||
authenticated: false,
|
isAuthenticated: req.session?.isAuthenticated,
|
||||||
error: 'No session'
|
authenticated: req.session?.authenticated,
|
||||||
});
|
address: req.session?.address
|
||||||
}
|
});
|
||||||
|
|
||||||
|
if (req.session && (req.session.isAuthenticated || req.session.authenticated)) {
|
||||||
res.json({
|
res.json({
|
||||||
authenticated: !!req.session.authenticated,
|
isAuthenticated: true,
|
||||||
address: req.session.siwe?.address
|
authenticated: true,
|
||||||
|
address: req.session.address,
|
||||||
|
isAdmin: req.session.isAdmin
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.json({
|
||||||
|
isAuthenticated: false,
|
||||||
|
authenticated: false,
|
||||||
|
address: null,
|
||||||
|
isAdmin: false
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка получения сессии:', error);
|
|
||||||
res.status(500).json({ error: 'Internal Server Error' });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Выход
|
app.get('/api/balance', requireAuth, async (req, res) => {
|
||||||
app.get('/signout', (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
req.session.destroy((err) => {
|
const balance = await contract.balanceOf(req.session.address);
|
||||||
if (err) {
|
res.json({ balance: balance.toString() });
|
||||||
console.error('Ошибка при удалении сессии:', err);
|
|
||||||
return res.status(500).json({ error: 'Failed to destroy session' });
|
|
||||||
}
|
|
||||||
res.status(200).json({ success: true });
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка выхода:', error);
|
res.status(500).json({ error: error.message });
|
||||||
res.status(500).json({ error: 'Internal Server Error' });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Базовый маршрут
|
// Добавляем тестовые маршруты API
|
||||||
app.get('/', (req, res) => {
|
app.get('/api/public', (req, res) => {
|
||||||
|
res.json({ message: 'This is a public API endpoint' });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/protected', (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
status: 'ok',
|
message: 'This is a protected API endpoint',
|
||||||
endpoints: {
|
user: {
|
||||||
nonce: 'GET /nonce',
|
address: req.session.address,
|
||||||
verify: 'POST /verify',
|
isAdmin: req.session.isAdmin
|
||||||
session: 'GET /session',
|
|
||||||
signout: 'GET /signout',
|
|
||||||
shutdown: 'POST /shutdown'
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Эндпоинт для остановки сервера
|
app.get('/api/admin', (req, res) => {
|
||||||
app.post('/shutdown', (req, res) => {
|
res.json({
|
||||||
res.json({ message: 'Сервер останавливается...' });
|
message: 'This is an admin API endpoint',
|
||||||
console.log('Получен запрос на остановку сервера');
|
user: {
|
||||||
setTimeout(() => {
|
address: req.session.address,
|
||||||
process.exit(0);
|
isAdmin: req.session.isAdmin
|
||||||
}, 1000);
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// 4. API роуты
|
|
||||||
app.use('/api', apiRoutes);
|
|
||||||
|
|
||||||
// Обработка 404
|
|
||||||
app.use((req, res) => {
|
|
||||||
console.log(`404: ${req.method} ${req.url}`);
|
|
||||||
res.status(404).json({
|
|
||||||
error: 'Not Found',
|
|
||||||
message: `Endpoint ${req.method} ${req.url} не существует`
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Обработка ошибок
|
// Добавьте обработчик ошибок
|
||||||
app.use((err, req, res, next) => {
|
app.use((err, req, res, next) => {
|
||||||
console.error('Ошибка сервера:', err);
|
console.error('Глобальный обработчик ошибок:', err);
|
||||||
res.status(500).json({
|
|
||||||
error: 'Internal Server Error',
|
// Обработка ошибок CSRF
|
||||||
message: err.message
|
if (err.code === 'EBADCSRFTOKEN') {
|
||||||
|
return res.status(403).json({
|
||||||
|
error: 'Недействительный CSRF-токен',
|
||||||
|
message: 'Возможно, ваша сессия истекла. Пожалуйста, обновите страницу и попробуйте снова.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка ошибок валидации
|
||||||
|
if (err.name === 'ValidationError') {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Ошибка валидации',
|
||||||
|
details: err.details || err.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка ошибок базы данных
|
||||||
|
if (err.code === '23505') { // Postgres unique violation
|
||||||
|
return res.status(409).json({
|
||||||
|
error: 'Конфликт данных',
|
||||||
|
message: 'Запись с такими данными уже существует.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Общая обработка ошибок
|
||||||
|
res.status(err.status || 500).json({
|
||||||
|
error: 'Внутренняя ошибка сервера',
|
||||||
|
message: process.env.NODE_ENV === 'production' ? 'Что-то пошло не так' : err.message
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3000;
|
// Перед запуском сервера
|
||||||
const server = app.listen(PORT, '127.0.0.1', () => {
|
console.log('Перед запуском сервера на порту:', PORT);
|
||||||
console.log(`SIWE сервер запущен на порту ${PORT}`);
|
|
||||||
console.log('Доступные эндпоинты:');
|
|
||||||
console.log(' GET / - Информация о сервере');
|
|
||||||
console.log(' GET /nonce - Получить nonce');
|
|
||||||
console.log(' POST /verify - Верифицировать сообщение');
|
|
||||||
console.log(' GET /session - Получить текущую сессию');
|
|
||||||
console.log(' GET /signout - Выйти из системы');
|
|
||||||
console.log(' POST /shutdown - Остановить сервер');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Обработка ошибки занятого порта
|
// Запуск сервера и инициализация сервисов
|
||||||
server.on('error', (error) => {
|
const server = app.listen(PORT, '0.0.0.0', async () => {
|
||||||
if (error.code === 'EADDRINUSE') {
|
console.log(`Server is running on port ${PORT}`);
|
||||||
console.log(`Порт ${PORT} занят. Останавливаем предыдущий процесс...`);
|
console.log('Server address:', server.address());
|
||||||
require('child_process').exec(`lsof -i :${PORT} | grep LISTEN | awk '{print $2}' | xargs kill -9`, (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Ошибка при остановке процесса:', err);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
console.log('Предыдущий процесс остановлен. Перезапускаем сервер...');
|
|
||||||
server.listen(PORT);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Функция корректного завершения работы
|
// Инициализируем сервисы без блокировки запуска сервера
|
||||||
async function gracefulShutdown(signal) {
|
initServices().catch(err => {
|
||||||
if (isShuttingDown) return;
|
console.error('Ошибка при инициализации сервисов:', err);
|
||||||
isShuttingDown = true;
|
});
|
||||||
|
|
||||||
console.log(`Получен сигнал ${signal}, начинаем корректное завершение...`);
|
|
||||||
|
|
||||||
|
// Проверяем доступность Ollama в фоновом режиме
|
||||||
try {
|
try {
|
||||||
if (telegramBot) {
|
const { checkOllamaAvailability } = require('./services/ollama');
|
||||||
console.log('Останавливаем Telegram бота...');
|
checkOllamaAvailability().catch(err => {
|
||||||
await telegramBot.stop();
|
console.error('Ошибка при проверке Ollama:', err);
|
||||||
}
|
});
|
||||||
if (emailBot) {
|
|
||||||
console.log('Останавливаем Email бота...');
|
|
||||||
await emailBot.stop();
|
|
||||||
}
|
|
||||||
console.log('Все сервисы остановлены');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при остановке сервисов:', error);
|
console.error('Ошибка при импорте модуля Ollama:', error);
|
||||||
}
|
}
|
||||||
|
}).on('error', (err) => {
|
||||||
|
if (err.code === 'EADDRINUSE') {
|
||||||
|
console.error(`Port ${PORT} is already in use. Please try another port.`);
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
console.error('Server error:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Даем время на завершение всех процессов
|
// Добавляем graceful shutdown
|
||||||
setTimeout(() => {
|
process.on('SIGTERM', () => {
|
||||||
|
console.log('SIGTERM signal received: closing HTTP server');
|
||||||
|
server.close(() => {
|
||||||
|
console.log('HTTP server closed');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}, 1000);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Обработчики сигналов
|
|
||||||
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
||||||
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
||||||
process.on('SIGUSR2', () => gracefulShutdown('SIGUSR2')); // Для nodemon
|
|
||||||
|
|
||||||
// Обработка необработанных исключений
|
|
||||||
process.on('uncaughtException', (error) => {
|
|
||||||
console.error('Необработанное исключение:', error);
|
|
||||||
gracefulShutdown('uncaughtException');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason, promise) => {
|
// Проверяем доступность Ollama сервера
|
||||||
console.error('Необработанное отклонение промиса:', reason);
|
async function checkOllamaServer() {
|
||||||
gracefulShutdown('unhandledRejection');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Функция для проверки и остановки существующих процессов бота
|
|
||||||
async function killExistingBotProcesses() {
|
|
||||||
try {
|
try {
|
||||||
console.log('Проверяем существующие процессы бота...');
|
const response = await axios.get('http://localhost:11434/api/tags');
|
||||||
const { stdout } = await execAsync('ps aux | grep "[n]ode.*telegram"');
|
if (response.status === 200) {
|
||||||
|
console.log('Ollama сервер доступен');
|
||||||
|
|
||||||
if (stdout) {
|
// Тестируем прямой запрос к Ollama
|
||||||
console.log('Найдены существующие процессы бота, останавливаем...');
|
try {
|
||||||
await execAsync('pkill -SIGTERM -f "[n]ode.*telegram"');
|
console.log('Тестируем прямой запрос к Ollama...');
|
||||||
// Ждем немного, чтобы процессы успели завершиться
|
const model = new ChatOllama({
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
baseUrl: 'http://localhost:11434',
|
||||||
|
model: 'llama3',
|
||||||
|
temperature: 0.2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await model.invoke('Привет, как дела?');
|
||||||
|
console.log('Ответ от Ollama:', result);
|
||||||
|
} catch (testError) {
|
||||||
|
console.error('Ошибка при тестировании Ollama:', testError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализируем векторное хранилище
|
||||||
|
try {
|
||||||
|
console.log('Инициализируем векторное хранилище...');
|
||||||
|
const vectorStore = await getVectorStore();
|
||||||
|
console.log('Векторное хранилище инициализировано');
|
||||||
|
} catch (vectorError) {
|
||||||
|
console.error('Ошибка при инициализации векторного хранилища:', vectorError);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ошибка означает, что процессы не найдены - это нормально
|
console.error('Ollama сервер недоступен:', error.message);
|
||||||
console.log('Активные процессы бота не найдены');
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Инициализация векторного хранилища
|
// Настройка периодической очистки устаревших сессий
|
||||||
async function initializeVectorStore() {
|
const pgSessionCleanup = setInterval(function() {
|
||||||
|
console.log('Cleaning up expired sessions...');
|
||||||
|
pool.query('DELETE FROM session WHERE expire < NOW()')
|
||||||
|
.then(result => {
|
||||||
|
if (result.rowCount > 0) {
|
||||||
|
console.log(`Removed ${result.rowCount} expired sessions`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.error('Error cleaning up sessions:', err));
|
||||||
|
}, 3600000); // Очистка каждый час
|
||||||
|
|
||||||
|
// Очистка интервала при завершении работы
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
clearInterval(pgSessionCleanup);
|
||||||
|
console.log('SIGTERM signal received: closing HTTP server');
|
||||||
|
server.close(() => {
|
||||||
|
console.log('HTTP server closed');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Функция для создания таблиц
|
||||||
|
async function ensureTablesExist() {
|
||||||
try {
|
try {
|
||||||
// Сначала останавливаем существующие процессы
|
// Проверяем существование таблицы users
|
||||||
await killExistingBotProcesses();
|
const result = await pool.query(`
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'users'
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
// Даем время на освобождение ресурсов
|
// Если таблица не существует, создаем все таблицы
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
if (!result.rows[0].exists) {
|
||||||
|
console.log('Таблицы не найдены, создаем...');
|
||||||
|
|
||||||
// Инициализируем embeddings
|
// SQL-запросы для создания таблиц
|
||||||
const embeddings = new OpenAIEmbeddings({
|
const createTablesSql = `
|
||||||
openAIApiKey: process.env.OPENAI_API_KEY,
|
-- Таблица пользователей
|
||||||
configuration: {
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
baseURL: process.env.OPENAI_API_BASE || 'https://api.openai.com/v1'
|
id SERIAL PRIMARY KEY,
|
||||||
}
|
address VARCHAR(255) UNIQUE,
|
||||||
});
|
email VARCHAR(255) UNIQUE,
|
||||||
|
telegram_id VARCHAR(255) UNIQUE,
|
||||||
|
username VARCHAR(255),
|
||||||
|
is_admin BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
// Инициализируем векторное хранилище
|
-- Индексы для таблицы пользователей
|
||||||
vectorStore = await PGVectorStore.initialize(embeddings, {
|
CREATE INDEX IF NOT EXISTS idx_users_address ON users(address);
|
||||||
postgresConnectionOptions: {
|
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||||
connectionString: process.env.DATABASE_URL
|
CREATE INDEX IF NOT EXISTS idx_users_telegram_id ON users(telegram_id);
|
||||||
},
|
|
||||||
tableName: 'documents',
|
|
||||||
columns: {
|
|
||||||
idColumnName: 'id',
|
|
||||||
vectorColumnName: 'embedding',
|
|
||||||
contentColumnName: 'content',
|
|
||||||
metadataColumnName: 'metadata',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Векторное хранилище инициализировано');
|
-- Таблица сессий
|
||||||
|
CREATE TABLE IF NOT EXISTS session (
|
||||||
|
sid VARCHAR NOT NULL,
|
||||||
|
sess JSON NOT NULL,
|
||||||
|
expire TIMESTAMP(6) NOT NULL,
|
||||||
|
CONSTRAINT session_pkey PRIMARY KEY (sid)
|
||||||
|
);
|
||||||
|
|
||||||
// Инициализируем ботов только после успешной инициализации хранилища
|
-- Индекс для таблицы сессий
|
||||||
if (!telegramBot) {
|
CREATE INDEX IF NOT EXISTS idx_session_expire ON session(expire);
|
||||||
telegramBot = new TelegramBotService(process.env.TELEGRAM_BOT_TOKEN, vectorStore);
|
|
||||||
await telegramBot.initialize().catch(error => {
|
-- Таблица канбан-досок
|
||||||
if (error.code === 409) {
|
CREATE TABLE IF NOT EXISTS kanban_boards (
|
||||||
console.log('Telegram бот уже запущен в другом процессе');
|
id SERIAL PRIMARY KEY,
|
||||||
} else {
|
title VARCHAR(255) NOT NULL,
|
||||||
throw error;
|
description TEXT,
|
||||||
}
|
owner_id INTEGER REFERENCES users(id),
|
||||||
});
|
is_public BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Таблица колонок канбан-доски
|
||||||
|
CREATE TABLE IF NOT EXISTS kanban_columns (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
board_id INTEGER REFERENCES kanban_boards(id) ON DELETE CASCADE,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
position INTEGER NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Таблица карточек канбан-доски
|
||||||
|
CREATE TABLE IF NOT EXISTS kanban_cards (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
column_id INTEGER REFERENCES kanban_columns(id) ON DELETE CASCADE,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
position INTEGER NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Индексы для таблиц канбан
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_kanban_boards_owner ON kanban_boards(owner_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_kanban_columns_board ON kanban_columns(board_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_kanban_cards_column ON kanban_cards(column_id);
|
||||||
|
|
||||||
|
-- Таблица сообщений чата
|
||||||
|
CREATE TABLE IF NOT EXISTS chat_messages (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER REFERENCES users(id),
|
||||||
|
sender VARCHAR(50) NOT NULL,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Индекс для таблицы сообщений
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chat_messages_user ON chat_messages(user_id);
|
||||||
|
`;
|
||||||
|
|
||||||
|
await pool.query(createTablesSql);
|
||||||
|
console.log('Таблицы успешно созданы');
|
||||||
|
} else {
|
||||||
|
console.log('Таблицы уже существуют');
|
||||||
}
|
}
|
||||||
if (!emailBot) {
|
|
||||||
emailBot = new EmailBotService(vectorStore);
|
|
||||||
}
|
|
||||||
|
|
||||||
return vectorStore;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при инициализации:', error);
|
console.error('Ошибка при проверке/создании таблиц:', error);
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Запускаем инициализацию
|
// Вызываем функцию при запуске сервера
|
||||||
console.log('Начинаем инициализацию векторного хранилища...');
|
ensureTablesExist();
|
||||||
initializeVectorStore().catch(error => {
|
|
||||||
console.error('Критическая ошибка при инициализации:', error);
|
// Добавляем middleware для проверки аутентификации
|
||||||
process.exit(1);
|
app.use('/api/protected', (req, res, next) => {
|
||||||
|
// console.log('Protected route middleware:', {
|
||||||
|
// session: req.session,
|
||||||
|
// authenticated: req.session.authenticated,
|
||||||
|
// address: req.session.address
|
||||||
|
// });
|
||||||
|
|
||||||
|
if (!req.session.authenticated) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавляем middleware для проверки прав администратора
|
||||||
|
app.use('/api/admin', (req, res, next) => {
|
||||||
|
// console.log('Admin route middleware:', {
|
||||||
|
// session: req.session,
|
||||||
|
// authenticated: req.session.authenticated,
|
||||||
|
// isAdmin: req.session.isAdmin
|
||||||
|
// });
|
||||||
|
|
||||||
|
if (!req.session.authenticated || !req.session.isAdmin) {
|
||||||
|
return res.status(403).json({ error: 'Forbidden' });
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
});
|
});
|
||||||
47
backend/services/documents.js
Normal file
47
backend/services/documents.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { Document } = require('langchain/document');
|
||||||
|
const { RecursiveCharacterTextSplitter } = require('langchain/text_splitter');
|
||||||
|
|
||||||
|
// Функция для загрузки документов из файлов
|
||||||
|
async function loadDocumentsFromFiles(directory) {
|
||||||
|
const documents = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const files = fs.readdirSync(directory);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(directory, file);
|
||||||
|
const stat = fs.statSync(filePath);
|
||||||
|
|
||||||
|
if (stat.isFile() && (file.endsWith('.txt') || file.endsWith('.md'))) {
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
|
||||||
|
documents.push(
|
||||||
|
new Document({
|
||||||
|
pageContent: content,
|
||||||
|
metadata: {
|
||||||
|
source: filePath,
|
||||||
|
filename: file,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Разделяем документы на чанки
|
||||||
|
const textSplitter = new RecursiveCharacterTextSplitter({
|
||||||
|
chunkSize: 1000,
|
||||||
|
chunkOverlap: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
const splitDocs = await textSplitter.splitDocuments(documents);
|
||||||
|
|
||||||
|
return splitDocs;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading documents:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { loadDocumentsFromFiles };
|
||||||
@@ -6,280 +6,63 @@ const Imap = require('imap');
|
|||||||
const { simpleParser } = require('mailparser');
|
const { simpleParser } = require('mailparser');
|
||||||
const { checkMailServer } = require('../utils/checkMail');
|
const { checkMailServer } = require('../utils/checkMail');
|
||||||
const { sleep, isValidEmail } = require('../utils/helpers');
|
const { sleep, isValidEmail } = require('../utils/helpers');
|
||||||
|
const { linkIdentity, getUserIdByIdentity } = require('../utils/identity-linker');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
class EmailBotService {
|
class EmailBotService {
|
||||||
constructor(vectorStore) {
|
constructor() {
|
||||||
if (!vectorStore) {
|
this.enabled = false;
|
||||||
throw new Error('Vector store is required');
|
console.log('EmailBotService: Сервис отключен (заглушка)');
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Инициализация Email бота...');
|
|
||||||
console.log('Проверяем настройки почты:', {
|
|
||||||
smtp: {
|
|
||||||
host: process.env.EMAIL_SMTP_HOST,
|
|
||||||
port: process.env.EMAIL_SMTP_PORT
|
|
||||||
},
|
|
||||||
imap: {
|
|
||||||
host: process.env.EMAIL_IMAP_HOST,
|
|
||||||
port: process.env.EMAIL_IMAP_PORT
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Инициализация базы данных
|
|
||||||
this.pool = new Pool({
|
|
||||||
connectionString: process.env.DATABASE_URL
|
|
||||||
});
|
|
||||||
|
|
||||||
this.vectorStore = vectorStore;
|
|
||||||
|
|
||||||
// Инициализация LLM
|
|
||||||
this.chat = new ChatOllama({
|
|
||||||
model: 'mistral',
|
|
||||||
baseUrl: 'http://localhost:11434'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Настройка почтового клиента для отправки
|
|
||||||
this.transporter = nodemailer.createTransport({
|
|
||||||
host: process.env.EMAIL_SMTP_HOST,
|
|
||||||
port: process.env.EMAIL_SMTP_PORT,
|
|
||||||
secure: true,
|
|
||||||
auth: {
|
|
||||||
user: process.env.EMAIL_USER,
|
|
||||||
pass: process.env.EMAIL_PASSWORD
|
|
||||||
},
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false,
|
|
||||||
minVersion: 'TLSv1',
|
|
||||||
ciphers: 'HIGH:MEDIUM:!aNULL:!eNULL:!NULL:!DH:!EDH:!EXP:!LOW:!SSLv2:!MD5'
|
|
||||||
},
|
|
||||||
debug: true,
|
|
||||||
logger: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Проверяем подключение к SMTP
|
|
||||||
this.transporter.verify((error, success) => {
|
|
||||||
if (error) {
|
|
||||||
console.error('Ошибка подключения к SMTP:', {
|
|
||||||
name: error.name,
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
command: error.command,
|
|
||||||
stack: error.stack
|
|
||||||
});
|
|
||||||
setTimeout(() => this.initSMTP(), 30000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Настройка IMAP для получения писем
|
|
||||||
const imapConfig = {
|
|
||||||
user: process.env.EMAIL_USER,
|
|
||||||
password: process.env.EMAIL_PASSWORD,
|
|
||||||
host: process.env.EMAIL_IMAP_HOST,
|
|
||||||
port: process.env.EMAIL_IMAP_PORT,
|
|
||||||
tls: true,
|
|
||||||
tlsOptions: { rejectUnauthorized: false },
|
|
||||||
keepalive: true,
|
|
||||||
authTimeout: 30000,
|
|
||||||
connTimeout: 30000
|
|
||||||
}
|
|
||||||
this.imap = new Imap(imapConfig);
|
|
||||||
|
|
||||||
// Добавляем обработчик для всех событий IMAP
|
|
||||||
this.imap.on('*', function(event, data) {
|
|
||||||
console.log('IMAP Event:', event, data);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Проверяем MX записи
|
|
||||||
const domain = process.env.EMAIL_USER ? process.env.EMAIL_USER.split('@')[1] : '';
|
|
||||||
if (domain) {
|
|
||||||
checkMailServer(domain).then(records => {
|
|
||||||
if (!records) {
|
|
||||||
console.error('Не удалось найти MX записи для домена');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.error('EMAIL_USER не настроен в .env файле');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isRunning = false;
|
|
||||||
this.initSMTP();
|
|
||||||
this.initIMAP();
|
|
||||||
console.log('Email bot service initialized');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async initSMTP() {
|
async start() {
|
||||||
try {
|
console.log('EmailBotService: Запуск сервиса отключен (заглушка)');
|
||||||
console.log('Попытка подключения к SMTP...');
|
return false;
|
||||||
await this.transporter.verify();
|
|
||||||
console.log('SMTP сервер готов к отправке сообщений');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка подключения к SMTP:', {
|
|
||||||
name: error.name,
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
command: error.command,
|
|
||||||
stack: error.stack
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async initIMAP() {
|
|
||||||
try {
|
|
||||||
await this.initEmailListener();
|
|
||||||
console.log('IMAP подключение установлено');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка инициализации IMAP:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async initEmailListener() {
|
|
||||||
try {
|
|
||||||
this.imap.on('ready', () => {
|
|
||||||
this.imap.openBox('INBOX', false, (err, box) => {
|
|
||||||
if (err) throw err;
|
|
||||||
|
|
||||||
// Слушаем новые письма
|
|
||||||
this.imap.on('mail', () => {
|
|
||||||
this.checkNewEmails();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.imap.on('error', (err) => {
|
|
||||||
console.log('IMAP ошибка:', err);
|
|
||||||
if (err.source === 'timeout-auth') {
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log('Попытка переподключения к IMAP...');
|
|
||||||
this.imap.connect();
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.imap.connect();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при инициализации IMAP:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async processEmail(message) {
|
|
||||||
try {
|
|
||||||
// Очищаем и валидируем email адрес
|
|
||||||
const cleanEmail = message.from.replace(/[<>]/g, '').trim();
|
|
||||||
if (!isValidEmail(cleanEmail)) {
|
|
||||||
console.log('Некорректный email адрес:', message.from);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем, не является ли отправитель no-reply адресом
|
|
||||||
if (cleanEmail.toLowerCase().includes('no-reply') ||
|
|
||||||
cleanEmail.toLowerCase().includes('noreply')) {
|
|
||||||
console.log('Пропускаем письмо от no-reply адреса:', cleanEmail);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем валидность домена получателя
|
|
||||||
const domain = cleanEmail.split('@')[1];
|
|
||||||
try {
|
|
||||||
console.log(`Проверяем MX записи для домена ${domain}...`);
|
|
||||||
const records = await checkMailServer(domain);
|
|
||||||
if (!records || records.length === 0) {
|
|
||||||
console.log('Пропускаем письмо - домен не найден:', domain);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log('Найдены MX записи:', records);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Ошибка при проверке MX записей:', err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получаем ответ от Ollama
|
|
||||||
const result = await this.chat.invoke(message.text);
|
|
||||||
|
|
||||||
// Отправляем ответ
|
|
||||||
await this.transporter.sendMail({
|
|
||||||
from: process.env.EMAIL_USER,
|
|
||||||
to: cleanEmail,
|
|
||||||
subject: `Re: ${message.subject}`,
|
|
||||||
text: result.content
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Ответ отправлен:', {
|
|
||||||
to: cleanEmail,
|
|
||||||
subject: message.subject
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при обработке email:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkNewEmails() {
|
|
||||||
try {
|
|
||||||
const messages = await new Promise((resolve, reject) => {
|
|
||||||
this.imap.search(['UNSEEN'], (err, results) => {
|
|
||||||
if (err) reject(err);
|
|
||||||
|
|
||||||
if (!results || !results.length) {
|
|
||||||
resolve([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetch = this.imap.fetch(results, {
|
|
||||||
bodies: '',
|
|
||||||
markSeen: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const messages = [];
|
|
||||||
|
|
||||||
fetch.on('message', (msg) => {
|
|
||||||
msg.on('body', (stream) => {
|
|
||||||
let buffer = '';
|
|
||||||
stream.on('data', (chunk) => {
|
|
||||||
buffer += chunk.toString('utf8');
|
|
||||||
});
|
|
||||||
stream.once('end', () => {
|
|
||||||
messages.push({
|
|
||||||
from: buffer.match(/From: (.*)/i)?.[1],
|
|
||||||
subject: buffer.match(/Subject: (.*)/i)?.[1],
|
|
||||||
text: buffer.split('\n\n').slice(1).join('\n\n')
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
fetch.once('error', reject);
|
|
||||||
fetch.once('end', () => resolve(messages));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Добавляем задержку между обработкой писем
|
|
||||||
for (const message of messages) {
|
|
||||||
await this.processEmail(message);
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000)); // 1 секунда между письмами
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при проверке новых писем:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop() {
|
async stop() {
|
||||||
if (this.isRunning) {
|
console.log('EmailBotService: Остановка сервиса отключена (заглушка)');
|
||||||
console.log('Останавливаем Email бота...');
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Закрываем SMTP соединение
|
isEnabled() {
|
||||||
if (this.transporter) {
|
return this.enabled;
|
||||||
await this.transporter.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Закрываем IMAP соединение
|
|
||||||
if (this.imap) {
|
|
||||||
this.imap.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isRunning = false;
|
|
||||||
console.log('Email бот остановлен');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// В обработчике команд добавьте код для связывания аккаунтов
|
||||||
|
async function processCommand(email, command, args) {
|
||||||
|
if (command === 'link' && args.length > 0) {
|
||||||
|
const ethAddress = args[0];
|
||||||
|
|
||||||
|
// Проверяем формат Ethereum-адреса
|
||||||
|
if (!/^0x[a-fA-F0-9]{40}$/.test(ethAddress)) {
|
||||||
|
return 'Неверный формат Ethereum-адреса. Используйте формат 0x...';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Получаем ID пользователя по Ethereum-адресу
|
||||||
|
const userId = await getUserIdByIdentity('ethereum', ethAddress);
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return 'Пользователь с таким Ethereum-адресом не найден. Сначала войдите через веб-интерфейс.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Связываем Email-аккаунт с пользователем
|
||||||
|
const success = await linkIdentity(userId, 'email', email);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
return `Ваш Email-аккаунт успешно связан с Ethereum-адресом ${ethAddress}`;
|
||||||
|
} else {
|
||||||
|
return 'Не удалось связать аккаунты. Возможно, этот Email-аккаунт уже связан с другим пользователем.';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при связывании аккаунтов:', error);
|
||||||
|
return 'Произошла ошибка при связывании аккаунтов. Попробуйте позже.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка других команд...
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = EmailBotService;
|
module.exports = EmailBotService;
|
||||||
149
backend/services/ollama.js
Normal file
149
backend/services/ollama.js
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
const { ChatOllama } = require('@langchain/ollama');
|
||||||
|
const { RetrievalQAChain } = require("langchain/chains");
|
||||||
|
const { PromptTemplate } = require("@langchain/core/prompts");
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
// Создаем шаблон для контекстного запроса
|
||||||
|
const PROMPT_TEMPLATE = `
|
||||||
|
Ты - AI-ассистент для бизнеса, специализирующийся на блокчейн-технологиях и Web3.
|
||||||
|
Используй следующий контекст для ответа на вопрос пользователя.
|
||||||
|
Если ты не знаешь ответа, просто скажи, что не знаешь, не пытайся придумать ответ.
|
||||||
|
|
||||||
|
Контекст: {context}
|
||||||
|
|
||||||
|
Вопрос: {query}
|
||||||
|
|
||||||
|
Ответ:
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Функция для проверки доступности Ollama
|
||||||
|
async function checkOllamaAvailability() {
|
||||||
|
console.log('Проверка доступности Ollama...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Добавляем таймаут для запроса
|
||||||
|
const response = await axios.get('http://localhost:11434/api/tags', {
|
||||||
|
timeout: 5000 // 5 секунд таймаут
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
console.log('Ollama доступен. Доступные модели:');
|
||||||
|
if (response.data && response.data.models) {
|
||||||
|
response.data.models.forEach(model => {
|
||||||
|
console.log(`- ${model.name}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ollama недоступен:', error.message);
|
||||||
|
console.log('Приложение продолжит работу без Ollama');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для прямого запроса к Ollama API
|
||||||
|
async function directOllamaQuery(message, model = 'mistral') {
|
||||||
|
try {
|
||||||
|
console.log(`Отправка запроса к Ollama (модель: ${model}):`, message);
|
||||||
|
|
||||||
|
// Проверяем доступность Ollama перед отправкой запроса
|
||||||
|
const isAvailable = await checkOllamaAvailability();
|
||||||
|
if (!isAvailable) {
|
||||||
|
throw new Error('Сервер Ollama недоступен');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем экземпляр ChatOllama
|
||||||
|
const ollama = new ChatOllama({
|
||||||
|
baseUrl: 'http://localhost:11434',
|
||||||
|
model: model,
|
||||||
|
temperature: 0.7,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Отправка запроса к Ollama...');
|
||||||
|
const result = await ollama.invoke(message);
|
||||||
|
console.log('Получен ответ от Ollama');
|
||||||
|
|
||||||
|
return result.content;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при запросе к Ollama:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для создания цепочки Ollama с RAG
|
||||||
|
async function createOllamaChain(vectorStore) {
|
||||||
|
try {
|
||||||
|
console.log('Создаем модель Ollama...');
|
||||||
|
// Создаем модель Ollama
|
||||||
|
const model = new ChatOllama({
|
||||||
|
baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
|
||||||
|
model: process.env.OLLAMA_MODEL || 'mistral',
|
||||||
|
temperature: 0.2,
|
||||||
|
timeout: 60000, // 60 секунд таймаут
|
||||||
|
});
|
||||||
|
console.log('Модель Ollama создана');
|
||||||
|
|
||||||
|
// Проверяем модель прямым запросом
|
||||||
|
try {
|
||||||
|
console.log('Тестируем модель прямым запросом...');
|
||||||
|
const testResponse = await model.invoke('Тестовый запрос');
|
||||||
|
console.log('Тест модели успешен:', testResponse);
|
||||||
|
} catch (testError) {
|
||||||
|
console.error('Ошибка при тестировании модели:', testError);
|
||||||
|
// Продолжаем выполнение, даже если тест не прошел
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Создаем шаблон запроса...');
|
||||||
|
// Создаем шаблон запроса
|
||||||
|
const prompt = new PromptTemplate({
|
||||||
|
template: PROMPT_TEMPLATE,
|
||||||
|
inputVariables: ["context", "query"],
|
||||||
|
});
|
||||||
|
console.log('Шаблон запроса создан');
|
||||||
|
|
||||||
|
console.log('Получаем retriever из векторного хранилища...');
|
||||||
|
const retriever = vectorStore.asRetriever();
|
||||||
|
console.log('Retriever получен');
|
||||||
|
|
||||||
|
console.log('Создаем цепочку для поиска и ответа...');
|
||||||
|
// Создаем цепочку для поиска и ответа
|
||||||
|
const chain = RetrievalQAChain.fromLLM(
|
||||||
|
model,
|
||||||
|
retriever,
|
||||||
|
{
|
||||||
|
returnSourceDocuments: true,
|
||||||
|
prompt: prompt,
|
||||||
|
inputKey: "query",
|
||||||
|
outputKey: "text",
|
||||||
|
verbose: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log('Цепочка для поиска и ответа создана');
|
||||||
|
|
||||||
|
return chain;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating Ollama chain:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для получения модели Ollama
|
||||||
|
async function getOllamaModel() {
|
||||||
|
try {
|
||||||
|
// Создаем модель Ollama
|
||||||
|
const model = new ChatOllama({
|
||||||
|
baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
|
||||||
|
model: process.env.OLLAMA_MODEL || 'mistral',
|
||||||
|
temperature: 0.2,
|
||||||
|
timeout: 60000, // 60 секунд таймаут
|
||||||
|
});
|
||||||
|
|
||||||
|
return model;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating Ollama model:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { getOllamaModel, createOllamaChain, checkOllamaAvailability, directOllamaQuery };
|
||||||
@@ -6,10 +6,12 @@ require('dotenv').config();
|
|||||||
const { sleep } = require('../utils/helpers');
|
const { sleep } = require('../utils/helpers');
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
const exec = util.promisify(require('child_process').exec);
|
const exec = util.promisify(require('child_process').exec);
|
||||||
|
const { linkIdentity, getUserIdByIdentity } = require('../utils/identity-linker');
|
||||||
|
|
||||||
class TelegramBotService {
|
class TelegramBotService {
|
||||||
constructor(token) {
|
constructor() {
|
||||||
if (!token) {
|
// Проверяем наличие токена
|
||||||
|
if (!process.env.TELEGRAM_BOT_TOKEN) {
|
||||||
throw new Error('Token is required');
|
throw new Error('Token is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,7 +20,7 @@ class TelegramBotService {
|
|||||||
this.retryDelay = 5000; // 5 секунд между попытками
|
this.retryDelay = 5000; // 5 секунд между попытками
|
||||||
|
|
||||||
// Создаем бота без polling
|
// Создаем бота без polling
|
||||||
this.bot = new TelegramBot(token, {
|
this.bot = new TelegramBot(process.env.TELEGRAM_BOT_TOKEN, {
|
||||||
polling: false,
|
polling: false,
|
||||||
request: {
|
request: {
|
||||||
proxy: null,
|
proxy: null,
|
||||||
@@ -30,7 +32,7 @@ class TelegramBotService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.token = token;
|
this.token = process.env.TELEGRAM_BOT_TOKEN;
|
||||||
this.chat = new ChatOllama({
|
this.chat = new ChatOllama({
|
||||||
model: 'mistral',
|
model: 'mistral',
|
||||||
baseUrl: 'http://localhost:11434'
|
baseUrl: 'http://localhost:11434'
|
||||||
@@ -45,6 +47,8 @@ class TelegramBotService {
|
|||||||
minVersion: 'TLSv1.2'
|
minVersion: 'TLSv1.2'
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
setupHandlers() {
|
setupHandlers() {
|
||||||
@@ -73,6 +77,39 @@ class TelegramBotService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.bot.onText(/\/link (.+)/, async (msg, match) => {
|
||||||
|
const chatId = msg.chat.id;
|
||||||
|
const ethAddress = match[1];
|
||||||
|
|
||||||
|
// Проверяем формат Ethereum-адреса
|
||||||
|
if (!/^0x[a-fA-F0-9]{40}$/.test(ethAddress)) {
|
||||||
|
this.bot.sendMessage(chatId, 'Неверный формат Ethereum-адреса. Используйте формат 0x...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Получаем ID пользователя по Ethereum-адресу
|
||||||
|
const userId = await getUserIdByIdentity('ethereum', ethAddress);
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
this.bot.sendMessage(chatId, 'Пользователь с таким Ethereum-адресом не найден. Сначала войдите через веб-интерфейс.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Связываем Telegram-аккаунт с пользователем
|
||||||
|
const success = await linkIdentity(userId, 'telegram', chatId.toString());
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
this.bot.sendMessage(chatId, `Ваш Telegram-аккаунт успешно связан с Ethereum-адресом ${ethAddress}`);
|
||||||
|
} else {
|
||||||
|
this.bot.sendMessage(chatId, 'Не удалось связать аккаунты. Возможно, этот Telegram-аккаунт уже связан с другим пользователем.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при связывании аккаунтов:', error);
|
||||||
|
this.bot.sendMessage(chatId, 'Произошла ошибка при связывании аккаунтов. Попробуйте позже.');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setupCommands() {
|
setupCommands() {
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
const TelegramBot = require('node-telegram-bot-api');
|
|
||||||
const { ChatOllama } = require('@langchain/ollama');
|
|
||||||
const { PGVectorStore } = require('@langchain/community/vectorstores/pgvector');
|
|
||||||
|
|
||||||
class TelegramBotService {
|
|
||||||
constructor(token, vectorStore) {
|
|
||||||
this.bot = new TelegramBot(token, { polling: true });
|
|
||||||
this.vectorStore = vectorStore;
|
|
||||||
this.chat = new ChatOllama({
|
|
||||||
model: 'mistral',
|
|
||||||
baseUrl: 'http://localhost:11434'
|
|
||||||
});
|
|
||||||
|
|
||||||
this.userRequests = new Map(); // для отслеживания запросов
|
|
||||||
|
|
||||||
this.setupHandlers();
|
|
||||||
}
|
|
||||||
|
|
||||||
isRateLimited(userId) {
|
|
||||||
const now = Date.now();
|
|
||||||
const userReqs = this.userRequests.get(userId) || [];
|
|
||||||
|
|
||||||
// Очищаем старые запросы
|
|
||||||
const recentReqs = userReqs.filter(time => now - time < 60000);
|
|
||||||
|
|
||||||
// Максимум 10 запросов в минуту
|
|
||||||
if (recentReqs.length >= 10) return true;
|
|
||||||
|
|
||||||
recentReqs.push(now);
|
|
||||||
this.userRequests.set(userId, recentReqs);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
setupHandlers() {
|
|
||||||
this.bot.on('message', async (msg) => {
|
|
||||||
const userId = msg.from.id;
|
|
||||||
|
|
||||||
if (this.isRateLimited(userId)) {
|
|
||||||
await this.bot.sendMessage(msg.chat.id,
|
|
||||||
'Пожалуйста, подождите минуту перед следующим запросом.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const chatId = msg.chat.id;
|
|
||||||
const userQuestion = msg.text;
|
|
||||||
|
|
||||||
// Поиск релевантных документов
|
|
||||||
const relevantDocs = await this.vectorStore.similaritySearch(userQuestion, 3);
|
|
||||||
|
|
||||||
// Формируем контекст из найденных документов
|
|
||||||
const context = relevantDocs.map(doc => doc.pageContent).join('\n');
|
|
||||||
|
|
||||||
// Получаем ответ от LLM
|
|
||||||
const response = await this.chat.invoke([
|
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: `You are a helpful assistant. Use this context to answer: ${context}`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
content: userQuestion
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
await this.bot.sendMessage(chatId, response);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Telegram bot error:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = TelegramBotService;
|
|
||||||
134
backend/services/vectorStore.js
Normal file
134
backend/services/vectorStore.js
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
const { HNSWLib } = require("@langchain/community/vectorstores/hnswlib");
|
||||||
|
const { OllamaEmbeddings } = require("@langchain/ollama");
|
||||||
|
const { DirectoryLoader } = require("langchain/document_loaders/fs/directory");
|
||||||
|
const { TextLoader } = require("langchain/document_loaders/fs/text");
|
||||||
|
const { RecursiveCharacterTextSplitter } = require("langchain/text_splitter");
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Путь к директории с документами
|
||||||
|
const DOCS_DIR = path.join(__dirname, '../data/documents');
|
||||||
|
// Путь к директории для хранения векторного индекса
|
||||||
|
const VECTOR_STORE_DIR = path.join(__dirname, '../data/vector_store');
|
||||||
|
|
||||||
|
// Создаем директории, если они не существуют
|
||||||
|
if (!fs.existsSync(DOCS_DIR)) {
|
||||||
|
fs.mkdirSync(DOCS_DIR, { recursive: true });
|
||||||
|
console.log(`Создана директория для документов: ${DOCS_DIR}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(VECTOR_STORE_DIR)) {
|
||||||
|
fs.mkdirSync(VECTOR_STORE_DIR, { recursive: true });
|
||||||
|
console.log(`Создана директория для векторного хранилища: ${VECTOR_STORE_DIR}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Глобальная переменная для хранения экземпляра векторного хранилища
|
||||||
|
let vectorStore = null;
|
||||||
|
|
||||||
|
// Функция для инициализации векторного хранилища
|
||||||
|
async function initializeVectorStore() {
|
||||||
|
try {
|
||||||
|
console.log('Инициализация векторного хранилища...');
|
||||||
|
|
||||||
|
// Проверяем, существует ли директория с документами
|
||||||
|
if (!fs.existsSync(DOCS_DIR)) {
|
||||||
|
console.warn(`Директория с документами не найдена: ${DOCS_DIR}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, есть ли документы в директории
|
||||||
|
const files = fs.readdirSync(DOCS_DIR);
|
||||||
|
if (files.length === 0) {
|
||||||
|
console.warn(`В директории с документами нет файлов: ${DOCS_DIR}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Найдено ${files.length} файлов в директории с документами`);
|
||||||
|
|
||||||
|
// Загружаем документы из директории
|
||||||
|
const loader = new DirectoryLoader(
|
||||||
|
DOCS_DIR,
|
||||||
|
{
|
||||||
|
".txt": (path) => new TextLoader(path),
|
||||||
|
".md": (path) => new TextLoader(path),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Загрузка документов...');
|
||||||
|
const docs = await loader.load();
|
||||||
|
console.log(`Загружено ${docs.length} документов`);
|
||||||
|
|
||||||
|
if (docs.length === 0) {
|
||||||
|
console.warn('Не удалось загрузить документы');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Разбиваем документы на чанки
|
||||||
|
const textSplitter = new RecursiveCharacterTextSplitter({
|
||||||
|
chunkSize: 1000,
|
||||||
|
chunkOverlap: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Разбиение документов на чанки...');
|
||||||
|
const splitDocs = await textSplitter.splitDocuments(docs);
|
||||||
|
console.log(`Документы разбиты на ${splitDocs.length} чанков`);
|
||||||
|
|
||||||
|
// Создаем эмбеддинги с помощью Ollama
|
||||||
|
console.log('Создание эмбеддингов...');
|
||||||
|
const embeddings = new OllamaEmbeddings({
|
||||||
|
model: "mistral",
|
||||||
|
baseUrl: "http://localhost:11434",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверяем, существует ли уже векторное хранилище
|
||||||
|
if (fs.existsSync(path.join(VECTOR_STORE_DIR, 'hnswlib.index'))) {
|
||||||
|
console.log('Загрузка существующего векторного хранилища...');
|
||||||
|
try {
|
||||||
|
vectorStore = await HNSWLib.load(
|
||||||
|
VECTOR_STORE_DIR,
|
||||||
|
embeddings
|
||||||
|
);
|
||||||
|
console.log('Векторное хранилище успешно загружено');
|
||||||
|
return vectorStore;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при загрузке векторного хранилища:', error);
|
||||||
|
console.log('Создание нового векторного хранилища...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем новое векторное хранилище
|
||||||
|
console.log('Создание нового векторного хранилища...');
|
||||||
|
vectorStore = await HNSWLib.fromDocuments(
|
||||||
|
splitDocs,
|
||||||
|
embeddings
|
||||||
|
);
|
||||||
|
|
||||||
|
// Сохраняем векторное хранилище
|
||||||
|
console.log('Сохранение векторного хранилища...');
|
||||||
|
await vectorStore.save(VECTOR_STORE_DIR);
|
||||||
|
console.log('Векторное хранилище успешно сохранено');
|
||||||
|
|
||||||
|
return vectorStore;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при инициализации векторного хранилища:', error);
|
||||||
|
console.log('Приложение продолжит работу без векторного хранилища');
|
||||||
|
// Возвращаем заглушку вместо реального хранилища
|
||||||
|
return {
|
||||||
|
addDocuments: async () => console.log('Векторное хранилище недоступно: addDocuments'),
|
||||||
|
similaritySearch: async () => {
|
||||||
|
console.log('Векторное хранилище недоступно: similaritySearch');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для получения экземпляра векторного хранилища
|
||||||
|
async function getVectorStore() {
|
||||||
|
if (!vectorStore) {
|
||||||
|
vectorStore = await initializeVectorStore();
|
||||||
|
}
|
||||||
|
return vectorStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { initializeVectorStore, getVectorStore };
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"cookie":{"originalMaxAge":2592000000,"expires":"2025-03-23T13:02:38.982Z","secure":false,"httpOnly":true,"domain":"127.0.0.1","path":"/","sameSite":"lax"},"__lastAccess":1740142958982}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"cookie":{"originalMaxAge":2591999999,"expires":"2025-03-24T09:38:02.826Z","secure":false,"httpOnly":true,"domain":"127.0.0.1","path":"/","sameSite":"lax"},"nonce":null,"__lastAccess":1740217082827,"siwe":{"domain":"127.0.0.1:5173","address":"0xF45aa4917b3775bA37f48Aeb3dc1a943561e9e0B","statement":"Sign in with Ethereum to access DApp features and AI Assistant","uri":"http://127.0.0.1:5173","version":"1","nonce":"3ZIxF8sn0dDIbwWZz","issuedAt":"2025-02-22T09:37:59.804Z","chainId":11155111,"resources":["http://127.0.0.1:5173/api/chat","http://127.0.0.1:5173/api/contract"]},"authenticated":true}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"cookie":{"originalMaxAge":2592000000,"expires":"2025-03-23T14:54:20.531Z","secure":false,"httpOnly":true,"domain":"127.0.0.1","path":"/","sameSite":"lax"},"__lastAccess":1740149660531}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"cookie":{"originalMaxAge":2592000000,"expires":"2025-03-23T14:53:08.151Z","secure":false,"httpOnly":true,"domain":"127.0.0.1","path":"/","sameSite":"lax"},"__lastAccess":1740149588151}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"cookie":{"originalMaxAge":2592000000,"expires":"2025-03-23T14:36:28.622Z","secure":false,"httpOnly":true,"domain":"127.0.0.1","path":"/","sameSite":"lax"},"__lastAccess":1740148588623}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"cookie":{"originalMaxAge":2592000000,"expires":"2025-03-23T14:49:54.299Z","secure":false,"httpOnly":true,"domain":"127.0.0.1","path":"/","sameSite":"lax"},"__lastAccess":1740149394299}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"cookie":{"originalMaxAge":2592000000,"expires":"2025-03-23T13:32:20.745Z","secure":false,"httpOnly":true,"domain":"127.0.0.1","path":"/","sameSite":"lax"},"__lastAccess":1740144740745}
|
|
||||||
45
backend/test/AccessToken.test.js
Normal file
45
backend/test/AccessToken.test.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
const { expect } = require("chai");
|
||||||
|
const { ethers } = require("hardhat");
|
||||||
|
|
||||||
|
describe("AccessToken", function () {
|
||||||
|
let AccessToken;
|
||||||
|
let accessToken;
|
||||||
|
let owner;
|
||||||
|
let addr1;
|
||||||
|
let addr2;
|
||||||
|
|
||||||
|
beforeEach(async function () {
|
||||||
|
[owner, addr1, addr2] = await ethers.getSigners();
|
||||||
|
AccessToken = await ethers.getContractFactory("AccessToken");
|
||||||
|
accessToken = await AccessToken.deploy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Minting", function () {
|
||||||
|
it("Should mint admin token", async function () {
|
||||||
|
await accessToken.mintAccessToken(addr1.address, 0);
|
||||||
|
expect(await accessToken.checkRole(addr1.address)).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should mint moderator token", async function () {
|
||||||
|
await accessToken.mintAccessToken(addr1.address, 1);
|
||||||
|
expect(await accessToken.checkRole(addr1.address)).to.equal(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Access Control", function () {
|
||||||
|
it("Should fail for non-token holders", async function () {
|
||||||
|
await expect(
|
||||||
|
accessToken.checkRole(addr1.address)
|
||||||
|
).to.be.revertedWith("No active token");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should revoke access", async function () {
|
||||||
|
await accessToken.mintAccessToken(addr1.address, 0);
|
||||||
|
const tokenId = await accessToken.activeTokens(addr1.address);
|
||||||
|
await accessToken.revokeToken(tokenId);
|
||||||
|
await expect(
|
||||||
|
accessToken.checkRole(addr1.address)
|
||||||
|
).to.be.revertedWith("No active token");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
const { expect } = require("chai");
|
|
||||||
const { ethers } = require("hardhat");
|
|
||||||
|
|
||||||
describe("MyContract", function () {
|
|
||||||
let myContract;
|
|
||||||
let owner;
|
|
||||||
let addr1;
|
|
||||||
let addr2;
|
|
||||||
|
|
||||||
beforeEach(async function () {
|
|
||||||
// Получаем аккаунты из Hardhat
|
|
||||||
[owner, addr1, addr2] = await ethers.getSigners();
|
|
||||||
|
|
||||||
// Деплоим контракт
|
|
||||||
const MyContract = await ethers.getContractFactory("MyContract");
|
|
||||||
myContract = await MyContract.deploy();
|
|
||||||
await myContract.deployed();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Deployment", function () {
|
|
||||||
it("Should set the right owner", async function () {
|
|
||||||
expect(await myContract.owner()).to.equal(owner.address);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Transactions", function () {
|
|
||||||
it("Should allow owner to set new owner", async function () {
|
|
||||||
await myContract.setOwner(addr1.address);
|
|
||||||
expect(await myContract.owner()).to.equal(addr1.address);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should fail if non-owner tries to set new owner", async function () {
|
|
||||||
// Подключаемся к контракту от имени addr1
|
|
||||||
const contractConnectedToAddr1 = myContract.connect(addr1);
|
|
||||||
|
|
||||||
// Ожидаем, что транзакция будет отменена
|
|
||||||
await expect(
|
|
||||||
contractConnectedToAddr1.setOwner(addr2.address)
|
|
||||||
).to.be.revertedWith("Only owner can call this function");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
89
backend/utils/access-check.js
Normal file
89
backend/utils/access-check.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
const { ethers } = require('ethers');
|
||||||
|
require('dotenv').config();
|
||||||
|
const contractArtifact = require('../artifacts/contracts/MyContract.sol/MyContract.json');
|
||||||
|
const contractABI = contractArtifact.abi;
|
||||||
|
|
||||||
|
// Проверяем наличие необходимых переменных окружения
|
||||||
|
if (!process.env.ACCESS_TOKEN_ADDRESS) {
|
||||||
|
console.error('ACCESS_TOKEN_ADDRESS не указан в .env файле');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!process.env.ETHEREUM_NETWORK_URL) {
|
||||||
|
console.error('ETHEREUM_NETWORK_URL не указан в .env файле');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подключение к контракту
|
||||||
|
const provider = new ethers.JsonRpcProvider(process.env.ETHEREUM_NETWORK_URL);
|
||||||
|
let accessToken;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const AccessTokenABI = require('../artifacts/contracts/AccessToken.sol/AccessToken.json').abi;
|
||||||
|
accessToken = new ethers.Contract(
|
||||||
|
process.env.ACCESS_TOKEN_ADDRESS,
|
||||||
|
AccessTokenABI,
|
||||||
|
provider
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка инициализации контракта AccessToken:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет доступ и роль пользователя
|
||||||
|
* @param {string} address - Ethereum адрес пользователя
|
||||||
|
* @returns {Promise<{hasAccess: boolean, role: string|null}>}
|
||||||
|
*/
|
||||||
|
async function checkAccess(address) {
|
||||||
|
try {
|
||||||
|
if (!address || !accessToken) {
|
||||||
|
return { hasAccess: false, role: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем активный токен
|
||||||
|
const activeTokenId = await accessToken.activeTokens(address);
|
||||||
|
if (activeTokenId.toString() === '0') {
|
||||||
|
return { hasAccess: false, role: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем роль
|
||||||
|
const roleId = await accessToken.checkRole(address);
|
||||||
|
const roles = ['ADMIN', 'MODERATOR', 'SUPPORT'];
|
||||||
|
const role = roles[roleId];
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasAccess: true,
|
||||||
|
role,
|
||||||
|
tokenId: activeTokenId.toString()
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Access check error:', error);
|
||||||
|
return { hasAccess: false, role: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkAdmin(address) {
|
||||||
|
try {
|
||||||
|
console.log('Проверка прав администратора для адреса:', address);
|
||||||
|
// Проверяем, является ли пользователь администратором через смарт-контракт
|
||||||
|
const contract = new ethers.Contract(
|
||||||
|
process.env.CONTRACT_ADDRESS,
|
||||||
|
contractABI,
|
||||||
|
provider
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Контракт инициализирован:', {
|
||||||
|
address: process.env.CONTRACT_ADDRESS,
|
||||||
|
provider: provider.connection.url
|
||||||
|
});
|
||||||
|
|
||||||
|
const isAdmin = await contract.isAdmin(address);
|
||||||
|
console.log('Результат проверки из контракта:', isAdmin);
|
||||||
|
|
||||||
|
return isAdmin;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при проверке прав администратора:', error);
|
||||||
|
// В случае ошибки возвращаем false вместо выброса исключения
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { checkAccess, checkAdmin };
|
||||||
84
backend/utils/auth.js
Normal file
84
backend/utils/auth.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
const { SiweMessage } = require('siwe');
|
||||||
|
const { ethers } = require('ethers');
|
||||||
|
const AccessTokenABI = require('../artifacts/contracts/AccessToken.sol/AccessToken.json').abi;
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const provider = new ethers.JsonRpcProvider(process.env.ETHEREUM_NETWORK_URL);
|
||||||
|
const accessToken = new ethers.Contract(
|
||||||
|
process.env.ACCESS_TOKEN_ADDRESS,
|
||||||
|
AccessTokenABI,
|
||||||
|
provider
|
||||||
|
);
|
||||||
|
|
||||||
|
// Проверяем наличие адреса контракта
|
||||||
|
if (!process.env.ACCESS_TOKEN_ADDRESS) {
|
||||||
|
console.error('ACCESS_TOKEN_ADDRESS не указан в .env файле');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет подпись сообщения
|
||||||
|
* @param {Object} message - Сообщение для проверки
|
||||||
|
* @param {string} signature - Подпись сообщения
|
||||||
|
* @param {string} address - Адрес кошелька
|
||||||
|
* @returns {Promise<boolean>} - Результат проверки
|
||||||
|
*/
|
||||||
|
async function verifySignature(message, signature, address) {
|
||||||
|
try {
|
||||||
|
// Формируем сообщение для проверки
|
||||||
|
const domain = message.domain || window.location.host;
|
||||||
|
const statement = message.statement || 'Sign in with Ethereum to the app.';
|
||||||
|
const uri = message.uri || window.location.origin;
|
||||||
|
const version = message.version || '1';
|
||||||
|
const chainId = message.chainId || '1';
|
||||||
|
const nonce = message.nonce;
|
||||||
|
|
||||||
|
const messageToVerify = `${domain} wants you to sign in with your Ethereum account:
|
||||||
|
${address}
|
||||||
|
|
||||||
|
${statement}
|
||||||
|
|
||||||
|
URI: ${uri}
|
||||||
|
Version: ${version}
|
||||||
|
Chain ID: ${chainId}
|
||||||
|
Nonce: ${nonce}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Восстанавливаем адрес из подписи
|
||||||
|
const recoveredAddress = ethers.verifyMessage(messageToVerify, signature);
|
||||||
|
|
||||||
|
return recoveredAddress.toLowerCase() === address.toLowerCase();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Signature verification error:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkAccess(address) {
|
||||||
|
// Временная заглушка
|
||||||
|
return { hasAccess: false, role: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyAndCheckAccess(message, signature, address) {
|
||||||
|
// Проверяем подпись
|
||||||
|
const verified = await verifySignature(message, signature, address);
|
||||||
|
if (!verified) {
|
||||||
|
return {
|
||||||
|
verified: false,
|
||||||
|
access: { hasAccess: false }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем доступ
|
||||||
|
const access = await checkAccess(address);
|
||||||
|
|
||||||
|
return {
|
||||||
|
verified: true,
|
||||||
|
access
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
verifyAndCheckAccess,
|
||||||
|
verifySignature,
|
||||||
|
checkAccess
|
||||||
|
};
|
||||||
95
backend/utils/identity-linker.js
Normal file
95
backend/utils/identity-linker.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
const { Pool } = require('pg');
|
||||||
|
|
||||||
|
// Подключение к БД
|
||||||
|
const pool = new Pool({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Связывает идентификатор с пользователем
|
||||||
|
* @param {number} userId - ID пользователя
|
||||||
|
* @param {string} identityType - Тип идентификатора ('ethereum', 'telegram', 'email')
|
||||||
|
* @param {string} identityValue - Значение идентификатора
|
||||||
|
* @returns {Promise<boolean>} - Результат операции
|
||||||
|
*/
|
||||||
|
async function linkIdentity(userId, identityType, identityValue) {
|
||||||
|
try {
|
||||||
|
// Проверяем, существует ли уже такой идентификатор
|
||||||
|
const existingResult = await pool.query(
|
||||||
|
'SELECT * FROM user_identities WHERE identity_type = $1 AND identity_value = $2',
|
||||||
|
[identityType, identityValue]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingResult.rows.length > 0) {
|
||||||
|
// Если идентификатор уже связан с другим пользователем, возвращаем ошибку
|
||||||
|
if (existingResult.rows[0].user_id !== userId) {
|
||||||
|
console.warn(`Identity ${identityType}:${identityValue} already linked to another user`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Если идентификатор уже связан с этим пользователем, ничего не делаем
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем новую связь
|
||||||
|
await pool.query(
|
||||||
|
'INSERT INTO user_identities (user_id, identity_type, identity_value, created_at) VALUES ($1, $2, $3, NOW())',
|
||||||
|
[userId, identityType, identityValue]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Successfully linked ${identityType}:${identityValue} to user ${userId}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error linking identity:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает ID пользователя по идентификатору
|
||||||
|
* @param {string} identityType - Тип идентификатора ('ethereum', 'telegram', 'email')
|
||||||
|
* @param {string} identityValue - Значение идентификатора
|
||||||
|
* @returns {Promise<number|null>} - ID пользователя или null, если не найден
|
||||||
|
*/
|
||||||
|
async function getUserIdByIdentity(identityType, identityValue) {
|
||||||
|
try {
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT user_id FROM user_identities WHERE identity_type = $1 AND identity_value = $2',
|
||||||
|
[identityType, identityValue]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.rows[0].user_id;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting user ID by identity:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает все идентификаторы пользователя
|
||||||
|
* @param {number} userId - ID пользователя
|
||||||
|
* @returns {Promise<Array|null>} - Массив идентификаторов или null в случае ошибки
|
||||||
|
*/
|
||||||
|
async function getUserIdentities(userId) {
|
||||||
|
try {
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT identity_type, identity_value FROM user_identities WHERE user_id = $1',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.rows;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting user identities:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
linkIdentity,
|
||||||
|
getUserIdByIdentity,
|
||||||
|
getUserIdentities
|
||||||
|
};
|
||||||
27
backend/utils/logger.js
Normal file
27
backend/utils/logger.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
const winston = require('winston');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const logger = winston.createLogger({
|
||||||
|
level: process.env.LOG_LEVEL || 'info',
|
||||||
|
format: winston.format.combine(
|
||||||
|
winston.format.timestamp(),
|
||||||
|
winston.format.json()
|
||||||
|
),
|
||||||
|
transports: [
|
||||||
|
new winston.transports.Console({
|
||||||
|
format: winston.format.combine(
|
||||||
|
winston.format.colorize(),
|
||||||
|
winston.format.simple()
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
new winston.transports.File({
|
||||||
|
filename: path.join(__dirname, '../logs/error.log'),
|
||||||
|
level: 'error'
|
||||||
|
}),
|
||||||
|
new winston.transports.File({
|
||||||
|
filename: path.join(__dirname, '../logs/combined.log')
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = logger;
|
||||||
2202
backend/yarn.lock
2202
backend/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user