Описание изменений
This commit is contained in:
160
backend/app.js
160
backend/app.js
@@ -7,67 +7,100 @@ const helmet = require('helmet');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const logger = require('./utils/logger');
|
const logger = require('./utils/logger');
|
||||||
const authService = require('./services/auth-service');
|
const authService = require('./services/auth-service');
|
||||||
|
const { errorHandler, AppError, ErrorTypes } = require('./middleware/errorHandler');
|
||||||
|
const aiAssistant = require('./services/ai-assistant');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
// Импорт маршрутов
|
// Импорт маршрутов
|
||||||
const authRoutes = require('./routes/auth');
|
const authRoutes = require('./routes/auth');
|
||||||
const accessRoutes = require('./routes/access');
|
|
||||||
const usersRoutes = require('./routes/users');
|
const usersRoutes = require('./routes/users');
|
||||||
const contractsRoutes = require('./routes/contracts');
|
|
||||||
const rolesRoutes = require('./routes/roles');
|
|
||||||
const identitiesRoutes = require('./routes/identities');
|
const identitiesRoutes = require('./routes/identities');
|
||||||
// const conversationsRoutes = require('./routes/conversations');
|
|
||||||
const messagesRoutes = require('./routes/messages');
|
|
||||||
const chatRoutes = require('./routes/chat');
|
const chatRoutes = require('./routes/chat');
|
||||||
const healthRoutes = require('./routes/health');
|
const adminRoutes = require('./routes/admin');
|
||||||
const debugRoutes = require('./routes/debug');
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// Настройка middleware для сессий
|
// Настройка CORS
|
||||||
|
app.use(cors({
|
||||||
|
origin: 'http://localhost:5173',
|
||||||
|
credentials: true,
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization', 'Cookie']
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Настройка сессии
|
||||||
app.use(session({
|
app.use(session({
|
||||||
store: new pgSession({
|
store: new pgSession({
|
||||||
pool,
|
pool,
|
||||||
tableName: 'session',
|
tableName: 'session',
|
||||||
createTableIfMissing: true,
|
|
||||||
}),
|
}),
|
||||||
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
secret: process.env.SESSION_SECRET || 'hb3atoken',
|
||||||
|
name: 'sessionId',
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: true,
|
saveUninitialized: true,
|
||||||
cookie: {
|
cookie: {
|
||||||
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 дней
|
maxAge: 30 * 24 * 60 * 60 * 1000,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
},
|
path: '/'
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Добавим middleware для проверки сессии
|
||||||
|
app.use(async (req, res, next) => {
|
||||||
|
console.log('Request cookies:', req.headers.cookie);
|
||||||
|
console.log('Session ID:', req.sessionID);
|
||||||
|
|
||||||
|
// Проверяем сессию в базе данных
|
||||||
|
if (req.sessionID) {
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT sess FROM session WHERE sid = $1',
|
||||||
|
[req.sessionID]
|
||||||
|
);
|
||||||
|
console.log('Session from DB:', result.rows[0]?.sess);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если сессия уже есть, используем её
|
||||||
|
if (req.session.authenticated) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем заголовок авторизации
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||||
|
const token = authHeader.split(' ')[1];
|
||||||
|
try {
|
||||||
|
// Находим пользователя по токену
|
||||||
|
const { rows } = await pool.query(`
|
||||||
|
SELECT u.id,
|
||||||
|
(u.role = 'admin') as is_admin,
|
||||||
|
u.address
|
||||||
|
FROM users u
|
||||||
|
WHERE u.id = $1
|
||||||
|
`, [token]);
|
||||||
|
|
||||||
|
if (rows.length > 0) {
|
||||||
|
const user = rows[0];
|
||||||
|
req.session.userId = user.id;
|
||||||
|
req.session.address = user.address;
|
||||||
|
req.session.isAdmin = user.is_admin;
|
||||||
|
req.session.authenticated = true;
|
||||||
|
|
||||||
|
await new Promise(resolve => req.session.save(resolve));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking auth header:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
// Настройка парсеров
|
// Настройка парсеров
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
// Настройка CORS
|
|
||||||
app.use(cors({
|
|
||||||
origin: function(origin, callback) {
|
|
||||||
// Разрешаем запросы с localhost и 127.0.0.1
|
|
||||||
const allowedOrigins = [
|
|
||||||
'http://localhost:5173',
|
|
||||||
'http://127.0.0.1:5173',
|
|
||||||
'http://localhost:3000',
|
|
||||||
'http://127.0.0.1:3000',
|
|
||||||
process.env.CORS_ORIGIN
|
|
||||||
].filter(Boolean);
|
|
||||||
|
|
||||||
if (!origin || allowedOrigins.includes(origin)) {
|
|
||||||
callback(null, true);
|
|
||||||
} else {
|
|
||||||
callback(new Error('Not allowed by CORS'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
credentials: true,
|
|
||||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
||||||
allowedHeaders: ['Content-Type', 'Authorization']
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Настройка безопасности
|
// Настройка безопасности
|
||||||
app.use(helmet({
|
app.use(helmet({
|
||||||
contentSecurityPolicy: false // Отключаем CSP для разработки
|
contentSecurityPolicy: false // Отключаем CSP для разработки
|
||||||
@@ -79,34 +112,51 @@ app.use((req, res, next) => {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Добавляем middleware для установки заголовков CORS
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
res.header('Access-Control-Allow-Origin', req.headers.origin || '*');
|
|
||||||
res.header('Access-Control-Allow-Credentials', 'true');
|
|
||||||
res.header('Access-Control-Allow-Methods', 'GET,HEAD,PUT,PATCH,POST,DELETE');
|
|
||||||
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Маршруты API
|
// Маршруты API
|
||||||
app.use('/api/auth', authRoutes);
|
app.use('/api/auth', authRoutes);
|
||||||
app.use('/api/access', accessRoutes);
|
|
||||||
app.use('/api/users', usersRoutes);
|
app.use('/api/users', usersRoutes);
|
||||||
app.use('/api/contracts', contractsRoutes);
|
|
||||||
app.use('/api/roles', rolesRoutes);
|
|
||||||
app.use('/api/identities', identitiesRoutes);
|
app.use('/api/identities', identitiesRoutes);
|
||||||
// app.use('/api/conversations', conversationsRoutes);
|
|
||||||
app.use('/api/messages', messagesRoutes);
|
|
||||||
app.use('/api/chat', chatRoutes);
|
app.use('/api/chat', chatRoutes);
|
||||||
app.use('/api/health', healthRoutes);
|
app.use('/api/admin', adminRoutes);
|
||||||
|
|
||||||
// Маршруты для отладки (только в режиме разработки)
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
app.use('/api/debug', debugRoutes);
|
|
||||||
}
|
|
||||||
|
|
||||||
const nonceStore = new Map(); // или любая другая реализация хранилища nonce
|
const nonceStore = new Map(); // или любая другая реализация хранилища nonce
|
||||||
|
|
||||||
console.log('SESSION_SECRET:', process.env.SESSION_SECRET);
|
console.log('SESSION_SECRET:', process.env.SESSION_SECRET);
|
||||||
|
|
||||||
|
// Добавляем обработчик ошибок последним
|
||||||
|
app.use(errorHandler);
|
||||||
|
|
||||||
|
// Эндпоинт для проверки состояния
|
||||||
|
app.get('/api/health', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Проверяем подключение к БД
|
||||||
|
await pool.query('SELECT NOW()');
|
||||||
|
|
||||||
|
// Проверяем AI сервис
|
||||||
|
const aiStatus = await aiAssistant.checkHealth();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
status: 'ok',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
database: 'connected',
|
||||||
|
ai: aiStatus
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Health check failed:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
status: 'error',
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Очистка старых сессий
|
||||||
|
setInterval(async () => {
|
||||||
|
try {
|
||||||
|
await pool.query('DELETE FROM session WHERE expire < NOW()');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cleaning old sessions:', error);
|
||||||
|
}
|
||||||
|
}, 15 * 60 * 1000); // Каждые 15 минут
|
||||||
|
|
||||||
module.exports = { app, nonceStore };
|
module.exports = { app, nonceStore };
|
||||||
|
|||||||
25
backend/config/session.js
Normal file
25
backend/config/session.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
const session = require('express-session');
|
||||||
|
const pgSession = require('connect-pg-simple')(session);
|
||||||
|
const { pool } = require('../db');
|
||||||
|
|
||||||
|
const sessionConfig = {
|
||||||
|
store: new pgSession({
|
||||||
|
pool,
|
||||||
|
tableName: 'session',
|
||||||
|
}),
|
||||||
|
secret: process.env.SESSION_SECRET || 'hb3atoken',
|
||||||
|
name: 'sessionId',
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: true,
|
||||||
|
cookie: {
|
||||||
|
maxAge: 30 * 24 * 60 * 60 * 1000,
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: 'lax',
|
||||||
|
path: '/'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sessionMiddleware: session(sessionConfig)
|
||||||
|
};
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
// 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 { USER, ADMIN }
|
|
||||||
|
|
||||||
// Маппинг токен 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 {
|
|
||||||
require(role == Role.USER || role == Role.ADMIN, "Invalid role");
|
|
||||||
|
|
||||||
_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
|
|
||||||
) internal override {
|
|
||||||
require(from == address(0) || to == address(0), "Token transfer not allowed");
|
|
||||||
super._beforeTokenTransfer(from, to, tokenId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
pragma solidity ^0.8.0;
|
|
||||||
|
|
||||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
|
||||||
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
|
||||||
|
|
||||||
contract MyContract is Ownable, ReentrancyGuard {
|
|
||||||
// Явно объявляем функцию owner
|
|
||||||
function owner() public view override returns (address) {
|
|
||||||
return super.owner();
|
|
||||||
}
|
|
||||||
|
|
||||||
uint256 public price;
|
|
||||||
|
|
||||||
event Purchase(address buyer, uint256 amount);
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
price = 0.01 ether; // Начальная цена 0.01 ETH
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPrice(uint256 newPrice) public onlyOwner {
|
|
||||||
price = newPrice;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPrice() public view returns (uint256) {
|
|
||||||
return price;
|
|
||||||
}
|
|
||||||
|
|
||||||
function purchase(uint256 amount) public payable nonReentrant {
|
|
||||||
require(msg.value == price * amount, "Incorrect payment amount");
|
|
||||||
emit Purchase(msg.sender, amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
function withdraw() public onlyOwner nonReentrant {
|
|
||||||
(bool success, ) = owner().call{value: address(this).balance}("");
|
|
||||||
require(success, "Transfer failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,21 @@
|
|||||||
const createGuestMessagesTable = require('./migrations/create_guest_messages_table');
|
const { Pool } = require('pg');
|
||||||
|
const logger = require('../utils/logger');
|
||||||
|
|
||||||
async function initDatabase() {
|
const pool = new Pool({
|
||||||
try {
|
user: process.env.DB_USER || 'dapp_user',
|
||||||
// ... существующий код ...
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
database: process.env.DB_NAME || 'dapp_db',
|
||||||
// Выполняем миграции
|
password: process.env.DB_PASSWORD,
|
||||||
await pool.query(createUsersTable);
|
port: process.env.DB_PORT || 5432,
|
||||||
await pool.query(createSessionTable);
|
});
|
||||||
await pool.query(createNoncesTable);
|
|
||||||
await pool.query(createMessagesTable);
|
// Проверка подключения
|
||||||
await pool.query(createConversationsTable);
|
pool.query('SELECT NOW()', (err, res) => {
|
||||||
await pool.query(createGuestMessagesTable);
|
if (err) {
|
||||||
|
logger.error('Error connecting to database:', err);
|
||||||
// ... существующий код ...
|
} else {
|
||||||
} catch (error) {
|
logger.info('Успешное подключение к базе данных:', res.rows[0]);
|
||||||
// ... существующий код ...
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
module.exports = { pool };
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { pool } = require('./index');
|
||||||
|
const logger = require('../utils/logger');
|
||||||
|
|
||||||
// Инициализация таблицы roles
|
// Инициализация таблицы roles
|
||||||
async function initRoles() {
|
async function initRoles() {
|
||||||
try {
|
try {
|
||||||
// Проверяем, существует ли таблица roles
|
// Проверяем, существует ли таблица roles
|
||||||
const tableExists = await db.query(`
|
const tableExists = await pool.query(`
|
||||||
SELECT EXISTS (
|
SELECT EXISTS (
|
||||||
SELECT FROM information_schema.tables
|
SELECT FROM information_schema.tables
|
||||||
WHERE table_name = 'roles'
|
WHERE table_name = 'roles'
|
||||||
@@ -11,7 +16,7 @@ async function initRoles() {
|
|||||||
|
|
||||||
if (!tableExists.rows[0].exists) {
|
if (!tableExists.rows[0].exists) {
|
||||||
// Создаем таблицу roles
|
// Создаем таблицу roles
|
||||||
await db.query(`
|
await pool.query(`
|
||||||
CREATE TABLE roles (
|
CREATE TABLE roles (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
name VARCHAR(50) NOT NULL UNIQUE,
|
name VARCHAR(50) NOT NULL UNIQUE,
|
||||||
@@ -21,7 +26,7 @@ async function initRoles() {
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
// Добавляем роли
|
// Добавляем роли
|
||||||
await db.query(`
|
await pool.query(`
|
||||||
INSERT INTO roles (id, name, description) VALUES
|
INSERT INTO roles (id, name, description) VALUES
|
||||||
(3, 'user', 'Обычный пользователь'),
|
(3, 'user', 'Обычный пользователь'),
|
||||||
(4, 'admin', 'Администратор с полным доступом');
|
(4, 'admin', 'Администратор с полным доступом');
|
||||||
@@ -30,24 +35,24 @@ async function initRoles() {
|
|||||||
console.log('Таблица roles создана и заполнена');
|
console.log('Таблица roles создана и заполнена');
|
||||||
} else {
|
} else {
|
||||||
// Проверяем наличие ролей
|
// Проверяем наличие ролей
|
||||||
const rolesExist = await db.query(`
|
const rolesExist = await pool.query(`
|
||||||
SELECT COUNT(*) FROM roles WHERE id IN (3, 4);
|
SELECT COUNT(*) FROM roles WHERE id IN (3, 4);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
if (rolesExist.rows[0].count < 2) {
|
if (rolesExist.rows[0].count < 2) {
|
||||||
// Добавляем недостающие роли
|
// Добавляем недостающие роли
|
||||||
const userRoleExists = await db.query(`SELECT EXISTS (SELECT FROM roles WHERE name = 'user');`);
|
const userRoleExists = await pool.query(`SELECT EXISTS (SELECT FROM roles WHERE name = 'user');`);
|
||||||
const adminRoleExists = await db.query(`SELECT EXISTS (SELECT FROM roles WHERE name = 'admin');`);
|
const adminRoleExists = await pool.query(`SELECT EXISTS (SELECT FROM roles WHERE name = 'admin');`);
|
||||||
|
|
||||||
if (!userRoleExists.rows[0].exists) {
|
if (!userRoleExists.rows[0].exists) {
|
||||||
await db.query(`
|
await pool.query(`
|
||||||
INSERT INTO roles (id, name, description) VALUES
|
INSERT INTO roles (id, name, description) VALUES
|
||||||
(3, 'user', 'Обычный пользователь');
|
(3, 'user', 'Обычный пользователь');
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!adminRoleExists.rows[0].exists) {
|
if (!adminRoleExists.rows[0].exists) {
|
||||||
await db.query(`
|
await pool.query(`
|
||||||
INSERT INTO roles (id, name, description) VALUES
|
INSERT INTO roles (id, name, description) VALUES
|
||||||
(4, 'admin', 'Администратор с полным доступом');
|
(4, 'admin', 'Администратор с полным доступом');
|
||||||
`);
|
`);
|
||||||
@@ -60,4 +65,55 @@ async function initRoles() {
|
|||||||
console.error('Ошибка при инициализации таблицы roles:', error);
|
console.error('Ошибка при инициализации таблицы roles:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function initializeDatabase() {
|
||||||
|
try {
|
||||||
|
// Создаем таблицу для отслеживания миграций, если её нет
|
||||||
|
await pool.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS migrations (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
executed_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Путь к папке с миграциями
|
||||||
|
const migrationsPath = path.join(__dirname, 'migrations');
|
||||||
|
|
||||||
|
// Получаем все файлы миграций
|
||||||
|
const migrationFiles = fs.readdirSync(migrationsPath)
|
||||||
|
.filter(file => file.endsWith('.sql'))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
// Получаем выполненные миграции
|
||||||
|
const { rows } = await pool.query('SELECT name FROM migrations');
|
||||||
|
const executedMigrations = new Set(rows.map(row => row.name));
|
||||||
|
|
||||||
|
// Выполняем только новые миграции
|
||||||
|
for (const file of migrationFiles) {
|
||||||
|
if (!executedMigrations.has(file)) {
|
||||||
|
const filePath = path.join(migrationsPath, file);
|
||||||
|
const sql = fs.readFileSync(filePath, 'utf8');
|
||||||
|
|
||||||
|
logger.info(`Executing migration: ${file}`);
|
||||||
|
await pool.query(sql);
|
||||||
|
|
||||||
|
// Записываем выполненную миграцию
|
||||||
|
await pool.query(
|
||||||
|
'INSERT INTO migrations (name) VALUES ($1)',
|
||||||
|
[file]
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`Migration completed: ${file}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('All migrations completed successfully');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error during database initialization:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { initializeDatabase };
|
||||||
8
backend/db/migrations/000_session_table.sql
Normal file
8
backend/db/migrations/000_session_table.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "session" (
|
||||||
|
"sid" varchar NOT NULL COLLATE "default",
|
||||||
|
"sess" json NOT NULL,
|
||||||
|
"expire" timestamp(6) NOT NULL,
|
||||||
|
CONSTRAINT "session_pkey" PRIMARY KEY ("sid")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "IDX_session_expire" ON "session" ("expire");
|
||||||
9
backend/db/migrations/001_initial_schema.sql
Normal file
9
backend/db/migrations/001_initial_schema.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
username VARCHAR(255),
|
||||||
|
email VARCHAR(255) UNIQUE,
|
||||||
|
address VARCHAR(255) UNIQUE,
|
||||||
|
status VARCHAR(50) DEFAULT 'active',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
24
backend/db/migrations/002_access_roles.sql
Normal file
24
backend/db/migrations/002_access_roles.sql
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS roles (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Добавляем базовые роли
|
||||||
|
INSERT INTO roles (name) VALUES ('admin'), ('user')
|
||||||
|
ON CONFLICT (name) DO NOTHING;
|
||||||
|
|
||||||
|
-- Добавляем связь пользователей с ролями
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'users' AND column_name = 'role_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN role_id INTEGER REFERENCES roles(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Создаем индекс для role_id после добавления колонки
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_role_id ON users(role_id);
|
||||||
28
backend/db/migrations/003_user_identities.sql
Normal file
28
backend/db/migrations/003_user_identities.sql
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS user_identities (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
provider VARCHAR(50) NOT NULL,
|
||||||
|
provider_id VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(provider, provider_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Создаем индексы после создания таблицы
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
-- Индекс для user_id
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_indexes
|
||||||
|
WHERE tablename = 'user_identities' AND indexname = 'idx_user_identities_user_id'
|
||||||
|
) THEN
|
||||||
|
CREATE INDEX idx_user_identities_user_id ON user_identities(user_id);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Индекс для provider и provider_id
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_indexes
|
||||||
|
WHERE tablename = 'user_identities' AND indexname = 'idx_user_identities_type_value'
|
||||||
|
) THEN
|
||||||
|
CREATE INDEX idx_user_identities_type_value ON user_identities(provider, provider_id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
10
backend/db/migrations/004_conversations.sql
Normal file
10
backend/db/migrations/004_conversations.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS conversations (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
title VARCHAR(255),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_conversations_user_id ON conversations(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_conversations_created_at ON conversations(created_at);
|
||||||
20
backend/db/migrations/005_messages.sql
Normal file
20
backend/db/migrations/005_messages.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS messages (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
conversation_id INTEGER REFERENCES conversations(id) ON DELETE CASCADE,
|
||||||
|
sender_type VARCHAR(20) NOT NULL,
|
||||||
|
sender_id INTEGER,
|
||||||
|
content TEXT,
|
||||||
|
channel VARCHAR(20) NOT NULL,
|
||||||
|
metadata JSONB,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
role VARCHAR(20) NOT NULL DEFAULT 'user',
|
||||||
|
guest_message_id INTEGER,
|
||||||
|
tokens_used INTEGER DEFAULT 0,
|
||||||
|
is_processed BOOLEAN DEFAULT FALSE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_messages_sender_type ON messages(sender_type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_messages_metadata ON messages USING gin(metadata);
|
||||||
52
backend/db/migrations/006_guest_messages.sql
Normal file
52
backend/db/migrations/006_guest_messages.sql
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS guest_messages (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
guest_id VARCHAR(255) NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
language VARCHAR(10) DEFAULT 'en',
|
||||||
|
is_ai BOOLEAN DEFAULT false,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_indexes
|
||||||
|
WHERE tablename = 'guest_messages' AND indexname = 'idx_guest_messages_guest_id'
|
||||||
|
) THEN
|
||||||
|
CREATE INDEX idx_guest_messages_guest_id ON guest_messages(guest_id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'messages' AND column_name = 'guest_message_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE messages ADD COLUMN guest_message_id INTEGER;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_messages_guest_message'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE messages
|
||||||
|
ADD CONSTRAINT fk_messages_guest_message
|
||||||
|
FOREIGN KEY (guest_message_id)
|
||||||
|
REFERENCES guest_messages(id)
|
||||||
|
ON DELETE SET NULL;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_indexes
|
||||||
|
WHERE tablename = 'messages' AND indexname = 'idx_messages_guest_message_id'
|
||||||
|
) THEN
|
||||||
|
CREATE INDEX idx_messages_guest_message_id ON messages(guest_message_id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
46
backend/db/migrations/007_user_preferences.sql
Normal file
46
backend/db/migrations/007_user_preferences.sql
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS user_preferences (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||||
|
preference_key VARCHAR(50) NOT NULL,
|
||||||
|
preference_value TEXT,
|
||||||
|
metadata JSONB DEFAULT '{}',
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
UNIQUE(user_id, preference_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Добавляем колонку metadata, если её нет
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'user_preferences' AND column_name = 'metadata'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE user_preferences ADD COLUMN metadata JSONB DEFAULT '{}';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_preferences_user_id ON user_preferences(user_id);
|
||||||
|
|
||||||
|
-- Базовые настройки
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO user_preferences (user_id, preference_key, preference_value, metadata)
|
||||||
|
SELECT id, 'language', 'ru', '{"available": ["ru", "en"]}'::jsonb
|
||||||
|
FROM users u
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM user_preferences
|
||||||
|
WHERE preference_key = 'language' AND user_id = u.id
|
||||||
|
);
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO user_preferences (user_id, preference_key, preference_value, metadata)
|
||||||
|
SELECT id, 'notifications', 'true', '{"channels": ["email", "telegram"]}'::jsonb
|
||||||
|
FROM users u
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM user_preferences
|
||||||
|
WHERE preference_key = 'notifications' AND user_id = u.id
|
||||||
|
);
|
||||||
|
END $$;
|
||||||
45
backend/db/migrations/008_update_messages_structure.sql
Normal file
45
backend/db/migrations/008_update_messages_structure.sql
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
-- Добавляем новые поля в messages
|
||||||
|
ALTER TABLE messages
|
||||||
|
ADD COLUMN IF NOT EXISTS role VARCHAR(20) NOT NULL DEFAULT 'user',
|
||||||
|
ADD COLUMN IF NOT EXISTS guest_message_id INTEGER,
|
||||||
|
ADD COLUMN IF NOT EXISTS tokens_used INTEGER DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS is_processed BOOLEAN DEFAULT FALSE;
|
||||||
|
|
||||||
|
-- Создаем функцию для связывания сообщений
|
||||||
|
CREATE OR REPLACE FUNCTION link_guest_messages(
|
||||||
|
p_user_id INTEGER,
|
||||||
|
p_guest_id VARCHAR(255)
|
||||||
|
) RETURNS VOID AS $$
|
||||||
|
DECLARE
|
||||||
|
v_conversation_id INTEGER;
|
||||||
|
BEGIN
|
||||||
|
-- Создаем новую беседу для гостевых сообщений
|
||||||
|
INSERT INTO conversations (created_at, updated_at)
|
||||||
|
VALUES (NOW(), NOW())
|
||||||
|
RETURNING id INTO v_conversation_id;
|
||||||
|
|
||||||
|
-- Копируем гостевые сообщения в основную таблицу
|
||||||
|
INSERT INTO messages (
|
||||||
|
conversation_id,
|
||||||
|
sender_type,
|
||||||
|
sender_id,
|
||||||
|
content,
|
||||||
|
role,
|
||||||
|
channel,
|
||||||
|
guest_message_id,
|
||||||
|
created_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
v_conversation_id,
|
||||||
|
CASE WHEN is_ai THEN 'assistant' ELSE 'user' END,
|
||||||
|
CASE WHEN NOT is_ai THEN p_user_id ELSE NULL END,
|
||||||
|
content,
|
||||||
|
CASE WHEN is_ai THEN 'assistant' ELSE 'user' END,
|
||||||
|
'chat',
|
||||||
|
id,
|
||||||
|
created_at
|
||||||
|
FROM guest_messages
|
||||||
|
WHERE guest_id = p_guest_id
|
||||||
|
ORDER BY created_at;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
13
backend/db/migrations/009_nonces_table.sql
Normal file
13
backend/db/migrations/009_nonces_table.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS nonces (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
identity_value VARCHAR(255) NOT NULL,
|
||||||
|
nonce VARCHAR(255) NOT NULL,
|
||||||
|
expires_at TIMESTAMP NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Индекс для быстрого поиска по identity_value
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_nonces_identity_value ON nonces(identity_value);
|
||||||
|
|
||||||
|
-- Индекс для очистки просроченных nonce
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_nonces_expires_at ON nonces(expires_at);
|
||||||
71
backend/db/migrations/010_cleanup_roles.sql
Normal file
71
backend/db/migrations/010_cleanup_roles.sql
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
-- Проверяем существование типа user_role
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_role') THEN
|
||||||
|
CREATE TYPE user_role AS ENUM ('user', 'admin');
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Удаляем лишние колонки и связи
|
||||||
|
ALTER TABLE users DROP CONSTRAINT IF EXISTS users_role_id_fkey;
|
||||||
|
ALTER TABLE users DROP COLUMN IF EXISTS role_id;
|
||||||
|
|
||||||
|
-- Добавляем колонку role если её нет
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'users' AND column_name = 'role'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE users ADD COLUMN role user_role DEFAULT 'user'::user_role;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Удаляем лишние триггеры и функции
|
||||||
|
DROP TRIGGER IF EXISTS sync_identity_type_trigger ON user_identities;
|
||||||
|
DROP TRIGGER IF EXISTS user_identity_role_check ON user_identities;
|
||||||
|
DROP TRIGGER IF EXISTS check_admin_role_trigger ON user_identities;
|
||||||
|
DROP FUNCTION IF EXISTS sync_identity_type() CASCADE;
|
||||||
|
DROP FUNCTION IF EXISTS update_user_role() CASCADE;
|
||||||
|
DROP FUNCTION IF EXISTS check_admin_role(INTEGER) CASCADE;
|
||||||
|
|
||||||
|
-- Создаем функцию проверки роли
|
||||||
|
CREATE FUNCTION check_admin_role()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
DECLARE
|
||||||
|
v_wallet_address VARCHAR;
|
||||||
|
BEGIN
|
||||||
|
SELECT provider_id INTO v_wallet_address
|
||||||
|
FROM user_identities
|
||||||
|
WHERE user_id = NEW.user_id
|
||||||
|
AND provider = 'wallet'
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF v_wallet_address IS NULL THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
UPDATE users
|
||||||
|
SET role = 'admin'::user_role
|
||||||
|
WHERE id = NEW.user_id;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- Создаем триггер
|
||||||
|
CREATE TRIGGER check_admin_role_trigger
|
||||||
|
AFTER INSERT OR UPDATE ON user_identities
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION check_admin_role();
|
||||||
|
|
||||||
|
-- Обновляем существующие записи
|
||||||
|
UPDATE users u
|
||||||
|
SET role = CASE
|
||||||
|
WHEN EXISTS (
|
||||||
|
SELECT 1 FROM user_identities ui
|
||||||
|
WHERE ui.user_id = u.id
|
||||||
|
AND ui.provider = 'wallet'
|
||||||
|
) THEN 'admin'::user_role
|
||||||
|
ELSE 'user'::user_role
|
||||||
|
END;
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
// Создаем таблицу для хранения сообщений неаутентифицированных пользователей
|
|
||||||
const createGuestMessagesTable = `
|
|
||||||
CREATE TABLE IF NOT EXISTS guest_messages (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
guest_id VARCHAR(255) NOT NULL,
|
|
||||||
content TEXT NOT NULL,
|
|
||||||
language VARCHAR(10) DEFAULT 'en',
|
|
||||||
is_ai BOOLEAN DEFAULT FALSE,
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_guest_messages_guest_id ON guest_messages(guest_id);
|
|
||||||
`;
|
|
||||||
|
|
||||||
module.exports = createGuestMessagesTable;
|
|
||||||
56
backend/db/migrations/functions/link_guest_messages.sql
Normal file
56
backend/db/migrations/functions/link_guest_messages.sql
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
CREATE OR REPLACE FUNCTION link_guest_messages(
|
||||||
|
p_user_id INTEGER,
|
||||||
|
p_guest_id VARCHAR(255)
|
||||||
|
) RETURNS VOID AS $$
|
||||||
|
DECLARE
|
||||||
|
v_conversation_id INTEGER;
|
||||||
|
v_count INTEGER;
|
||||||
|
BEGIN
|
||||||
|
-- Логируем входные параметры
|
||||||
|
RAISE NOTICE 'Linking messages for user_id: %, guest_id: %', p_user_id, p_guest_id;
|
||||||
|
|
||||||
|
-- Проверяем наличие гостевых сообщений
|
||||||
|
SELECT COUNT(*) INTO v_count
|
||||||
|
FROM guest_messages
|
||||||
|
WHERE guest_id = p_guest_id;
|
||||||
|
|
||||||
|
RAISE NOTICE 'Found % guest messages', v_count;
|
||||||
|
|
||||||
|
-- Создаем новую беседу
|
||||||
|
INSERT INTO conversations (user_id, created_at, updated_at)
|
||||||
|
VALUES (p_user_id, NOW(), NOW())
|
||||||
|
RETURNING id INTO v_conversation_id;
|
||||||
|
|
||||||
|
RAISE NOTICE 'Created conversation with id: %', v_conversation_id;
|
||||||
|
|
||||||
|
-- Копируем сообщения пользователя
|
||||||
|
WITH inserted_messages AS (
|
||||||
|
INSERT INTO messages (
|
||||||
|
conversation_id,
|
||||||
|
sender_type,
|
||||||
|
sender_id,
|
||||||
|
content,
|
||||||
|
role,
|
||||||
|
channel,
|
||||||
|
guest_message_id,
|
||||||
|
created_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
v_conversation_id,
|
||||||
|
CASE WHEN is_ai THEN 'assistant' ELSE 'user' END,
|
||||||
|
CASE WHEN NOT is_ai THEN p_user_id ELSE NULL END,
|
||||||
|
content,
|
||||||
|
CASE WHEN is_ai THEN 'assistant' ELSE 'user' END,
|
||||||
|
'chat',
|
||||||
|
id, -- Сохраняем связь с гостевым сообщением
|
||||||
|
created_at
|
||||||
|
FROM guest_messages
|
||||||
|
WHERE guest_id = p_guest_id
|
||||||
|
ORDER BY created_at
|
||||||
|
RETURNING id
|
||||||
|
)
|
||||||
|
SELECT COUNT(*) INTO v_count FROM inserted_messages;
|
||||||
|
|
||||||
|
RAISE NOTICE 'Inserted % messages', v_count;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
@@ -9,66 +9,76 @@ const db = require('../db');
|
|||||||
*/
|
*/
|
||||||
const requireAuth = async (req, res, next) => {
|
const requireAuth = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
console.log('Session in requireAuth:', req.session);
|
console.log('Session in requireAuth:', {
|
||||||
console.log('Cookies received:', req.headers.cookie);
|
id: req.sessionID,
|
||||||
console.log('Authorization header:', req.headers.authorization);
|
userId: req.session?.userId,
|
||||||
|
authenticated: req.session?.authenticated
|
||||||
// Проверяем, что пользователь аутентифицирован через сессию
|
});
|
||||||
if (req.session && req.session.authenticated && req.session.userId) {
|
|
||||||
// Добавляем информацию о пользователе в запрос
|
// Проверяем сессию
|
||||||
|
if (req.session?.authenticated && req.session?.userId) {
|
||||||
|
// Обновляем время жизни сессии
|
||||||
|
req.session.touch();
|
||||||
|
|
||||||
req.user = {
|
req.user = {
|
||||||
userId: req.session.userId,
|
userId: req.session.userId,
|
||||||
address: req.session.address || null,
|
address: req.session.address,
|
||||||
email: req.session.email || null,
|
isAdmin: req.session.isAdmin,
|
||||||
telegramId: req.session.telegramId || null,
|
authType: req.session.authType
|
||||||
isAdmin: req.session.isAdmin || false,
|
|
||||||
authType: req.session.authType || 'unknown'
|
|
||||||
};
|
};
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем заголовок авторизации
|
// Проверяем Bearer токен
|
||||||
const authHeader = req.headers.authorization;
|
const authHeader = req.headers.authorization;
|
||||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
if (authHeader?.startsWith('Bearer ')) {
|
||||||
const token = authHeader.split(' ')[1];
|
const address = authHeader.split(' ')[1];
|
||||||
|
|
||||||
// Проверяем, это адрес кошелька или JWT-токен
|
if (address.startsWith('0x')) {
|
||||||
if (token.startsWith('0x')) {
|
const result = await db.query(`
|
||||||
// Это адрес кошелька
|
SELECT u.id, u.is_admin
|
||||||
const address = token;
|
FROM users u
|
||||||
console.log('Found address in Authorization header:', address);
|
JOIN user_identities ui ON u.id = ui.user_id
|
||||||
|
WHERE ui.identity_type = 'wallet'
|
||||||
try {
|
AND LOWER(ui.identity_value) = LOWER($1)
|
||||||
// Проверяем, существует ли пользователь с таким адресом
|
`, [address]);
|
||||||
const result = await db.query(`
|
|
||||||
SELECT u.id, u.is_admin
|
if (result.rows.length > 0) {
|
||||||
FROM users u
|
const user = result.rows[0];
|
||||||
JOIN user_identities ui ON u.id = ui.user_id
|
|
||||||
WHERE ui.identity_type = 'wallet' AND LOWER(ui.identity_value) = LOWER($1)
|
|
||||||
`, [address]);
|
|
||||||
|
|
||||||
if (result.rows.length > 0) {
|
// Создаем новую сессию
|
||||||
const user = result.rows[0];
|
req.session.regenerate(async (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error regenerating session:', err);
|
||||||
|
return res.status(500).json({ error: 'Session error' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем данные сессии
|
||||||
|
req.session.authenticated = true;
|
||||||
|
req.session.userId = user.id;
|
||||||
|
req.session.address = address;
|
||||||
|
req.session.isAdmin = user.is_admin;
|
||||||
|
req.session.authType = 'wallet';
|
||||||
|
|
||||||
|
// Сохраняем сессию
|
||||||
|
await new Promise((resolve) => req.session.save(resolve));
|
||||||
|
|
||||||
req.user = {
|
req.user = {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
address: address,
|
address: address,
|
||||||
isAdmin: user.is_admin,
|
isAdmin: user.is_admin,
|
||||||
authType: 'wallet'
|
authType: 'wallet'
|
||||||
};
|
};
|
||||||
return next();
|
next();
|
||||||
}
|
});
|
||||||
} catch (error) {
|
return;
|
||||||
console.error('Error finding user by address:', error);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Здесь можно добавить логику проверки JWT, если используется
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если пользователь не аутентифицирован, возвращаем ошибку
|
|
||||||
return res.status(401).json({ error: 'Unauthorized' });
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Unexpected error in requireAuth middleware:', error);
|
console.error('Auth middleware error:', error);
|
||||||
return res.status(500).json({ error: 'Internal server error' });
|
return res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
const logger = require('../utils/logger');
|
|
||||||
const { ERROR_CODES } = require('../utils/constants');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Middleware для обработки ошибок
|
|
||||||
*/
|
|
||||||
function errorMiddleware(err, req, res, next) {
|
|
||||||
// Логируем ошибку
|
|
||||||
logger.error(`Error: ${err.message}`, {
|
|
||||||
stack: err.stack,
|
|
||||||
url: req.originalUrl,
|
|
||||||
method: req.method,
|
|
||||||
ip: req.ip,
|
|
||||||
userId: req.session?.userId
|
|
||||||
});
|
|
||||||
|
|
||||||
// Определяем тип ошибки
|
|
||||||
let statusCode = 500;
|
|
||||||
let errorCode = ERROR_CODES.INTERNAL_ERROR;
|
|
||||||
let errorMessage = 'Внутренняя ошибка сервера';
|
|
||||||
|
|
||||||
// Обрабатываем разные типы ошибок
|
|
||||||
if (err.name === 'UnauthorizedError' || err.status === 401) {
|
|
||||||
statusCode = 401;
|
|
||||||
errorCode = ERROR_CODES.UNAUTHORIZED;
|
|
||||||
errorMessage = 'Требуется аутентификация';
|
|
||||||
} else if (err.status === 403) {
|
|
||||||
statusCode = 403;
|
|
||||||
errorCode = ERROR_CODES.FORBIDDEN;
|
|
||||||
errorMessage = 'Доступ запрещен';
|
|
||||||
} else if (err.status === 404) {
|
|
||||||
statusCode = 404;
|
|
||||||
errorCode = ERROR_CODES.NOT_FOUND;
|
|
||||||
errorMessage = 'Ресурс не найден';
|
|
||||||
} else if (err.status === 400) {
|
|
||||||
statusCode = 400;
|
|
||||||
errorCode = ERROR_CODES.BAD_REQUEST;
|
|
||||||
errorMessage = err.message || 'Некорректный запрос';
|
|
||||||
}
|
|
||||||
|
|
||||||
// В режиме разработки возвращаем стек ошибки
|
|
||||||
const devError = process.env.NODE_ENV === 'development'
|
|
||||||
? { stack: err.stack }
|
|
||||||
: {};
|
|
||||||
|
|
||||||
// Отправляем ответ клиенту
|
|
||||||
res.status(statusCode).json({
|
|
||||||
error: {
|
|
||||||
code: errorCode,
|
|
||||||
message: errorMessage,
|
|
||||||
...devError
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = errorMiddleware;
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
const session = require('express-session');
|
|
||||||
const pgSession = require('connect-pg-simple')(session);
|
|
||||||
const { pool } = require('../db');
|
|
||||||
|
|
||||||
const sessionMiddleware = session({
|
|
||||||
store: new pgSession({
|
|
||||||
pool,
|
|
||||||
tableName: 'session',
|
|
||||||
createTableIfMissing: true,
|
|
||||||
}),
|
|
||||||
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
|
||||||
resave: false,
|
|
||||||
saveUninitialized: true,
|
|
||||||
cookie: {
|
|
||||||
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 дней
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production', // В production должно быть true
|
|
||||||
sameSite: 'lax', // Попробуйте изменить на 'none' если используете разные домены
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = sessionMiddleware;
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
-- Добавляем поле для роли в таблицу 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);
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS roles (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name VARCHAR(50) NOT NULL UNIQUE,
|
|
||||||
description TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Добавление базовых ролей
|
|
||||||
INSERT INTO roles (name, description) VALUES
|
|
||||||
('admin', 'Администратор с полным доступом к системе'),
|
|
||||||
('user', 'Обычный пользователь с базовым доступом')
|
|
||||||
ON CONFLICT (name) DO NOTHING;
|
|
||||||
|
|
||||||
-- Добавление поля role_id в таблицу users, если оно еще не существует
|
|
||||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS role_id INTEGER REFERENCES roles(id) DEFAULT 2;
|
|
||||||
|
|
||||||
-- Таблица для отслеживания токенов доступа
|
|
||||||
CREATE TABLE IF NOT EXISTS access_tokens (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
wallet_address VARCHAR(42) NOT NULL,
|
|
||||||
token_id INTEGER NOT NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE(wallet_address, token_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Индекс для быстрого поиска по адресу кошелька
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_access_tokens_wallet ON access_tokens(wallet_address);
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
-- Таблица идентификаторов пользователей
|
|
||||||
CREATE TABLE IF NOT EXISTS user_identities (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
identity_type VARCHAR(20) NOT NULL, -- 'wallet', 'telegram', 'email'
|
|
||||||
identity_value VARCHAR(255) NOT NULL,
|
|
||||||
verified BOOLEAN DEFAULT FALSE,
|
|
||||||
verification_token VARCHAR(100),
|
|
||||||
verification_expires TIMESTAMP,
|
|
||||||
last_used TIMESTAMP,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE(identity_type, identity_value)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Таблица диалогов
|
|
||||||
CREATE TABLE IF NOT EXISTS conversations (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id INTEGER REFERENCES users(id),
|
|
||||||
title VARCHAR(255),
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Таблица сообщений
|
|
||||||
CREATE TABLE IF NOT EXISTS messages (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
conversation_id INTEGER REFERENCES conversations(id),
|
|
||||||
sender_type VARCHAR(20) NOT NULL, -- 'user', 'ai', 'admin'
|
|
||||||
sender_id INTEGER, -- ID пользователя или администратора
|
|
||||||
content TEXT,
|
|
||||||
channel VARCHAR(20) NOT NULL, -- 'web', 'telegram', 'email'
|
|
||||||
metadata JSONB, -- Дополнительная информация о сообщении
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Добавление языковых настроек в таблицу пользователей
|
|
||||||
ALTER TABLE users
|
|
||||||
ADD COLUMN IF NOT EXISTS language VARCHAR(10) DEFAULT 'en',
|
|
||||||
ADD COLUMN IF NOT EXISTS last_token_check TIMESTAMP;
|
|
||||||
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
-- Создание таблицы для хранения истории диалогов
|
|
||||||
CREATE TABLE IF NOT EXISTS chat_history (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id INTEGER REFERENCES users(id),
|
|
||||||
channel VARCHAR(20) NOT NULL, -- 'web', 'telegram', 'email'
|
|
||||||
sender_type VARCHAR(10) NOT NULL, -- 'user', 'ai', 'admin'
|
|
||||||
content TEXT,
|
|
||||||
metadata JSONB,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Индексы для быстрого поиска
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_chat_history_user_id ON chat_history(user_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_chat_history_channel ON chat_history(channel);
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
DROP FUNCTION IF EXISTS find_or_create_user_by_identity;
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION find_or_create_user_by_identity(
|
|
||||||
identity_type_param VARCHAR(20),
|
|
||||||
identity_value_param VARCHAR(255)
|
|
||||||
)
|
|
||||||
RETURNS TABLE(user_id INTEGER, is_new BOOLEAN)
|
|
||||||
AS $$
|
|
||||||
DECLARE
|
|
||||||
existing_user_id INTEGER;
|
|
||||||
new_user_id INTEGER;
|
|
||||||
BEGIN
|
|
||||||
SELECT ui.user_id INTO existing_user_id
|
|
||||||
FROM user_identities ui
|
|
||||||
WHERE ui.identity_type = identity_type_param
|
|
||||||
AND ui.identity_value = identity_value_param;
|
|
||||||
|
|
||||||
IF existing_user_id IS NOT NULL THEN
|
|
||||||
RETURN QUERY SELECT existing_user_id::INTEGER, FALSE::BOOLEAN;
|
|
||||||
RETURN;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
INSERT INTO users (created_at)
|
|
||||||
VALUES (NOW())
|
|
||||||
RETURNING id INTO new_user_id;
|
|
||||||
|
|
||||||
INSERT INTO user_identities (user_id, identity_type, identity_value, created_at, verified)
|
|
||||||
VALUES (new_user_id, identity_type_param, identity_value_param, NOW(), TRUE);
|
|
||||||
|
|
||||||
RETURN QUERY SELECT new_user_id::INTEGER, TRUE::BOOLEAN;
|
|
||||||
END;
|
|
||||||
$$ LANGUAGE plpgsql;
|
|
||||||
@@ -18,7 +18,8 @@
|
|||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"format": "prettier --write \"**/*.{js,vue,json,md}\"",
|
"format": "prettier --write \"**/*.{js,vue,json,md}\"",
|
||||||
"format:check": "prettier --check \"**/*.{js,vue,json,md}\""
|
"format:check": "prettier --check \"**/*.{js,vue,json,md}\"",
|
||||||
|
"run-migrations": "node scripts/run-migrations.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@langchain/community": "^0.3.34",
|
"@langchain/community": "^0.3.34",
|
||||||
|
|||||||
@@ -1,227 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
const { Pool } = require('pg');
|
|
||||||
const { requireAuth, requireAdmin } = require('../middleware/auth');
|
|
||||||
const db = require('../db');
|
|
||||||
const { ethers } = require('ethers');
|
|
||||||
const authService = require('../services/auth-service');
|
|
||||||
const logger = require('../utils/logger');
|
|
||||||
|
|
||||||
// Проверка доступа
|
|
||||||
router.get('/check', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { address } = req.query;
|
|
||||||
|
|
||||||
if (!address) {
|
|
||||||
return res.status(400).json({ error: 'Address is required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdmin = await authService.checkAdminToken(address);
|
|
||||||
|
|
||||||
res.json({ isAdmin });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error checking access: ${error.message}`);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Проверка прав администратора
|
|
||||||
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) => {
|
|
||||||
try {
|
|
||||||
const tokens = await authService.getAllTokens();
|
|
||||||
res.json(tokens);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error getting tokens: ${error.message}`);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Создание токена
|
|
||||||
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 isAdmin = await authService.checkAdminToken(walletAddress);
|
|
||||||
|
|
||||||
if (!isAdmin) {
|
|
||||||
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 db.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 isAdmin = await authService.checkAdminToken(walletAddress);
|
|
||||||
|
|
||||||
if (!isAdmin) {
|
|
||||||
return res.status(403).json({ error: 'Access denied' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = req.params;
|
|
||||||
|
|
||||||
// Удаляем токен
|
|
||||||
await db.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 });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Получение информации о роли текущего пользователя
|
|
||||||
router.get('/role', requireAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const role = await authService.getUserRole(req.user.id);
|
|
||||||
return res.json({ role });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Ошибка при получении роли:', error);
|
|
||||||
return res.status(500).json({ error: 'Внутренняя ошибка сервера' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Получение списка всех пользователей (только для администраторов)
|
|
||||||
router.get('/users', requireAuth, requireAdmin, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const result = await db.query(`
|
|
||||||
SELECT u.id, ui.identity_value as wallet_address, r.name as role, u.created_at
|
|
||||||
FROM users u
|
|
||||||
JOIN roles r ON u.role_id = r.id
|
|
||||||
LEFT JOIN user_identities ui ON u.id = ui.user_id AND ui.identity_type = 'wallet'
|
|
||||||
`);
|
|
||||||
|
|
||||||
return res.json(result.rows);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Ошибка при получении списка пользователей:', error);
|
|
||||||
return res.status(500).json({ error: 'Внутренняя ошибка сервера' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Обновление роли пользователя
|
|
||||||
router.post('/update-role', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { userId, role } = req.body;
|
|
||||||
|
|
||||||
if (!userId || !role) {
|
|
||||||
return res.status(400).json({ error: 'User ID and role are required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const success = await authService.updateUserRole(userId, role);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
res.json({ success: true });
|
|
||||||
} else {
|
|
||||||
res.status(400).json({ error: 'Failed to update role' });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error updating role: ${error.message}`);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Связывание нового идентификатора с аккаунтом
|
|
||||||
router.post('/link-identity', requireAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { identityType, identityValue } = req.body;
|
|
||||||
|
|
||||||
if (!identityType || !identityValue) {
|
|
||||||
return res.status(400).json({ error: 'Отсутствуют обязательные поля' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем, не привязан ли уже этот идентификатор к другому пользователю
|
|
||||||
const existingUserId = await authService.getUserIdByIdentity(identityType, identityValue);
|
|
||||||
|
|
||||||
if (existingUserId && existingUserId !== req.user.id) {
|
|
||||||
return res.status(400).json({ error: 'Этот идентификатор уже привязан к другому аккаунту' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Добавляем новый идентификатор
|
|
||||||
if (!existingUserId) {
|
|
||||||
await db.query(
|
|
||||||
'INSERT INTO user_identities (user_id, identity_type, identity_value, created_at) VALUES ($1, $2, $3, NOW())',
|
|
||||||
[req.user.id, identityType, identityValue]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если добавлен кошелек, проверяем токены
|
|
||||||
if (identityType === 'wallet') {
|
|
||||||
await authService.checkTokensAndUpdateRole(identityValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получаем все идентификаторы пользователя
|
|
||||||
const identities = await authService.getAllUserIdentities(req.user.id);
|
|
||||||
|
|
||||||
// Получаем текущую роль
|
|
||||||
const isAdmin = await authService.isAdmin(req.user.id);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
identities,
|
|
||||||
isAdmin
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Link identity error: ${error.message}`);
|
|
||||||
res.status(500).json({ error: 'Ошибка сервера' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,15 +1,38 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const db = require('../db');
|
|
||||||
const { requireAdmin } = require('../middleware/auth');
|
const { requireAdmin } = require('../middleware/auth');
|
||||||
|
const authService = require('../services/auth-service');
|
||||||
|
const logger = require('../utils/logger');
|
||||||
|
|
||||||
// Маршрут для получения списка пользователей
|
// Роли
|
||||||
router.get('/users', async (req, res) => {
|
router.get('/roles', requireAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const result = await db.query('SELECT * FROM users');
|
const roles = await authService.getAllRoles();
|
||||||
res.json(result.rows);
|
res.json({ success: true, roles });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при получении списка пользователей:', error);
|
logger.error('Error getting roles:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/roles', requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { name, permissions } = req.body;
|
||||||
|
const role = await authService.createRole(name, permissions);
|
||||||
|
res.json({ success: true, role });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error creating role:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Админ функции
|
||||||
|
router.get('/users', requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const users = await authService.getAllUsers();
|
||||||
|
res.json({ success: true, users });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error getting users:', error);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { ethers } = require('ethers');
|
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const db = require('../db');
|
const db = require('../db');
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
@@ -8,11 +7,11 @@ const helmet = require('helmet');
|
|||||||
const rateLimit = require('express-rate-limit');
|
const rateLimit = require('express-rate-limit');
|
||||||
const { checkRole, requireAuth } = require('../middleware/auth');
|
const { checkRole, requireAuth } = require('../middleware/auth');
|
||||||
const { pool } = require('../db');
|
const { pool } = require('../db');
|
||||||
const { verifySignature, checkAccess, findOrCreateUser } = require('../utils/auth');
|
|
||||||
const authService = require('../services/auth-service');
|
const authService = require('../services/auth-service');
|
||||||
const { SiweMessage } = require('siwe');
|
const { SiweMessage } = require('siwe');
|
||||||
const { sendEmail } = require('../services/emailBot');
|
const { sendEmail } = require('../services/emailBot');
|
||||||
const { verificationCodes } = require('../services/telegramBot');
|
const { verificationCodes } = require('../services/telegramBot');
|
||||||
|
const { checkTokensAndUpdateRole } = require('../services/auth-service');
|
||||||
|
|
||||||
// Создайте лимитер для попыток аутентификации
|
// Создайте лимитер для попыток аутентификации
|
||||||
const authLimiter = rateLimit({
|
const authLimiter = rateLimit({
|
||||||
@@ -87,85 +86,64 @@ async function checkUserRole(address, req) {
|
|||||||
router.post('/verify', async (req, res) => {
|
router.post('/verify', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { address, signature, message } = req.body;
|
const { address, signature, message } = req.body;
|
||||||
|
console.log('Received verification request:', { address, message });
|
||||||
|
console.log('Signature:', signature);
|
||||||
|
|
||||||
|
// Проверяем подпись через SIWE
|
||||||
|
const siwe = new SiweMessage(message);
|
||||||
|
console.log('Created SIWE message object');
|
||||||
|
|
||||||
|
const fields = await siwe.verify({ signature });
|
||||||
|
console.log('SIWE validation result:', fields);
|
||||||
|
|
||||||
console.log('Verify request: address=' + address + ', signature=' + signature.substring(0, 10) + '...');
|
if (!fields || !fields.success) {
|
||||||
|
console.log('SIWE validation failed');
|
||||||
// Получаем nonce из базы данных
|
|
||||||
const nonceResult = await db.query(`
|
|
||||||
SELECT nonce FROM nonces
|
|
||||||
WHERE identity_value = $1 AND expires_at > NOW() AND used = false
|
|
||||||
`, [address.toLowerCase()]);
|
|
||||||
|
|
||||||
if (nonceResult.rows.length === 0) {
|
|
||||||
console.error(`No valid nonce found for address ${address}`);
|
|
||||||
return res.status(401).json({ error: 'Invalid or expired nonce' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const nonce = nonceResult.rows[0].nonce;
|
|
||||||
console.log(`Found nonce ${nonce} for address ${address}`);
|
|
||||||
|
|
||||||
// Проверяем подпись
|
|
||||||
const isValid = await verifySignature(nonce, signature, address);
|
|
||||||
console.log('Signature verification result:', isValid);
|
|
||||||
|
|
||||||
if (!isValid) {
|
|
||||||
return res.status(401).json({ error: 'Invalid signature' });
|
return res.status(401).json({ error: 'Invalid signature' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Помечаем nonce как использованный
|
// Проверяем nonce
|
||||||
await db.query(`
|
const nonceResult = await db.query(
|
||||||
UPDATE nonces
|
`SELECT nonce FROM nonces
|
||||||
SET used = true
|
WHERE identity_value = $1
|
||||||
WHERE identity_value = $1
|
AND expires_at > NOW()`,
|
||||||
`, [address.toLowerCase()]);
|
[address.toLowerCase()]
|
||||||
|
);
|
||||||
// Находим или создаем пользователя
|
console.log('Nonce check result:', nonceResult.rows);
|
||||||
console.log('Finding or creating user for address:', address);
|
|
||||||
const { userId, isAdmin } = await findOrCreateUser(address);
|
if (!nonceResult.rows.length) {
|
||||||
console.log('User found/created:', { userId, isAdmin });
|
console.log('Invalid or expired nonce');
|
||||||
|
return res.status(401).json({ error: 'Invalid or expired nonce' });
|
||||||
// Сохраняем guestId перед обновлением сессии
|
|
||||||
const currentGuestId = req.session.guestId;
|
|
||||||
|
|
||||||
// Устанавливаем пользователя в сессии
|
|
||||||
req.session.userId = userId;
|
|
||||||
req.session.address = address;
|
|
||||||
req.session.isAdmin = isAdmin;
|
|
||||||
req.session.authenticated = true;
|
|
||||||
|
|
||||||
// Сохраняем guestId в новой сессии
|
|
||||||
if (currentGuestId) {
|
|
||||||
req.session.guestId = currentGuestId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сохраняем сессию ПЕРЕД отправкой ответа
|
// Проверяем соответствие nonce
|
||||||
await new Promise((resolve, reject) => {
|
if (nonceResult.rows[0].nonce !== fields.data.nonce) {
|
||||||
req.session.save(err => {
|
console.log('Nonce mismatch');
|
||||||
if (err) {
|
return res.status(401).json({ error: 'Invalid nonce' });
|
||||||
console.error('Error saving session:', err);
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
console.log('Session saved successfully');
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Authentication successful for user:', {
|
|
||||||
userId,
|
|
||||||
address,
|
|
||||||
isAdmin,
|
|
||||||
guestId: currentGuestId
|
|
||||||
});
|
|
||||||
console.log('Session after save:', req.session);
|
|
||||||
|
|
||||||
// Обрабатываем гостевые сообщения, если они есть
|
|
||||||
if (currentGuestId) {
|
|
||||||
console.log(`Processing guest messages for guestId: ${currentGuestId}`);
|
|
||||||
await authService.processGuestMessages(userId, currentGuestId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json({
|
// Получаем или создаем пользователя
|
||||||
|
const userResult = await db.query(
|
||||||
|
`INSERT INTO users (address, created_at, updated_at)
|
||||||
|
VALUES ($1, NOW(), NOW())
|
||||||
|
ON CONFLICT (address) DO UPDATE
|
||||||
|
SET updated_at = NOW()
|
||||||
|
RETURNING id, role = 'admin' as is_admin`,
|
||||||
|
[address]
|
||||||
|
);
|
||||||
|
|
||||||
|
const userId = userResult.rows[0].id;
|
||||||
|
const isAdmin = userResult.rows[0].is_admin;
|
||||||
|
|
||||||
|
// Используем централизованный сервис
|
||||||
|
await authService.createSession(req, {
|
||||||
|
userId,
|
||||||
|
address,
|
||||||
|
isAdmin,
|
||||||
|
authType: 'wallet',
|
||||||
|
guestId: req.session.guestId
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
userId,
|
userId,
|
||||||
address,
|
address,
|
||||||
@@ -173,7 +151,7 @@ router.post('/verify', async (req, res) => {
|
|||||||
authType: 'wallet'
|
authType: 'wallet'
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during wallet verification:', error);
|
console.error('Error:', error);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -818,31 +796,35 @@ router.post('/link-identity', requireAuth, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Добавляем маршрут для проверки прав доступа
|
// Добавляем маршрут для проверки прав доступа
|
||||||
router.get('/check-access', requireAuth, (req, res) => {
|
router.get('/check-access', requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// Получаем информацию о пользователе
|
const userId = req.session.userId;
|
||||||
const userData = {
|
const address = req.session.address;
|
||||||
address: req.session.address,
|
|
||||||
isAdmin: req.session.isAdmin || false,
|
|
||||||
roles: req.session.roles || [],
|
|
||||||
authenticated: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Проверяем доступ к различным разделам
|
if (address) {
|
||||||
const access = {
|
const isAdmin = await checkTokensAndUpdateRole(address);
|
||||||
dashboard: true, // Все аутентифицированные пользователи имеют доступ к панели управления
|
|
||||||
admin: userData.isAdmin, // Только администраторы имеют доступ к админке
|
// Обновляем сессию
|
||||||
contracts: userData.roles.includes('CONTRACT_MANAGER') || userData.isAdmin,
|
req.session.isAdmin = isAdmin;
|
||||||
users: userData.roles.includes('USER_MANAGER') || userData.isAdmin,
|
|
||||||
};
|
|
||||||
|
|
||||||
res.json({
|
return res.json({
|
||||||
user: userData,
|
success: true,
|
||||||
access: access,
|
isAdmin,
|
||||||
|
userId,
|
||||||
|
address
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
isAdmin: false,
|
||||||
|
userId,
|
||||||
|
address: null
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при проверке прав доступа:', error);
|
logger.error('Error checking access:', error);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
return res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { ChatOllama } = require('@langchain/ollama');
|
const aiAssistant = require('../services/ai-assistant');
|
||||||
const { getVectorStore } = require('../services/vectorStore');
|
|
||||||
const db = require('../db');
|
const db = require('../db');
|
||||||
const { requireAuth, requireAdmin } = require('../middleware/auth');
|
const { requireAuth, requireAdmin } = require('../middleware/auth');
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
@@ -105,7 +104,7 @@ async function processGuestMessages(userId, guestId) {
|
|||||||
|
|
||||||
// Получаем ответ от AI
|
// Получаем ответ от AI
|
||||||
console.log(`Getting AI response for message ${msg.id} in ${language}`);
|
console.log(`Getting AI response for message ${msg.id} in ${language}`);
|
||||||
const aiResponse = await getAIResponse(msg.content, language);
|
const aiResponse = await aiAssistant.getResponse(msg.content, language);
|
||||||
|
|
||||||
// Сохраняем ответ AI в ту же беседу
|
// Сохраняем ответ AI в ту же беседу
|
||||||
await db.query(
|
await db.query(
|
||||||
@@ -128,54 +127,44 @@ async function processGuestMessages(userId, guestId) {
|
|||||||
|
|
||||||
// Обработчик для гостевых сообщений
|
// Обработчик для гостевых сообщений
|
||||||
router.post('/guest-message', async (req, res) => {
|
router.post('/guest-message', async (req, res) => {
|
||||||
const { message, language } = req.body;
|
|
||||||
|
|
||||||
// Генерируем временный ID сессии, если его нет
|
|
||||||
if (!req.session.guestId) {
|
|
||||||
req.session.guestId = crypto.randomBytes(16).toString('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Создаем запись в conversations для гостя
|
const { message, language } = req.body;
|
||||||
const conversationResult = await db.query(
|
const guestId = req.session.guestId || crypto.randomBytes(16).toString('hex');
|
||||||
`INSERT INTO conversations (created_at)
|
|
||||||
VALUES (NOW())
|
|
||||||
RETURNING id`
|
|
||||||
);
|
|
||||||
|
|
||||||
const conversationId = conversationResult.rows[0].id;
|
|
||||||
|
|
||||||
// Создаем метаданные
|
// Сохраняем ID гостя в сессии
|
||||||
const metadata = {
|
req.session.guestId = guestId;
|
||||||
guest_id: req.session.guestId,
|
await req.session.save();
|
||||||
language: language || 'en'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Сохраняем только сообщение пользователя
|
console.log('Saving guest message:', { guestId, message });
|
||||||
await db.query(
|
|
||||||
`INSERT INTO messages
|
// Сохраняем сообщение пользователя
|
||||||
(conversation_id, sender_type, content, channel, metadata, created_at)
|
const result = await db.query(
|
||||||
VALUES ($1, 'guest', $2, 'chat', $3, NOW())`,
|
'INSERT INTO guest_messages (guest_id, content, language, is_ai) VALUES ($1, $2, $3, false) RETURNING id',
|
||||||
[
|
[guestId, message, language]
|
||||||
conversationId,
|
|
||||||
message,
|
|
||||||
JSON.stringify(metadata)
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({ success: true });
|
console.log('Guest message saved:', result.rows[0]);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
messageId: result.rows[0].id
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error processing message:', error);
|
console.error('Error saving guest message:', error);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ success: false, error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Маршрут для обычных сообщений (для аутентифицированных пользователей)
|
// Маршрут для обычных сообщений (для аутентифицированных пользователей)
|
||||||
router.post('/message', requireAuth, async (req, res) => {
|
router.post('/message', requireAuth, async (req, res) => {
|
||||||
const { message, language } = req.body;
|
|
||||||
const userId = req.session.userId;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const { message, language } = req.body;
|
||||||
|
const userId = req.session.userId;
|
||||||
|
|
||||||
|
// Используем методы из aiAssistant вместо прямого обращения к vectorStore
|
||||||
|
const similarDocs = await aiAssistant.findSimilarDocuments(message);
|
||||||
|
const aiResponse = await aiAssistant.getResponse(message, language);
|
||||||
|
|
||||||
// Создаем новую беседу или получаем существующую
|
// Создаем новую беседу или получаем существующую
|
||||||
const conversationResult = await db.query(
|
const conversationResult = await db.query(
|
||||||
`INSERT INTO conversations (user_id, created_at)
|
`INSERT INTO conversations (user_id, created_at)
|
||||||
@@ -189,20 +178,25 @@ router.post('/message', requireAuth, async (req, res) => {
|
|||||||
// Сохраняем сообщение пользователя
|
// Сохраняем сообщение пользователя
|
||||||
await db.query(
|
await db.query(
|
||||||
`INSERT INTO messages
|
`INSERT INTO messages
|
||||||
(conversation_id, sender_type, content, channel, created_at)
|
(conversation_id, sender_type, content, channel, metadata, created_at)
|
||||||
VALUES ($1, 'user', $2, 'chat', NOW())`,
|
VALUES ($1, 'user', $2, 'chat', $3, NOW())`,
|
||||||
[conversationId, message]
|
[
|
||||||
|
conversationId,
|
||||||
|
message,
|
||||||
|
JSON.stringify({ language: language || 'ru' })
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Получаем ответ от AI
|
|
||||||
const aiResponse = await getAIResponse(message, language);
|
|
||||||
|
|
||||||
// Сохраняем ответ AI
|
// Сохраняем ответ AI
|
||||||
await db.query(
|
await db.query(
|
||||||
`INSERT INTO messages
|
`INSERT INTO messages
|
||||||
(conversation_id, sender_type, content, channel, created_at)
|
(conversation_id, sender_type, content, channel, metadata, created_at)
|
||||||
VALUES ($1, 'assistant', $2, 'chat', NOW())`,
|
VALUES ($1, 'assistant', $2, 'chat', $3, NOW())`,
|
||||||
[conversationId, aiResponse]
|
[
|
||||||
|
conversationId,
|
||||||
|
aiResponse,
|
||||||
|
JSON.stringify({ language: language || 'ru' })
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -210,7 +204,7 @@ router.post('/message', requireAuth, async (req, res) => {
|
|||||||
message: aiResponse
|
message: aiResponse
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error processing message:', error);
|
logger.error('Error processing message:', error);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -233,47 +227,44 @@ router.get('/models', async (req, res) => {
|
|||||||
|
|
||||||
// Получение истории сообщений
|
// Получение истории сообщений
|
||||||
router.get('/history', async (req, res) => {
|
router.get('/history', async (req, res) => {
|
||||||
const limit = parseInt(req.query.limit) || 2; // По умолчанию только последнее сообщение и ответ
|
|
||||||
const offset = parseInt(req.query.offset) || 0;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log('Session in history route:', {
|
||||||
|
id: req.sessionID,
|
||||||
|
userId: req.session.userId,
|
||||||
|
address: req.session.address,
|
||||||
|
authenticated: req.session.authenticated
|
||||||
|
});
|
||||||
|
|
||||||
if (!req.session.authenticated || !req.session.userId) {
|
if (!req.session.authenticated || !req.session.userId) {
|
||||||
return res.status(401).json({ error: 'Unauthorized' });
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получаем общее количество сообщений
|
const limit = parseInt(req.query.limit) || 50;
|
||||||
const countResult = await db.query(
|
const offset = parseInt(req.query.offset) || 0;
|
||||||
`SELECT COUNT(*) as total
|
|
||||||
FROM messages m
|
|
||||||
JOIN conversations c ON m.conversation_id = c.id
|
|
||||||
WHERE c.user_id = $1
|
|
||||||
OR (m.metadata->>'guest_id' = $2 AND m.metadata->>'processed' = 'true')`,
|
|
||||||
[req.session.userId, req.session.guestId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const total = parseInt(countResult.rows[0].total);
|
|
||||||
|
|
||||||
// Получаем сообщения с пагинацией
|
// Получаем сообщения с пагинацией
|
||||||
const result = await db.query(
|
const result = await db.query(
|
||||||
`SELECT m.id, m.content, m.sender_type as role, m.created_at,
|
`SELECT
|
||||||
c.user_id, m.metadata
|
m.id,
|
||||||
|
m.content,
|
||||||
|
m.sender_type as role,
|
||||||
|
m.created_at,
|
||||||
|
c.user_id
|
||||||
FROM messages m
|
FROM messages m
|
||||||
JOIN conversations c ON m.conversation_id = c.id
|
JOIN conversations c ON m.conversation_id = c.id
|
||||||
WHERE c.user_id = $1
|
WHERE c.user_id = $1
|
||||||
OR (m.metadata->>'guest_id' = $2 AND m.metadata->>'processed' = 'true')
|
|
||||||
ORDER BY m.created_at DESC
|
ORDER BY m.created_at DESC
|
||||||
LIMIT $3 OFFSET $4`,
|
LIMIT $2 OFFSET $3`,
|
||||||
[req.session.userId, req.session.guestId, limit, offset]
|
[req.session.userId, limit, offset]
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
messages: result.rows.reverse(),
|
messages: result.rows.reverse()
|
||||||
total
|
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting chat history:', error);
|
logger.error('Error getting chat history:', error);
|
||||||
return res.status(500).json({ error: 'Internal server error' });
|
return res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -314,75 +305,41 @@ router.get('/admin/history', requireAdmin, async (req, res) => {
|
|||||||
// Обработчик для связывания гостевых сообщений с пользователем
|
// Обработчик для связывания гостевых сообщений с пользователем
|
||||||
router.post('/link-guest-messages', requireAuth, async (req, res) => {
|
router.post('/link-guest-messages', requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const userId = req.session.userId;
|
const { userId } = req.session;
|
||||||
const guestId = req.session.guestId;
|
const guestId = req.session.guestId;
|
||||||
|
|
||||||
|
console.log('Linking messages:', { userId, guestId });
|
||||||
|
|
||||||
if (!guestId) {
|
if (!guestId) {
|
||||||
|
console.log('No guestId in session');
|
||||||
return res.json({ success: true, message: 'No guest messages to link' });
|
return res.json({ success: true, message: 'No guest messages to link' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Связываем гостевые сообщения с пользователем
|
|
||||||
await db.query(`
|
|
||||||
INSERT INTO messages (user_id, content, role, created_at)
|
|
||||||
SELECT $1, content, CASE WHEN is_ai THEN 'assistant' ELSE 'user' END, created_at
|
|
||||||
FROM guest_messages
|
|
||||||
WHERE guest_id = $2
|
|
||||||
ORDER BY created_at
|
|
||||||
`, [userId, guestId]);
|
|
||||||
|
|
||||||
// Удаляем гостевые сообщения
|
|
||||||
await db.query(`
|
|
||||||
DELETE FROM guest_messages
|
|
||||||
WHERE guest_id = $1
|
|
||||||
`, [guestId]);
|
|
||||||
|
|
||||||
// Удаляем временный ID из сессии
|
|
||||||
delete req.session.guestId;
|
|
||||||
|
|
||||||
return res.json({ success: true });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error linking guest messages:', error);
|
|
||||||
return res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Обновляем маршрут верификации кошелька
|
// Проверяем наличие гостевых сообщений
|
||||||
router.post('/verify', async (req, res) => {
|
const guestMessages = await db.query(
|
||||||
const { address, signature, message } = req.body;
|
'SELECT EXISTS(SELECT 1 FROM guest_messages WHERE guest_id = $1)',
|
||||||
|
[guestId]
|
||||||
try {
|
);
|
||||||
// ... существующий код верификации ...
|
|
||||||
|
|
||||||
// После успешной верификации и создания пользователя
|
console.log('Guest messages check:', guestMessages.rows[0]);
|
||||||
if (req.session.guestId) {
|
|
||||||
console.log('Found guest messages, processing...');
|
if (!guestMessages.rows[0].exists) {
|
||||||
await processGuestMessages(userId, req.session.guestId);
|
console.log('No guest messages found for guestId:', guestId);
|
||||||
|
return res.json({ success: true, message: 'No guest messages to link' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сохраняем данные в сессии
|
// Связываем сообщения
|
||||||
req.session.userId = userId;
|
console.log('Calling link_guest_messages function');
|
||||||
req.session.address = address;
|
await db.query('SELECT link_guest_messages($1, $2)', [userId, guestId]);
|
||||||
req.session.isAdmin = isAdmin;
|
|
||||||
req.session.authenticated = true;
|
// Очищаем guestId из сессии после связывания
|
||||||
|
delete req.session.guestId;
|
||||||
console.log('Authentication successful for user:', {
|
|
||||||
userId,
|
console.log('Messages linked successfully');
|
||||||
address,
|
res.json({ success: true });
|
||||||
isAdmin,
|
|
||||||
guestId: req.session.guestId
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
authenticated: true,
|
|
||||||
userId: userId,
|
|
||||||
address: address,
|
|
||||||
isAdmin: isAdmin,
|
|
||||||
authType: 'wallet'
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during wallet verification:', error);
|
console.error('Error linking guest messages:', error);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ success: false, error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -406,4 +363,36 @@ router.post('/auth/telegram/verify', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Маршрут для удаления сообщений
|
||||||
|
router.delete('/message/:id', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const messageId = req.params.id;
|
||||||
|
const userId = req.session.userId;
|
||||||
|
|
||||||
|
// Проверяем права на удаление
|
||||||
|
const messageCheck = await db.query(
|
||||||
|
`SELECT m.id
|
||||||
|
FROM messages m
|
||||||
|
JOIN conversations c ON m.conversation_id = c.id
|
||||||
|
WHERE m.id = $1 AND c.user_id = $2`,
|
||||||
|
[messageId, userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (messageCheck.rows.length === 0) {
|
||||||
|
return res.status(403).json({ error: 'Forbidden' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем сообщение
|
||||||
|
await db.query(
|
||||||
|
'DELETE FROM messages WHERE id = $1',
|
||||||
|
[messageId]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error deleting message:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
const { requireRole } = require('../middleware/auth');
|
|
||||||
|
|
||||||
// Получение информации о контрактах
|
|
||||||
router.get('/', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
message: 'Contracts API endpoint',
|
|
||||||
contracts: [
|
|
||||||
// Удаляем AccessToken
|
|
||||||
// {
|
|
||||||
// name: 'AccessToken',
|
|
||||||
// address: process.env.ACCESS_TOKEN_ADDRESS,
|
|
||||||
// },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Защищенный эндпоинт для получения детальной информации о контрактах
|
|
||||||
router.get('/details', requireRole('ADMIN'), (req, res) => {
|
|
||||||
res.json({
|
|
||||||
message: 'Contract details endpoint',
|
|
||||||
contracts: [
|
|
||||||
// Удаляем AccessToken
|
|
||||||
// {
|
|
||||||
// name: 'AccessToken',
|
|
||||||
// address: process.env.ACCESS_TOKEN_ADDRESS,
|
|
||||||
// network: process.env.ETHEREUM_NETWORK_URL.includes('sepolia') ? 'Sepolia' : 'Unknown',
|
|
||||||
// },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
const db = require('../db');
|
|
||||||
|
|
||||||
// Маршрут для проверки состояния сервера
|
|
||||||
router.get('/status', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
status: 'ok',
|
|
||||||
uptime: process.uptime(),
|
|
||||||
timestamp: Date.now(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Маршрут для проверки сессии
|
|
||||||
router.get('/session', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
session: req.session,
|
|
||||||
authenticated: req.session.authenticated || false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Маршрут для проверки содержимого таблицы session
|
|
||||||
router.get('/sessions', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const result = await db.query('SELECT * FROM session');
|
|
||||||
res.json(result.rows);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при получении данных из таблицы session:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,59 +1,47 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { linkIdentity, getUserIdentities } = require('../utils/identity-linker');
|
|
||||||
const db = require('../db');
|
|
||||||
const { requireAuth } = require('../middleware/auth');
|
const { requireAuth } = require('../middleware/auth');
|
||||||
|
const authService = require('../services/auth-service');
|
||||||
|
const logger = require('../utils/logger');
|
||||||
|
|
||||||
// Получение связанных идентификаторов пользователя
|
// Получение всех идентификаторов пользователя
|
||||||
router.get('/', requireAuth, async (req, res) => {
|
router.get('/', requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// Получаем ID пользователя по Ethereum-адресу
|
const userId = req.session.userId;
|
||||||
const result = await db.query('SELECT id FROM users WHERE address = $1', [
|
const identities = await authService.getUserIdentities(userId);
|
||||||
req.session.address,
|
res.json({ success: true, identities });
|
||||||
]);
|
|
||||||
|
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error('Error getting user identities:', error);
|
logger.error('Error getting identities:', error);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Удаление связанного идентификатора
|
// Связывание нового идентификатора
|
||||||
router.delete('/:type/:value', requireAuth, async (req, res) => {
|
router.post('/link', requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { type, value } = req.params;
|
const { type, value } = req.body;
|
||||||
|
const userId = req.session.userId;
|
||||||
|
|
||||||
// Получаем ID пользователя по Ethereum-адресу
|
await authService.linkIdentity(userId, type, value);
|
||||||
const result = await db.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' });
|
if (type === 'wallet') {
|
||||||
|
req.session.address = value;
|
||||||
|
req.session.isAdmin = await authService.checkTokensAndUpdateRole(value);
|
||||||
|
} else if (type === 'telegram') {
|
||||||
|
req.session.telegramId = value;
|
||||||
|
} else if (type === 'email') {
|
||||||
|
req.session.email = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = result.rows[0].id;
|
res.json({
|
||||||
|
success: true,
|
||||||
// Удаляем идентификатор
|
message: 'Identity linked successfully',
|
||||||
await db.query(
|
isAdmin: req.session.isAdmin
|
||||||
'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) {
|
} catch (error) {
|
||||||
console.error('Error deleting user identity:', error);
|
logger.error('Error linking identity:', error);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,246 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
const { pool } = require('../db');
|
|
||||||
const { requireAuth } = require('../middleware/auth');
|
|
||||||
const { processMessage, getUserInfo } = require('../services/ai-assistant');
|
|
||||||
|
|
||||||
// Получение списка диалогов пользователя
|
|
||||||
router.get('/conversations', requireAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const userId = req.session.userId;
|
|
||||||
|
|
||||||
const result = await pool.query(
|
|
||||||
`SELECT * FROM conversation_view
|
|
||||||
WHERE user_id = $1
|
|
||||||
ORDER BY updated_at DESC`,
|
|
||||||
[userId]
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json(result.rows);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching conversations:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Получение сообщений диалога
|
|
||||||
router.get('/conversations/:id/messages', requireAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const userId = req.session.userId;
|
|
||||||
const conversationId = req.params.id;
|
|
||||||
|
|
||||||
// Проверка доступа к диалогу
|
|
||||||
const conversationCheck = await pool.query(
|
|
||||||
'SELECT id FROM conversations WHERE id = $1 AND user_id = $2',
|
|
||||||
[conversationId, userId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (conversationCheck.rows.length === 0) {
|
|
||||||
return res.status(403).json({ error: 'Access denied' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await pool.query(
|
|
||||||
`SELECT * FROM message_view
|
|
||||||
WHERE conversation_id = $1
|
|
||||||
ORDER BY created_at ASC`,
|
|
||||||
[conversationId]
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json(result.rows);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching messages:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Отправка сообщения
|
|
||||||
router.post('/conversations/:id/messages', requireAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const userId = req.session.userId;
|
|
||||||
const conversationId = req.params.id;
|
|
||||||
const { content } = req.body;
|
|
||||||
|
|
||||||
if (!content || content.trim() === '') {
|
|
||||||
return res.status(400).json({ error: 'Message content is required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверка доступа к диалогу
|
|
||||||
const conversationCheck = await pool.query(
|
|
||||||
'SELECT id FROM conversations WHERE id = $1 AND user_id = $2',
|
|
||||||
[conversationId, userId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (conversationCheck.rows.length === 0) {
|
|
||||||
return res.status(403).json({ error: 'Access denied' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновление времени последней активности диалога
|
|
||||||
await pool.query('UPDATE conversations SET updated_at = NOW() WHERE id = $1', [conversationId]);
|
|
||||||
|
|
||||||
// Сохранение сообщения пользователя
|
|
||||||
const userMessageResult = await pool.query(
|
|
||||||
`INSERT INTO messages
|
|
||||||
(conversation_id, sender_type, sender_id, content, channel)
|
|
||||||
VALUES ($1, 'user', $2, $3, 'web')
|
|
||||||
RETURNING *`,
|
|
||||||
[conversationId, userId, content]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Получение информации о пользователе для ИИ
|
|
||||||
const userInfo = await getUserInfo(userId);
|
|
||||||
|
|
||||||
// Обработка сообщения ИИ-ассистентом
|
|
||||||
const aiResponse = await processMessage(userId, content, userInfo.language || 'ru');
|
|
||||||
|
|
||||||
// Сохранение ответа ИИ
|
|
||||||
const aiMessageResult = await pool.query(
|
|
||||||
`INSERT INTO messages
|
|
||||||
(conversation_id, sender_type, content, channel)
|
|
||||||
VALUES ($1, 'ai', $2, 'web')
|
|
||||||
RETURNING *`,
|
|
||||||
[conversationId, aiResponse]
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
userMessage: userMessageResult.rows[0],
|
|
||||||
aiMessage: aiMessageResult.rows[0],
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error sending message:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Создание нового диалога
|
|
||||||
router.post('/conversations', requireAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const userId = req.session.userId;
|
|
||||||
const { title } = req.body;
|
|
||||||
|
|
||||||
// Создание нового диалога
|
|
||||||
const result = await pool.query(
|
|
||||||
`INSERT INTO conversations (user_id, title)
|
|
||||||
VALUES ($1, $2)
|
|
||||||
RETURNING *`,
|
|
||||||
[userId, title || 'Новый диалог']
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json(result.rows[0]);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error creating conversation:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Обновление заголовка диалога
|
|
||||||
router.put('/conversations/:id', requireAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const userId = req.session.userId;
|
|
||||||
const conversationId = req.params.id;
|
|
||||||
const { title } = req.body;
|
|
||||||
|
|
||||||
if (!title || title.trim() === '') {
|
|
||||||
return res.status(400).json({ error: 'Title is required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверка доступа к диалогу
|
|
||||||
const conversationCheck = await pool.query(
|
|
||||||
'SELECT id FROM conversations WHERE id = $1 AND user_id = $2',
|
|
||||||
[conversationId, userId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (conversationCheck.rows.length === 0) {
|
|
||||||
return res.status(403).json({ error: 'Access denied' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновление заголовка
|
|
||||||
const result = await pool.query(
|
|
||||||
'UPDATE conversations SET title = $1 WHERE id = $2 RETURNING *',
|
|
||||||
[title, conversationId]
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json(result.rows[0]);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating conversation:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Удаление диалога
|
|
||||||
router.delete('/conversations/:id', requireAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const userId = req.session.userId;
|
|
||||||
const conversationId = req.params.id;
|
|
||||||
|
|
||||||
// Проверка доступа к диалогу
|
|
||||||
const conversationCheck = await pool.query(
|
|
||||||
'SELECT id FROM conversations WHERE id = $1 AND user_id = $2',
|
|
||||||
[conversationId, userId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (conversationCheck.rows.length === 0) {
|
|
||||||
return res.status(403).json({ error: 'Access denied' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Удаление диалога (каскадно удалит все сообщения)
|
|
||||||
await pool.query('DELETE FROM conversations WHERE id = $1', [conversationId]);
|
|
||||||
|
|
||||||
res.json({ success: true });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting conversation:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Маршруты для администраторов
|
|
||||||
|
|
||||||
// Получение всех диалогов (только для администраторов)
|
|
||||||
router.get('/admin/conversations', requireAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
// Проверка прав администратора
|
|
||||||
if (!req.session.isAdmin) {
|
|
||||||
return res.status(403).json({ error: 'Admin access required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await pool.query(
|
|
||||||
`SELECT * FROM conversation_view
|
|
||||||
ORDER BY updated_at DESC`
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json(result.rows);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching all conversations:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Получение статистики по каналам (только для администраторов)
|
|
||||||
router.get('/admin/stats/channels', requireAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
// Проверка прав администратора
|
|
||||||
if (!req.session.isAdmin) {
|
|
||||||
return res.status(403).json({ error: 'Admin access required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await pool.query(
|
|
||||||
`SELECT
|
|
||||||
channel,
|
|
||||||
COUNT(*) AS message_count,
|
|
||||||
COUNT(DISTINCT conversation_id) AS conversation_count,
|
|
||||||
COUNT(DISTINCT sender_id) AS user_count,
|
|
||||||
MIN(created_at) AS first_message,
|
|
||||||
MAX(created_at) AS last_message
|
|
||||||
FROM
|
|
||||||
messages
|
|
||||||
GROUP BY
|
|
||||||
channel`
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json(result.rows);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching channel stats:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
const db = require('../db');
|
|
||||||
const { requireAuth, requireAdmin } = require('../middleware/auth');
|
|
||||||
const authService = require('../services/auth-service');
|
|
||||||
const logger = require('../utils/logger');
|
|
||||||
const { USER_ROLES } = require('../utils/constants');
|
|
||||||
|
|
||||||
// Получение всех ролей
|
|
||||||
router.get('/', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const result = await db.query('SELECT * FROM roles ORDER BY id');
|
|
||||||
res.json(result.rows);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error getting roles: ${error.message}`);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Получение роли текущего пользователя
|
|
||||||
router.get('/me', requireAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const userId = req.session.userId;
|
|
||||||
|
|
||||||
const result = await db.query(`
|
|
||||||
SELECT r.name as role
|
|
||||||
FROM users u
|
|
||||||
JOIN roles r ON u.role_id = r.id
|
|
||||||
WHERE u.id = $1
|
|
||||||
`, [userId]);
|
|
||||||
|
|
||||||
if (result.rows.length === 0) {
|
|
||||||
return res.status(404).json({ error: 'User not found' });
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ role: result.rows[0].role });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error getting user role: ${error.message}`);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Создание новой роли (только для администраторов)
|
|
||||||
router.post('/', requireAuth, requireAdmin, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { name, description } = req.body;
|
|
||||||
|
|
||||||
if (!name) {
|
|
||||||
return res.status(400).json({ error: 'Role name is required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем, существует ли уже такая роль
|
|
||||||
const existingRole = await db.query('SELECT * FROM roles WHERE name = $1', [name]);
|
|
||||||
|
|
||||||
if (existingRole.rows.length > 0) {
|
|
||||||
return res.status(400).json({ error: 'Role already exists' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создаем новую роль
|
|
||||||
const result = await db.query(
|
|
||||||
'INSERT INTO roles (name, description) VALUES ($1, $2) RETURNING *',
|
|
||||||
[name, description || '']
|
|
||||||
);
|
|
||||||
|
|
||||||
res.status(201).json(result.rows[0]);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error creating role: ${error.message}`);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Обновление роли (только для администраторов)
|
|
||||||
router.put('/:id', requireAuth, requireAdmin, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const { name, description } = req.body;
|
|
||||||
|
|
||||||
if (!name) {
|
|
||||||
return res.status(400).json({ error: 'Role name is required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем, существует ли роль
|
|
||||||
const existingRole = await db.query('SELECT * FROM roles WHERE id = $1', [id]);
|
|
||||||
|
|
||||||
if (existingRole.rows.length === 0) {
|
|
||||||
return res.status(404).json({ error: 'Role not found' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновляем роль
|
|
||||||
const result = await db.query(
|
|
||||||
'UPDATE roles SET name = $1, description = $2 WHERE id = $3 RETURNING *',
|
|
||||||
[name, description || '', id]
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json(result.rows[0]);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error updating role: ${error.message}`);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Удаление роли (только для администраторов)
|
|
||||||
router.delete('/:id', requireAuth, requireAdmin, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
|
|
||||||
// Проверяем, существует ли роль
|
|
||||||
const existingRole = await db.query('SELECT * FROM roles WHERE id = $1', [id]);
|
|
||||||
|
|
||||||
if (existingRole.rows.length === 0) {
|
|
||||||
return res.status(404).json({ error: 'Role not found' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем, не используется ли роль
|
|
||||||
const usersWithRole = await db.query('SELECT COUNT(*) FROM users WHERE role_id = $1', [id]);
|
|
||||||
|
|
||||||
if (parseInt(usersWithRole.rows[0].count) > 0) {
|
|
||||||
return res.status(400).json({ error: 'Cannot delete role that is assigned to users' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Удаляем роль
|
|
||||||
await db.query('DELETE FROM roles WHERE id = $1', [id]);
|
|
||||||
|
|
||||||
res.json({ success: true });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error deleting role: ${error.message}`);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Назначение роли пользователю (только для администраторов)
|
|
||||||
router.post('/assign', requireAuth, requireAdmin, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { userId, roleName } = req.body;
|
|
||||||
|
|
||||||
if (!userId || !roleName) {
|
|
||||||
return res.status(400).json({ error: 'User ID and role name are required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем, существует ли роль
|
|
||||||
const roleResult = await db.query('SELECT id FROM roles WHERE name = $1', [roleName]);
|
|
||||||
|
|
||||||
if (roleResult.rows.length === 0) {
|
|
||||||
return res.status(404).json({ error: 'Role not found' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const roleId = roleResult.rows[0].id;
|
|
||||||
|
|
||||||
// Проверяем, существует ли пользователь
|
|
||||||
const userResult = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
|
|
||||||
|
|
||||||
if (userResult.rows.length === 0) {
|
|
||||||
return res.status(404).json({ error: 'User not found' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Назначаем роль
|
|
||||||
await db.query('UPDATE users SET role_id = $1 WHERE id = $2', [roleId, userId]);
|
|
||||||
|
|
||||||
res.json({ success: true });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error assigning role: ${error.message}`);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
const { getContract } = require('../utils/contracts');
|
|
||||||
const logger = require('../utils/logger');
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
try {
|
|
||||||
const accessToken = await getContract('AccessToken');
|
|
||||||
|
|
||||||
const owner = await accessToken.owner();
|
|
||||||
logger.info('Contract owner:', owner);
|
|
||||||
|
|
||||||
// Проверяем все токены и их владельцев
|
|
||||||
logger.info('\nAll tokens:');
|
|
||||||
for (let i = 1; i <= 10; i++) {
|
|
||||||
try {
|
|
||||||
const tokenOwner = await accessToken.ownerOf(i);
|
|
||||||
logger.info(`Token ${i} owner: ${tokenOwner}`);
|
|
||||||
} catch (error) {
|
|
||||||
if (!error.message.includes('invalid token ID')) {
|
|
||||||
logger.error(`Token ${i} error:`, error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем активные токены для всех известных адресов
|
|
||||||
const addresses = [owner, '0x70997970C51812dc3A010C7d01b50e0d17dc79C8'];
|
|
||||||
|
|
||||||
logger.info('\nActive tokens:');
|
|
||||||
for (const address of addresses) {
|
|
||||||
const activeToken = await accessToken.activeTokens(address);
|
|
||||||
logger.info(`${address}: Token ${activeToken.toString()}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
||||||
.then(() => process.exit(0))
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
const { checkAllUsersTokens } = require('../utils/access-check');
|
|
||||||
const logger = require('../utils/logger');
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
logger.info('Starting token balance check for all users');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await checkAllUsersTokens();
|
|
||||||
logger.info('Token balance check completed successfully');
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error during token balance check: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Запуск скрипта
|
|
||||||
main()
|
|
||||||
.then(() => process.exit(0))
|
|
||||||
.catch(error => {
|
|
||||||
logger.error(`Unhandled error: ${error.message}`);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
const hre = require('hardhat');
|
|
||||||
const logger = require('../utils/logger');
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
try {
|
|
||||||
const AccessToken = await hre.ethers.getContractFactory('AccessToken');
|
|
||||||
const accessToken = await AccessToken.deploy();
|
|
||||||
await accessToken.waitForDeployment();
|
|
||||||
|
|
||||||
const address = await accessToken.getAddress();
|
|
||||||
logger.info('AccessToken deployed to:', address);
|
|
||||||
|
|
||||||
// Создаем первый админский токен для владельца контракта
|
|
||||||
const [owner] = await hre.ethers.getSigners();
|
|
||||||
const tx = await accessToken.mintAccessToken(owner.address, 1); // 1 = ADMIN
|
|
||||||
await tx.wait();
|
|
||||||
logger.info('Admin token minted for:', owner.address);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Deployment error:', error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
||||||
.then(() => process.exit(0))
|
|
||||||
.catch((error) => {
|
|
||||||
logger.error('Unhandled error:', error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
const hre = require('hardhat');
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
console.log('Начинаем деплой контракта...');
|
|
||||||
|
|
||||||
// Получаем контракт
|
|
||||||
const MyContract = await hre.ethers.getContractFactory('MyContract');
|
|
||||||
|
|
||||||
// Деплоим контракт
|
|
||||||
const myContract = await MyContract.deploy();
|
|
||||||
await myContract.waitForDeployment();
|
|
||||||
|
|
||||||
const address = await myContract.getAddress();
|
|
||||||
console.log('Контракт развернут по адресу:', address);
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
||||||
.then(() => process.exit(0))
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
@@ -1,13 +1,8 @@
|
|||||||
const { Pool } = require('pg');
|
const fs = require('fs').promises;
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
const { pool } = require('../db');
|
||||||
// Подключение к БД
|
const logger = require('../utils/logger');
|
||||||
const pool = new Pool({
|
|
||||||
connectionString: process.env.DATABASE_URL,
|
|
||||||
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
|
|
||||||
});
|
|
||||||
|
|
||||||
async function runMigrations() {
|
async function runMigrations() {
|
||||||
try {
|
try {
|
||||||
@@ -18,39 +13,64 @@ async function runMigrations() {
|
|||||||
CREATE TABLE IF NOT EXISTS migrations (
|
CREATE TABLE IF NOT EXISTS migrations (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
name VARCHAR(255) NOT NULL,
|
name VARCHAR(255) NOT NULL,
|
||||||
applied_at TIMESTAMP DEFAULT NOW()
|
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Получаем список уже примененных миграций
|
// Получаем список выполненных миграций
|
||||||
const { rows } = await pool.query('SELECT name FROM migrations');
|
const { rows } = await pool.query('SELECT name FROM migrations');
|
||||||
const appliedMigrations = rows.map((row) => row.name);
|
const executedMigrations = new Set(rows.map(row => row.name));
|
||||||
|
|
||||||
// Получаем список файлов миграций
|
// Читаем файлы миграций
|
||||||
const migrationsDir = path.join(__dirname, '../migrations');
|
const migrationsDir = path.join(__dirname, '../db/migrations');
|
||||||
const migrationFiles = fs
|
const files = await fs.readdir(migrationsDir);
|
||||||
.readdirSync(migrationsDir)
|
|
||||||
.filter((file) => file.endsWith('.sql'))
|
|
||||||
.sort(); // Сортируем файлы по имени
|
|
||||||
|
|
||||||
// Применяем миграции, которые еще не были применены
|
// Сортируем файлы по номеру
|
||||||
|
const migrationFiles = files
|
||||||
|
.filter(f => f.endsWith('.sql'))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const numA = parseInt(a.split('_')[0]);
|
||||||
|
const numB = parseInt(b.split('_')[0]);
|
||||||
|
return numA - numB;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Выполняем миграции
|
||||||
for (const file of migrationFiles) {
|
for (const file of migrationFiles) {
|
||||||
if (!appliedMigrations.includes(file)) {
|
if (!executedMigrations.has(file)) {
|
||||||
console.log(`Применение миграции: ${file}`);
|
|
||||||
|
|
||||||
// Читаем содержимое файла миграции
|
|
||||||
const filePath = path.join(migrationsDir, file);
|
const filePath = path.join(migrationsDir, file);
|
||||||
const sql = fs.readFileSync(filePath, 'utf8');
|
const sql = await fs.readFile(filePath, 'utf-8');
|
||||||
|
|
||||||
// Выполняем SQL-запросы из файла
|
await pool.query('BEGIN');
|
||||||
await pool.query(sql);
|
try {
|
||||||
|
await pool.query(sql);
|
||||||
|
await pool.query('INSERT INTO migrations (name) VALUES ($1)', [file]);
|
||||||
|
await pool.query('COMMIT');
|
||||||
|
logger.info(`Migration ${file} executed successfully`);
|
||||||
|
} catch (error) {
|
||||||
|
await pool.query('ROLLBACK');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Записываем информацию о примененной миграции
|
// Выполняем SQL-функции
|
||||||
await pool.query('INSERT INTO migrations (name) VALUES ($1)', [file]);
|
const functionsDir = path.join(migrationsDir, 'functions');
|
||||||
|
if (await fs.stat(functionsDir).then(() => true).catch(() => false)) {
|
||||||
console.log(`Миграция ${file} успешно применена`);
|
const functionFiles = await fs.readdir(functionsDir);
|
||||||
} else {
|
|
||||||
console.log(`Миграция ${file} уже применена`);
|
for (const file of functionFiles) {
|
||||||
|
if (file.endsWith('.sql')) {
|
||||||
|
const filePath = path.join(functionsDir, file);
|
||||||
|
const sql = await fs.readFile(filePath, 'utf-8');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pool.query(sql);
|
||||||
|
logger.info(`Function ${file} executed successfully`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error executing function ${file}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
const { checkAllUsersTokens } = require('../utils/access-check');
|
|
||||||
const db = require('../db');
|
|
||||||
const logger = require('../utils/logger');
|
|
||||||
|
|
||||||
async function updateRolesFromOldStructure() {
|
|
||||||
try {
|
|
||||||
logger.info('Starting migration of user roles from old structure');
|
|
||||||
|
|
||||||
// Получаем пользователей со старым полем role
|
|
||||||
const usersWithOldRoles = await db.query(`
|
|
||||||
SELECT id, role, address
|
|
||||||
FROM users
|
|
||||||
WHERE role IS NOT NULL AND role_id IS NULL
|
|
||||||
`);
|
|
||||||
|
|
||||||
logger.info(`Found ${usersWithOldRoles.rows.length} users with old role structure`);
|
|
||||||
|
|
||||||
for (const user of usersWithOldRoles.rows) {
|
|
||||||
// Определяем ID роли
|
|
||||||
let roleId = 2; // По умолчанию 'user'
|
|
||||||
|
|
||||||
if (user.role === 'ADMIN' || user.role === 'admin') {
|
|
||||||
roleId = 1; // 'admin'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновляем пользователя
|
|
||||||
await db.query(
|
|
||||||
'UPDATE users SET role_id = $1 WHERE id = $2',
|
|
||||||
[roleId, user.id]
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.info(`Updated user ${user.id} with role_id ${roleId} (from old role ${user.role})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Запускаем проверку токенов для всех пользователей
|
|
||||||
await checkAllUsersTokens();
|
|
||||||
|
|
||||||
logger.info('Role migration completed successfully');
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error during role migration: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Запуск скрипта
|
|
||||||
updateRolesFromOldStructure()
|
|
||||||
.then(() => {
|
|
||||||
logger.info('Migration script completed');
|
|
||||||
process.exit(0);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
logger.error(`Unhandled error: ${error.message}`);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,39 +1,19 @@
|
|||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const { SiweMessage, generateNonce } = require('siwe');
|
|
||||||
const { ethers } = require('ethers');
|
const { ethers } = require('ethers');
|
||||||
// const TelegramBotService = require('./services/telegramBot');
|
|
||||||
const EmailBotService = require('./services/emailBot');
|
const EmailBotService = require('./services/emailBot');
|
||||||
const { initializeVectorStore } = require('./services/vectorStore');
|
|
||||||
const session = require('express-session');
|
const session = require('express-session');
|
||||||
const { app, nonceStore } = require('./app');
|
const { app, nonceStore } = require('./app');
|
||||||
const usersRouter = require('./routes/users');
|
const usersRouter = require('./routes/users');
|
||||||
const authRouter = require('./routes/auth');
|
const authRouter = require('./routes/auth');
|
||||||
const contractsRouter = require('./routes/contracts');
|
|
||||||
const accessRouter = require('./routes/access');
|
|
||||||
const path = require('path');
|
|
||||||
const axios = require('axios');
|
|
||||||
const { ChatOllama } = require('@langchain/ollama');
|
|
||||||
const { getVectorStore } = require('./services/vectorStore');
|
|
||||||
// const debugRoutes = require('./routes/debug');
|
|
||||||
const identitiesRouter = require('./routes/identities');
|
const identitiesRouter = require('./routes/identities');
|
||||||
const { pool } = require('./db');
|
const { pool } = require('./db');
|
||||||
const fs = require('fs');
|
|
||||||
const pgSession = require('connect-pg-simple')(session);
|
|
||||||
const sessionStore = new pgSession({
|
|
||||||
pool: pool,
|
|
||||||
tableName: 'sessions',
|
|
||||||
createTableIfMissing: true,
|
|
||||||
});
|
|
||||||
const helmet = require('helmet');
|
const helmet = require('helmet');
|
||||||
// const csrf = require('csurf');
|
const TelegramBotService = require('./services/telegramBot');
|
||||||
// const cookieParser = require('cookie-parser');
|
const pgSession = require('connect-pg-simple')(session);
|
||||||
const messagesRouter = require('./routes/messages');
|
const authService = require('./services/auth-service');
|
||||||
const sessionMiddleware = require('./middleware/session');
|
const logger = require('./utils/logger');
|
||||||
|
|
||||||
// Импорт сервисов
|
|
||||||
const telegramService = require('./services/telegramBot');
|
|
||||||
|
|
||||||
const PORT = process.env.PORT || 8000;
|
const PORT = process.env.PORT || 8000;
|
||||||
|
|
||||||
@@ -42,83 +22,21 @@ console.log('Переменная окружения PORT:', process.env.PORT);
|
|||||||
console.log('Используемый порт:', process.env.PORT || 8000);
|
console.log('Используемый порт:', process.env.PORT || 8000);
|
||||||
|
|
||||||
// Инициализация сервисов
|
// Инициализация сервисов
|
||||||
let telegramBot;
|
async function initServices() {
|
||||||
let emailBot;
|
try {
|
||||||
|
console.log('Инициализация сервисов...');
|
||||||
|
|
||||||
// Проверяем, что библиотека ethers.js правильно импортирована
|
if (process.env.TELEGRAM_BOT_TOKEN) {
|
||||||
console.log('Ethers.js version:', ethers.version);
|
const telegramBot = new TelegramBotService(process.env.TELEGRAM_BOT_TOKEN);
|
||||||
|
global.telegramBot = telegramBot; // Сохраняем экземпляр глобально
|
||||||
|
console.log('Telegram бот инициализирован');
|
||||||
|
}
|
||||||
|
|
||||||
// Порядок middleware важен!
|
console.log('Все сервисы успешно инициализированы');
|
||||||
// 1. CORS должен быть первым
|
} catch (error) {
|
||||||
app.use(
|
console.error('Ошибка при инициализации сервисов:', error);
|
||||||
cors({
|
|
||||||
origin: ['http://localhost:5173', 'http://127.0.0.1:5173'], // Укажем точные домены
|
|
||||||
credentials: true, // Важно для передачи куки
|
|
||||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
||||||
allowedHeaders: ['Content-Type', 'Authorization']
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Добавьте после настройки CORS
|
|
||||||
app.use(helmet());
|
|
||||||
|
|
||||||
// 2. Затем парсеры
|
|
||||||
app.use(express.json());
|
|
||||||
app.use(express.urlencoded({ extended: true }));
|
|
||||||
|
|
||||||
// Добавьте после настройки парсеров
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|
||||||
const requireAuth = (req, res, next) => {
|
|
||||||
if (!req.session.authenticated || !req.session.address) {
|
|
||||||
return res.status(401).json({ error: 'Unauthorized' });
|
|
||||||
}
|
}
|
||||||
next();
|
}
|
||||||
};
|
|
||||||
|
|
||||||
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('Сессия:', req.session);
|
|
||||||
console.log('Куки:', req.headers.cookie);
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Настройка сессий
|
// Настройка сессий
|
||||||
app.use(session({
|
app.use(session({
|
||||||
@@ -136,551 +54,34 @@ app.use(session({
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
async function initServices() {
|
// Маршруты API
|
||||||
try {
|
|
||||||
console.log('Инициализация сервисов...');
|
|
||||||
|
|
||||||
// Инициализируем ботов, если они нужны
|
|
||||||
if (process.env.TELEGRAM_BOT_TOKEN) {
|
|
||||||
telegramBot = new telegramService(process.env.TELEGRAM_BOT_TOKEN);
|
|
||||||
console.log('Telegram бот инициализирован');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.EMAIL_USER && process.env.EMAIL_PASS) {
|
|
||||||
emailBot = new EmailBotService(process.env.EMAIL_USER, process.env.EMAIL_PASS);
|
|
||||||
console.log('Email бот инициализирован');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Все сервисы успешно инициализированы');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при инициализации сервисов:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app.use('/api/users', usersRouter);
|
app.use('/api/users', usersRouter);
|
||||||
app.use('/api/auth', authRouter);
|
app.use('/api/auth', authRouter);
|
||||||
app.use('/api/contracts', contractsRouter);
|
|
||||||
app.use('/api/access', accessRouter);
|
|
||||||
// app.use('/api/chat', chatRouter);
|
|
||||||
// app.use('/api/debug', debugRoutes);
|
|
||||||
app.use('/api/identities', identitiesRouter);
|
app.use('/api/identities', identitiesRouter);
|
||||||
app.use('/api/messages', messagesRouter);
|
|
||||||
|
|
||||||
// Добавьте простой эндпоинт для проверки состояния сервера
|
// Эндпоинт для проверки состояния сервера
|
||||||
app.get('/api/health', (req, res) => {
|
app.get('/api/health', (req, res) => {
|
||||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Добавьте после настройки маршрутов
|
// Запуск сервера
|
||||||
app.post('/api/verify', async (req, res) => {
|
app.listen(PORT, async () => {
|
||||||
try {
|
try {
|
||||||
// Перенаправляем запрос на /api/auth/verify
|
await initServices();
|
||||||
const { message, signature } = req.body;
|
console.log('Server is running on port', PORT);
|
||||||
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' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Только после проверки устанавливаем сессию
|
|
||||||
req.session.authenticated = true;
|
|
||||||
req.session.address = fields.address;
|
|
||||||
req.session.lastSignature = signature;
|
|
||||||
|
|
||||||
// Сохраняем сессию
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
req.session.save((err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Ошибка при сохранении сессии:', err);
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
console.log('Сессия успешно сохранена');
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} 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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
res.cookie('authToken', 'true', {
|
|
||||||
maxAge: 86400000,
|
|
||||||
httpOnly: false,
|
|
||||||
secure: false,
|
|
||||||
sameSite: 'lax',
|
|
||||||
path: '/',
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
address: address,
|
|
||||||
isAdmin: isAdmin,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка верификации:', error);
|
console.error('Error starting server:', error);
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: error.message || 'Внутренняя ошибка сервера',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Добавьте после настройки маршрутов
|
|
||||||
app.get('/api/session', (req, res) => {
|
|
||||||
console.log('Запрос сессии в server.js:', {
|
|
||||||
sessionExists: !!req.session,
|
|
||||||
sessionID: req.sessionID,
|
|
||||||
isAuthenticated: req.session?.isAuthenticated,
|
|
||||||
authenticated: req.session?.authenticated,
|
|
||||||
address: req.session?.address,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (req.session && (req.session.isAuthenticated || req.session.authenticated)) {
|
|
||||||
res.json({
|
|
||||||
isAuthenticated: true,
|
|
||||||
authenticated: true,
|
|
||||||
address: req.session.address,
|
|
||||||
isAdmin: req.session.isAdmin,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
res.json({
|
|
||||||
isAuthenticated: false,
|
|
||||||
authenticated: false,
|
|
||||||
address: null,
|
|
||||||
isAdmin: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/balance', requireAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const balance = await contract.balanceOf(req.session.address);
|
|
||||||
res.json({ balance: balance.toString() });
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Добавляем тестовые маршруты API
|
|
||||||
app.get('/api/public', (req, res) => {
|
|
||||||
res.json({ message: 'This is a public API endpoint' });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/protected', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
message: 'This is a protected API endpoint',
|
|
||||||
user: {
|
|
||||||
address: req.session.address,
|
|
||||||
isAdmin: req.session.isAdmin,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/admin', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
message: 'This is an admin API endpoint',
|
|
||||||
user: {
|
|
||||||
address: req.session.address,
|
|
||||||
isAdmin: req.session.isAdmin,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Добавьте обработчик ошибок
|
|
||||||
app.use((err, req, res, next) => {
|
|
||||||
console.error('Глобальная ошибка:', err.stack);
|
|
||||||
if (!res.headersSent) {
|
|
||||||
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Перед запуском сервера
|
|
||||||
console.log('Перед запуском сервера на порту:', PORT);
|
|
||||||
|
|
||||||
// Запуск сервера и инициализация сервисов
|
|
||||||
let server;
|
|
||||||
|
|
||||||
checkDatabaseStructure().then(() => {
|
|
||||||
// Запускаем сервер
|
|
||||||
server = app.listen(PORT, () => {
|
|
||||||
console.log(`Server is running on port ${PORT}`);
|
|
||||||
console.log('Server address:', server.address());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Добавляем graceful shutdown
|
|
||||||
process.on('SIGTERM', () => {
|
|
||||||
console.log('SIGTERM signal received: closing HTTP server');
|
|
||||||
server.close(() => {
|
|
||||||
console.log('HTTP server closed');
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Проверяем доступность Ollama сервера
|
|
||||||
async function checkOllamaServer() {
|
|
||||||
try {
|
|
||||||
const response = await axios.get('http://localhost:11434/api/tags');
|
|
||||||
if (response.status === 200) {
|
|
||||||
console.log('Ollama сервер доступен');
|
|
||||||
|
|
||||||
// Тестируем прямой запрос к Ollama
|
|
||||||
try {
|
|
||||||
console.log('Тестируем прямой запрос к Ollama...');
|
|
||||||
const model = new ChatOllama({
|
|
||||||
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) {
|
|
||||||
console.error('Ollama сервер недоступен:', error.message);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Настройка периодической очистки устаревших сессий
|
|
||||||
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 {
|
|
||||||
// Проверяем существование таблицы users
|
|
||||||
const result = await pool.query(`
|
|
||||||
SELECT EXISTS (
|
|
||||||
SELECT FROM information_schema.tables
|
|
||||||
WHERE table_schema = 'public'
|
|
||||||
AND table_name = 'users'
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Если таблица не существует, создаем все таблицы
|
|
||||||
if (!result.rows[0].exists) {
|
|
||||||
console.log('Таблицы не найдены, создаем...');
|
|
||||||
|
|
||||||
// SQL-запросы для создания таблиц
|
|
||||||
const createTablesSql = `
|
|
||||||
-- Таблица пользователей
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
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()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Индексы для таблицы пользователей
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_address ON users(address);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_telegram_id ON users(telegram_id);
|
|
||||||
|
|
||||||
-- Таблица сессий
|
|
||||||
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)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Индекс для таблицы сессий
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_session_expire ON session(expire);
|
|
||||||
|
|
||||||
-- Таблица канбан-досок
|
|
||||||
CREATE TABLE IF NOT EXISTS kanban_boards (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
title VARCHAR(255) 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(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('Таблицы уже существуют');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при проверке/создании таблиц:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Вызываем функцию при запуске сервера
|
|
||||||
ensureTablesExist();
|
|
||||||
|
|
||||||
// Добавляем middleware для проверки аутентификации
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Проверка структуры базы данных
|
|
||||||
async function checkDatabaseStructure() {
|
|
||||||
try {
|
|
||||||
const db = require('./db');
|
|
||||||
|
|
||||||
// Проверяем наличие таблицы roles
|
|
||||||
const rolesTable = await db.query(`
|
|
||||||
SELECT EXISTS (
|
|
||||||
SELECT FROM information_schema.tables
|
|
||||||
WHERE table_name = 'roles'
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (!rolesTable.rows[0].exists) {
|
|
||||||
console.error('Таблица roles не существует. Выполните миграцию.');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем наличие колонки role_id в таблице users
|
|
||||||
const roleIdColumn = await db.query(`
|
|
||||||
SELECT EXISTS (
|
|
||||||
SELECT FROM information_schema.columns
|
|
||||||
WHERE table_name = 'users' AND column_name = 'role_id'
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (!roleIdColumn.rows[0].exists) {
|
|
||||||
console.error('Колонка role_id не существует в таблице users. Выполните миграцию.');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Структура базы данных проверена успешно.');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при проверке структуры базы данных:', error);
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Обработка сигналов завершения
|
|
||||||
process.on('SIGINT', () => {
|
|
||||||
console.log('Получен сигнал SIGINT, завершаем работу...');
|
|
||||||
server.close(() => {
|
|
||||||
console.log('Сервер остановлен');
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', () => {
|
// Обработка ошибок
|
||||||
console.log('Получен сигнал SIGTERM, завершаем работу...');
|
process.on('unhandledRejection', (err) => {
|
||||||
server.close(() => {
|
logger.error('Unhandled Rejection:', err);
|
||||||
console.log('Сервер остановлен');
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Обработка необработанных исключений
|
process.on('uncaughtException', (err) => {
|
||||||
process.on('uncaughtException', (error) => {
|
logger.error('Uncaught Exception:', err);
|
||||||
console.error('Необработанное исключение:', error);
|
|
||||||
// Не завершаем процесс, чтобы nodemon мог перезапустить сервер
|
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason, promise) => {
|
module.exports = app;
|
||||||
console.error('Необработанное отклонение промиса:', reason);
|
|
||||||
// Не завершаем процесс, чтобы nodemon мог перезапустить сервер
|
|
||||||
});
|
|
||||||
|
|
||||||
// Инициализация Telegram бота
|
|
||||||
telegramService.initTelegramBot();
|
|
||||||
|
|
||||||
// Добавьте после других маршрутов
|
|
||||||
const chatRouter = require('./routes/chat');
|
|
||||||
app.use('/api/chat', chatRouter);
|
|
||||||
|
|
||||||
const cron = require('node-cron');
|
|
||||||
const { checkAllUsersTokens } = require('./utils/access-check');
|
|
||||||
const logger = require('./utils/logger');
|
|
||||||
|
|
||||||
// Настройка cron-задачи для проверки токенов каждые 30 минут
|
|
||||||
cron.schedule('*/30 * * * *', async () => {
|
|
||||||
logger.info('Running scheduled token balance check');
|
|
||||||
await checkAllUsersTokens();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Периодическая очистка устаревших сессий
|
|
||||||
const cleanupInterval = 24 * 60 * 60 * 1000; // 24 часа
|
|
||||||
|
|
||||||
setInterval(async () => {
|
|
||||||
try {
|
|
||||||
const { pool } = require('./db');
|
|
||||||
const result = await pool.query('DELETE FROM session WHERE expire < NOW()');
|
|
||||||
console.log(`Очищено ${result.rowCount} устаревших сессий`);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Ошибка при очистке сессий:', err);
|
|
||||||
}
|
|
||||||
}, cleanupInterval);
|
|
||||||
|
|
||||||
// Запускаем первую очистку через 5 минут после старта сервера
|
|
||||||
setTimeout(async () => {
|
|
||||||
try {
|
|
||||||
const { pool } = require('./db');
|
|
||||||
const result = await pool.query('DELETE FROM session WHERE expire < NOW()');
|
|
||||||
console.log(`Первоначальная очистка: удалено ${result.rowCount} устаревших сессий`);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Ошибка при первоначальной очистке сессий:', err);
|
|
||||||
}
|
|
||||||
}, 5 * 60 * 1000);
|
|
||||||
|
|
||||||
app.get('/session-debug', (req, res) => {
|
|
||||||
// Implementation of the endpoint
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/check-sessions', async (req, res) => {
|
|
||||||
// Implementation of the endpoint
|
|
||||||
});
|
|
||||||
|
|
||||||
// Функция для очистки старых сессий
|
|
||||||
async function cleanupSessions() {
|
|
||||||
try {
|
|
||||||
// Удаляем сессии старше 30 дней
|
|
||||||
const result = await pool.query('DELETE FROM session WHERE expire < NOW() - INTERVAL \'30 days\'');
|
|
||||||
console.log(`Очищено ${result.rowCount} старых сессий`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при очистке старых сессий:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,158 +1,109 @@
|
|||||||
const { ChatOllama } = require('@langchain/ollama');
|
const { ChatOllama } = require('@langchain/ollama');
|
||||||
const { pool } = require('../db');
|
const { HNSWLib } = require('langchain/vectorstores/hnswlib');
|
||||||
|
const { OpenAIEmbeddings } = require('langchain/embeddings/openai');
|
||||||
|
const logger = require('../utils/logger');
|
||||||
|
|
||||||
// Инициализация модели Ollama
|
class AIAssistant {
|
||||||
const model = new ChatOllama({
|
constructor() {
|
||||||
baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
|
this.baseUrl = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
|
||||||
model: process.env.OLLAMA_MODEL || 'llama2',
|
this.defaultModel = process.env.OLLAMA_MODEL || 'mistral';
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Обработка сообщения пользователя и получение ответа от ИИ
|
|
||||||
* @param {number} userId - ID пользователя
|
|
||||||
* @param {string} message - Текст сообщения
|
|
||||||
* @param {string} language - Язык пользователя
|
|
||||||
* @returns {Promise<string>} - Ответ ИИ
|
|
||||||
*/
|
|
||||||
async function processMessage(userId, message, language = 'ru') {
|
|
||||||
try {
|
|
||||||
// Получение информации о пользователе
|
|
||||||
const userInfo = await getUserInfo(userId);
|
|
||||||
|
|
||||||
// Получение истории диалога (последние 10 сообщений)
|
|
||||||
const history = await getConversationHistory(userId);
|
|
||||||
|
|
||||||
// Формирование контекста для ИИ
|
|
||||||
const context = `
|
|
||||||
Пользователь: ${userInfo.username || 'Пользователь'} (ID: ${userId})
|
|
||||||
Язык: ${language}
|
|
||||||
Роль: ${userInfo.is_admin ? 'Администратор' : 'Пользователь'}
|
|
||||||
История диалога:
|
|
||||||
${history}
|
|
||||||
|
|
||||||
Текущее сообщение: ${message}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Временная заглушка для ответа ИИ
|
|
||||||
// В будущем здесь будет интеграция с реальной моделью ИИ
|
|
||||||
const responses = {
|
|
||||||
ru: [
|
|
||||||
'Спасибо за ваше сообщение! Чем я могу помочь?',
|
|
||||||
'Я понимаю ваш запрос. Давайте разберемся с этим вопросом.',
|
|
||||||
'Интересный вопрос! Вот что я могу предложить...',
|
|
||||||
'Я обработал вашу информацию. Есть ли у вас дополнительные вопросы?',
|
|
||||||
'Я готов помочь вам с этим запросом. Нужны ли дополнительные детали?',
|
|
||||||
],
|
|
||||||
en: [
|
|
||||||
'Thank you for your message! How can I help you?',
|
|
||||||
"I understand your request. Let's figure this out.",
|
|
||||||
"Interesting question! Here's what I can suggest...",
|
|
||||||
"I've processed your information. Do you have any additional questions?",
|
|
||||||
"I'm ready to help you with this request. Do you need any additional details?",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const langResponses = responses[language] || responses['ru'];
|
|
||||||
const randomIndex = Math.floor(Math.random() * langResponses.length);
|
|
||||||
|
|
||||||
// Имитация задержки ответа ИИ
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
||||||
|
|
||||||
return langResponses[randomIndex];
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing message:', error);
|
|
||||||
return 'Извините, произошла ошибка при обработке вашего сообщения. Пожалуйста, попробуйте еще раз позже.';
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Создание экземпляра ChatOllama с нужными параметрами
|
||||||
* Получение информации о пользователе
|
createChat(language = 'ru') {
|
||||||
* @param {number} userId - ID пользователя
|
const systemPrompt = language === 'ru'
|
||||||
* @returns {Promise<Object>} - Информация о пользователе
|
? 'Вы - полезный ассистент. Отвечайте на русском языке.'
|
||||||
*/
|
: 'You are a helpful assistant. Respond in English.';
|
||||||
async function getUserInfo(userId) {
|
|
||||||
try {
|
|
||||||
const userResult = await pool.query(
|
|
||||||
`SELECT u.id, u.username, u.address, u.is_admin, u.language, r.name as role
|
|
||||||
FROM users u
|
|
||||||
JOIN roles r ON u.role_id = r.id
|
|
||||||
WHERE u.id = $1`,
|
|
||||||
[userId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (userResult.rows.length === 0) {
|
return new ChatOllama({
|
||||||
return { id: userId };
|
baseUrl: this.baseUrl,
|
||||||
|
model: this.defaultModel,
|
||||||
|
system: systemPrompt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Определение языка сообщения
|
||||||
|
detectLanguage(message) {
|
||||||
|
const cyrillicPattern = /[а-яА-ЯёЁ]/;
|
||||||
|
return cyrillicPattern.test(message) ? 'ru' : 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Основной метод для получения ответа
|
||||||
|
async getResponse(message, language = 'auto') {
|
||||||
|
try {
|
||||||
|
// Определяем язык, если не указан явно
|
||||||
|
const detectedLanguage = language === 'auto'
|
||||||
|
? this.detectLanguage(message)
|
||||||
|
: language;
|
||||||
|
|
||||||
|
const chat = this.createChat(detectedLanguage);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Пробуем получить ответ через ChatOllama
|
||||||
|
const response = await chat.invoke(message);
|
||||||
|
return response.content;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error using ChatOllama:', error);
|
||||||
|
|
||||||
|
// Пробуем альтернативный метод через прямой API
|
||||||
|
return await this.fallbackRequest(message, detectedLanguage);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error in getResponse:', error);
|
||||||
|
return "Извините, я не смог обработать ваш запрос. Пожалуйста, попробуйте позже.";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получение идентификаторов пользователя
|
|
||||||
const identitiesResult = await pool.query(
|
|
||||||
`SELECT identity_type, identity_value, verified
|
|
||||||
FROM user_identities
|
|
||||||
WHERE user_id = $1`,
|
|
||||||
[userId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const user = userResult.rows[0];
|
|
||||||
user.identities = identitiesResult.rows;
|
|
||||||
|
|
||||||
return user;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting user info:', error);
|
|
||||||
return { id: userId };
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Альтернативный метод запроса через прямой API
|
||||||
* Получение истории диалога
|
async fallbackRequest(message, language) {
|
||||||
* @param {number} userId - ID пользователя
|
try {
|
||||||
* @param {number} limit - Максимальное количество сообщений
|
logger.info('Using fallback request method');
|
||||||
* @returns {Promise<string>} - История диалога в текстовом формате
|
|
||||||
*/
|
const systemPrompt = language === 'ru'
|
||||||
async function getConversationHistory(userId, limit = 10) {
|
? 'Вы - полезный ассистент. Отвечайте на русском языке.'
|
||||||
try {
|
: 'You are a helpful assistant. Respond in English.';
|
||||||
// Получение последнего активного диалога пользователя
|
|
||||||
const conversationResult = await pool.query(
|
|
||||||
`SELECT id FROM conversations
|
|
||||||
WHERE user_id = $1
|
|
||||||
ORDER BY updated_at DESC
|
|
||||||
LIMIT 1`,
|
|
||||||
[userId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (conversationResult.rows.length === 0) {
|
const response = await fetch(`${this.baseUrl}/api/generate`, {
|
||||||
return '';
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: this.defaultModel,
|
||||||
|
prompt: message,
|
||||||
|
system: systemPrompt,
|
||||||
|
stream: false
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.response;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error in fallback request:', error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const conversationId = conversationResult.rows[0].id;
|
// Получение списка доступных моделей
|
||||||
|
async getAvailableModels() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.baseUrl}/api/tags`);
|
||||||
|
const data = await response.json();
|
||||||
|
return data.models || [];
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error getting available models:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Получение последних сообщений из диалога
|
// Добавляем методы из vectorStore.js
|
||||||
const messagesResult = await pool.query(
|
async initVectorStore() {
|
||||||
`SELECT sender_type, content, created_at
|
// ... код инициализации ...
|
||||||
FROM messages
|
}
|
||||||
WHERE conversation_id = $1
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT $2`,
|
|
||||||
[conversationId, limit]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Формирование истории в текстовом формате
|
async findSimilarDocuments(query, k = 3) {
|
||||||
const history = messagesResult.rows
|
// ... код поиска документов ...
|
||||||
.reverse()
|
|
||||||
.map((msg) => {
|
|
||||||
const sender = msg.sender_type === 'user' ? 'Пользователь' : 'ИИ';
|
|
||||||
return `${sender}: ${msg.content}`;
|
|
||||||
})
|
|
||||||
.join('\n\n');
|
|
||||||
|
|
||||||
return history;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting conversation history:', error);
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
// Создаем и экспортируем единственный экземпляр
|
||||||
processMessage,
|
const aiAssistant = new AIAssistant();
|
||||||
getUserInfo,
|
module.exports = aiAssistant;
|
||||||
getConversationHistory,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,69 +1,54 @@
|
|||||||
const db = require('../db');
|
const db = require('../db');
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
const { ethers } = require('ethers');
|
const { ethers } = require('ethers');
|
||||||
|
const crypto = require('crypto');
|
||||||
const { processMessage } = require('./ai-assistant'); // Используем AI Assistant
|
const { processMessage } = require('./ai-assistant'); // Используем AI Assistant
|
||||||
|
|
||||||
// В начале файла auth-service.js
|
|
||||||
const getProvider = (network) => {
|
|
||||||
const primaryUrl = process.env[`RPC_URL_${network.toUpperCase()}`];
|
|
||||||
const backupUrls = {
|
|
||||||
eth: 'https://eth-mainnet.public.blastapi.io',
|
|
||||||
polygon: 'https://polygon-rpc.com',
|
|
||||||
bsc: 'https://bsc-dataseed.binance.org',
|
|
||||||
arbitrum: 'https://arb1.arbitrum.io/rpc'
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
return new ethers.JsonRpcProvider(primaryUrl);
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn(`Failed to connect to primary URL for ${network}, using backup`);
|
|
||||||
return new ethers.JsonRpcProvider(backupUrls[network]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const providers = {
|
|
||||||
eth: getProvider('eth'),
|
|
||||||
polygon: getProvider('polygon'),
|
|
||||||
bsc: getProvider('bsc'),
|
|
||||||
arbitrum: getProvider('arbitrum')
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Сервис для работы с аутентификацией и авторизацией
|
|
||||||
*/
|
|
||||||
class AuthService {
|
class AuthService {
|
||||||
/**
|
constructor() {
|
||||||
* Проверяет наличие токенов на кошельке и обновляет роль
|
// Инициализация провайдеров для разных сетей
|
||||||
* @param {string} walletAddress - Адрес кошелька
|
this.providers = {
|
||||||
* @returns {Promise<boolean>} - Имеет ли пользователь права администратора
|
eth: new ethers.JsonRpcProvider(process.env.RPC_URL_ETH),
|
||||||
*/
|
polygon: new ethers.JsonRpcProvider(process.env.RPC_URL_POLYGON),
|
||||||
async checkTokensAndUpdateRole(walletAddress) {
|
bsc: new ethers.JsonRpcProvider(process.env.RPC_URL_BSC),
|
||||||
|
arbitrum: new ethers.JsonRpcProvider(process.env.RPC_URL_ARBITRUM)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Конфигурация токенов для разных сетей
|
||||||
|
this.tokenContracts = [
|
||||||
|
{ address: "0xd95a45fc46a7300e6022885afec3d618d7d3f27c", network: "eth" }, // Ethereum
|
||||||
|
{ address: "0x1d47f12ffA279BFE59Ab16d56fBb10d89AECdD5D", network: "bsc" }, // Binance Smart Chain
|
||||||
|
{ address: "0xdce769b847a0a697239777d0b1c7dd33b6012ba0", network: "arbitrum" }, // Arbitrum
|
||||||
|
{ address: "0x351f59de4fedbdf7601f5592b93db3b9330c1c1d", network: "polygon" } // Polygon
|
||||||
|
];
|
||||||
|
|
||||||
|
this.MIN_BALANCE = ethers.parseUnits("1000000.0", 18); // 1,000,000 токенов для роли админа
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка подписи
|
||||||
|
async verifySignature(message, signature, address) {
|
||||||
try {
|
try {
|
||||||
// Получаем ID пользователя по адресу кошелька
|
logger.info('Verifying signature:', {
|
||||||
const userResult = await db.query(`
|
message: message.substring(0, 100) + '...',
|
||||||
SELECT u.id FROM users u
|
signature: signature.substring(0, 10) + '...',
|
||||||
JOIN user_identities ui ON u.id = ui.user_id
|
address
|
||||||
WHERE ui.identity_type = 'wallet' AND ui.identity_value = $1
|
});
|
||||||
`, [walletAddress]);
|
|
||||||
|
if (!message || !signature || !address) {
|
||||||
if (userResult.rows.length === 0) {
|
logger.error('Missing parameters for signature verification');
|
||||||
logger.warn(`User with wallet ${walletAddress} not found`);
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Восстанавливаем адрес из подписи через ethers
|
||||||
|
const recoveredAddress = ethers.verifyMessage(message, signature);
|
||||||
|
return ethers.getAddress(recoveredAddress) === ethers.getAddress(address);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error in signature verification:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = userResult.rows[0].id;
|
|
||||||
|
|
||||||
// Проверяем наличие токенов на кошельке
|
|
||||||
const isAdmin = await this.checkAdminTokens(walletAddress);
|
|
||||||
|
|
||||||
// Обновляем роль в базе данных
|
|
||||||
await this.updateUserRole(userId, isAdmin ? 'admin' : 'user');
|
|
||||||
|
|
||||||
logger.info(`User ${userId} with address ${walletAddress}: admin=${isAdmin}`);
|
|
||||||
|
|
||||||
return isAdmin;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error checking tokens: ${error.message}`);
|
logger.error('Error in verifySignature:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,80 +60,195 @@ class AuthService {
|
|||||||
*/
|
*/
|
||||||
async checkAdminTokens(walletAddress) {
|
async checkAdminTokens(walletAddress) {
|
||||||
try {
|
try {
|
||||||
const tokenContracts = [
|
for (const contract of this.tokenContracts) {
|
||||||
{ address: "0xd95a45fc46a7300e6022885afec3d618d7d3f27c", network: "eth" }, // Ethereum
|
|
||||||
{ address: "0x1d47f12ffA279BFE59Ab16d56fBb10d89AECdD5D", network: "bsc" }, // Binance Smart Chain
|
|
||||||
{ address: "0xdce769b847a0a697239777d0b1c7dd33b6012ba0", network: "arbitrum" }, // Arbitrum
|
|
||||||
{ address: "0x351f59de4fedbdf7601f5592b93db3b9330c1c1d", network: "polygon" } // Polygon
|
|
||||||
];
|
|
||||||
|
|
||||||
const MIN_BALANCE = ethers.parseUnits("1.0", 18); // 1 токен
|
|
||||||
|
|
||||||
for (const contract of tokenContracts) {
|
|
||||||
try {
|
try {
|
||||||
const provider = providers[contract.network];
|
const provider = this.providers[contract.network];
|
||||||
if (!provider) {
|
const tokenContract = new ethers.Contract(
|
||||||
logger.warn(`Provider not found for network: ${contract.network}`);
|
contract.address,
|
||||||
continue;
|
['function balanceOf(address) view returns (uint256)'],
|
||||||
}
|
provider
|
||||||
|
);
|
||||||
// Проверка доступности провайдера
|
|
||||||
try {
|
|
||||||
await provider.getBlockNumber(); // Простой запрос для проверки соединения
|
|
||||||
} catch (providerError) {
|
|
||||||
logger.warn(`Provider for ${contract.network} is not responding: ${providerError.message}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenContract = new ethers.Contract(contract.address, [
|
|
||||||
"function balanceOf(address owner) view returns (uint256)"
|
|
||||||
], provider);
|
|
||||||
|
|
||||||
const balance = await tokenContract.balanceOf(walletAddress);
|
const balance = await tokenContract.balanceOf(walletAddress);
|
||||||
logger.info(`Balance for ${walletAddress} on ${contract.network}: ${balance.toString()}`);
|
logger.info(`Balance for ${walletAddress} on ${contract.network}: ${balance}`);
|
||||||
|
|
||||||
if (balance >= MIN_BALANCE) {
|
if (balance >= this.MIN_BALANCE) {
|
||||||
logger.info(`Admin token found on ${contract.network} for ${walletAddress}`);
|
logger.info(`Admin token found on ${contract.network} for ${walletAddress}`);
|
||||||
return true; // Если найден хотя бы один токен, возвращаем true
|
return true;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error checking balance on ${contract.network}: ${error.message}`);
|
logger.error(`Error checking balance on ${contract.network}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`No admin tokens found for ${walletAddress}`);
|
logger.info(`No admin tokens found for ${walletAddress}`);
|
||||||
return false; // Если не найдено ни одного токена, возвращаем false
|
return false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error in checkAdminTokens: ${error.message}`);
|
logger.error('Error in checkAdminTokens:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Обновляет роль пользователя в базе данных
|
* Проверяет баланс токенов и обновляет роль пользователя
|
||||||
* @param {number} userId - ID пользователя
|
* @param {string} address - Адрес кошелька
|
||||||
* @param {string} role - Новая роль ('admin' или 'user')
|
* @returns {Promise<boolean>} - Является ли пользователь админом
|
||||||
* @returns {Promise<boolean>} - Успешно ли обновлена роль
|
|
||||||
*/
|
*/
|
||||||
async updateUserRole(userId, role) {
|
async checkTokensAndUpdateRole(address) {
|
||||||
try {
|
try {
|
||||||
// Получаем ID роли
|
const isAdmin = await this.checkAdminTokens(address);
|
||||||
const roleResult = await db.query('SELECT id FROM roles WHERE name = $1', [role]);
|
|
||||||
|
|
||||||
if (roleResult.rows.length === 0) {
|
// Обновляем роль в базе данных
|
||||||
logger.error(`Role ${role} not found`);
|
await this.updateUserRole(address, isAdmin);
|
||||||
return false;
|
|
||||||
|
logger.info(`Updated role for user with address ${address}: admin=${isAdmin}`);
|
||||||
|
return isAdmin;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error in checkTokensAndUpdateRole:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Находит или создает пользователя по адресу кошелька
|
||||||
|
* @param {string} address - Адрес кошелька
|
||||||
|
* @returns {Promise<{userId: number, isAdmin: boolean}>}
|
||||||
|
*/
|
||||||
|
async findOrCreateUser(address) {
|
||||||
|
try {
|
||||||
|
const existingUser = await db.query(
|
||||||
|
`SELECT u.id,
|
||||||
|
(u.role = 'admin') as is_admin
|
||||||
|
FROM users u
|
||||||
|
JOIN user_identities ui ON u.id = ui.user_id
|
||||||
|
WHERE ui.provider = 'wallet'
|
||||||
|
AND LOWER(ui.provider_id) = LOWER($1)`,
|
||||||
|
[address]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingUser.rows.length > 0) {
|
||||||
|
return existingUser.rows[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleId = roleResult.rows[0].id;
|
// Если пользователь не найден, создаем нового
|
||||||
|
const result = await db.query(
|
||||||
// Обновляем роль пользователя
|
'INSERT INTO users DEFAULT VALUES RETURNING id',
|
||||||
await db.query('UPDATE users SET role_id = $1 WHERE id = $2', [roleId, userId]);
|
[]
|
||||||
|
);
|
||||||
logger.info(`Updated role for user ${userId} to ${role}`);
|
const userId = result.rows[0].id;
|
||||||
|
|
||||||
|
// Добавляем wallet identity
|
||||||
|
await db.query(
|
||||||
|
`INSERT INTO user_identities
|
||||||
|
(user_id, provider, provider_id, identity_type, identity_value)
|
||||||
|
VALUES ($1, 'wallet', $2, 'wallet', $2)`,
|
||||||
|
[userId, address.toLowerCase()]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Проверяем роль админа
|
||||||
|
const isAdmin = await this.checkAdminRole(userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId,
|
||||||
|
isAdmin
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in findOrCreateUser:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновляет роль пользователя и связанные данные
|
||||||
|
*/
|
||||||
|
async updateUserRole(address, isAdmin) {
|
||||||
|
try {
|
||||||
|
const result = await db.query(`
|
||||||
|
UPDATE users u
|
||||||
|
SET
|
||||||
|
role = $2::user_role
|
||||||
|
FROM user_identities ui
|
||||||
|
WHERE u.id = ui.user_id
|
||||||
|
AND ui.provider = 'wallet'
|
||||||
|
AND LOWER(ui.provider_id) = LOWER($1)
|
||||||
|
RETURNING u.id
|
||||||
|
`, [
|
||||||
|
address,
|
||||||
|
isAdmin ? 'admin' : 'user'
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (result.rows.length > 0) {
|
||||||
|
logger.info(`Updated role for user ${result.rows[0].id} to ${isAdmin ? 'admin' : 'user'}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error updating user role:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Связывание идентификаторов (из identity-linker.js)
|
||||||
|
async linkIdentity(userId, type, value) {
|
||||||
|
try {
|
||||||
|
// Проверяем, не связан ли идентификатор с другим пользователем
|
||||||
|
const existingResult = await db.query(
|
||||||
|
`SELECT user_id
|
||||||
|
FROM user_identities
|
||||||
|
WHERE identity_type = $1
|
||||||
|
AND LOWER(identity_value) = LOWER($2)`,
|
||||||
|
[type, value]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingResult.rows.length > 0 && existingResult.rows[0].user_id !== userId) {
|
||||||
|
throw new Error('Identity already linked to another user');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем или обновляем идентификатор
|
||||||
|
await db.query(
|
||||||
|
`INSERT INTO user_identities
|
||||||
|
(user_id, identity_type, identity_value, verified, created_at)
|
||||||
|
VALUES ($1, $2, $3, true, NOW())
|
||||||
|
ON CONFLICT (identity_type, identity_value)
|
||||||
|
DO UPDATE SET user_id = $1, verified = true`,
|
||||||
|
[userId, type, value]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Если это кошелек, проверяем права админа
|
||||||
|
if (type === 'wallet') {
|
||||||
|
await this.checkTokensAndUpdateRole(value);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error updating user role: ${error.message}`);
|
logger.error('Error linking identity:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение всех идентификаторов пользователя
|
||||||
|
async getUserIdentities(userId) {
|
||||||
|
try {
|
||||||
|
const result = await db.query(
|
||||||
|
`SELECT identity_type, identity_value, verified, created_at
|
||||||
|
FROM user_identities
|
||||||
|
WHERE user_id = $1`,
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
return result.rows;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error getting user identities:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка роли админа
|
||||||
|
async isAdmin(userId) {
|
||||||
|
try {
|
||||||
|
const result = await db.query(
|
||||||
|
'SELECT is_admin FROM users WHERE id = $1',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
return result.rows.length > 0 && result.rows[0].is_admin;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error checking admin status:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,46 +308,6 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Получает все идентификаторы пользователя
|
|
||||||
* @param {number} userId - ID пользователя
|
|
||||||
* @returns {Promise<Array>} - Список идентификаторов
|
|
||||||
*/
|
|
||||||
async getAllUserIdentities(userId) {
|
|
||||||
try {
|
|
||||||
const result = await db.query(`
|
|
||||||
SELECT identity_type, identity_value, verified, created_at
|
|
||||||
FROM user_identities
|
|
||||||
WHERE user_id = $1
|
|
||||||
`, [userId]);
|
|
||||||
|
|
||||||
return result.rows;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error getting user identities: ${error.message}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Проверяет, является ли пользователь администратором
|
|
||||||
* @param {number} userId - ID пользователя
|
|
||||||
* @returns {Promise<boolean>} - Является ли пользователь администратором
|
|
||||||
*/
|
|
||||||
async isAdmin(userId) {
|
|
||||||
try {
|
|
||||||
const result = await db.query('SELECT is_admin FROM users WHERE id = $1', [userId]);
|
|
||||||
|
|
||||||
if (result.rows.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.rows[0].is_admin;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error checking admin status: ${error.message}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Обрабатывает гостевые сообщения после аутентификации
|
* Обрабатывает гостевые сообщения после аутентификации
|
||||||
*/
|
*/
|
||||||
@@ -349,6 +409,95 @@ class AuthService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createSession(req, userData) {
|
||||||
|
// Сохраняем существующие данные сессии
|
||||||
|
const existingData = { ...req.session };
|
||||||
|
|
||||||
|
req.session.userId = userData.userId;
|
||||||
|
req.session.address = userData.address;
|
||||||
|
req.session.isAdmin = userData.isAdmin;
|
||||||
|
req.session.authenticated = true;
|
||||||
|
req.session.authType = userData.authType;
|
||||||
|
|
||||||
|
// Если есть гостевые сообщения в существующей сессии
|
||||||
|
if (existingData.guestId) {
|
||||||
|
req.session.guestId = existingData.guestId;
|
||||||
|
// Связываем сообщения сразу здесь
|
||||||
|
await this.linkGuestMessages(req, {
|
||||||
|
userId: userData.userId,
|
||||||
|
guestId: existingData.guestId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
req.session.save((err) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async linkGuestMessages(req, userData) {
|
||||||
|
if (!userData.guestId) return;
|
||||||
|
|
||||||
|
const { rows } = await db.query(
|
||||||
|
'SELECT EXISTS(SELECT 1 FROM guest_messages WHERE guest_id = $1)',
|
||||||
|
[userData.guestId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rows[0].exists) {
|
||||||
|
// Сначала связываем сообщения
|
||||||
|
await db.query('SELECT link_guest_messages($1, $2)',
|
||||||
|
[userData.userId, userData.guestId]
|
||||||
|
);
|
||||||
|
// Только после успешного связывания удаляем guestId
|
||||||
|
delete req.session.guestId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkAdminRole(userId) {
|
||||||
|
try {
|
||||||
|
// Получаем все идентификаторы пользователя
|
||||||
|
const identities = await db.query(
|
||||||
|
`SELECT provider, provider_id
|
||||||
|
FROM user_identities
|
||||||
|
WHERE user_id = $1`,
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ищем wallet среди идентификаторов
|
||||||
|
const wallet = identities.rows.find(i => i.provider === 'wallet');
|
||||||
|
if (!wallet) return false;
|
||||||
|
|
||||||
|
// Проверяем баланс токенов
|
||||||
|
const hasTokens = await this.checkAdminTokens(wallet.provider_id);
|
||||||
|
if (!hasTokens) return false;
|
||||||
|
|
||||||
|
// Обновляем роль пользователя
|
||||||
|
await db.query(
|
||||||
|
`UPDATE users SET role = 'admin' WHERE id = $1`,
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking admin role:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка при каждой аутентификации
|
||||||
|
async verifyIdentity(type, value) {
|
||||||
|
const userId = await this.getUserIdByIdentity(type, value);
|
||||||
|
if (!userId) return false;
|
||||||
|
|
||||||
|
// Проверяем роль только если есть связанный кошелек
|
||||||
|
await this.checkAdminRole(userId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new AuthService();
|
// Создаем и экспортируем единственный экземпляр
|
||||||
|
const authService = new AuthService();
|
||||||
|
module.exports = authService;
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
const { ChatOllama } = require('@langchain/ollama');
|
|
||||||
const { RetrievalQAChain } = require('langchain/chains');
|
|
||||||
const { PromptTemplate } = require('@langchain/core/prompts');
|
|
||||||
const axios = require('axios');
|
|
||||||
const { Ollama } = require('ollama');
|
|
||||||
const { HumanMessage } = require('@langchain/core/messages');
|
|
||||||
|
|
||||||
// Создаем шаблон для контекстного запроса
|
|
||||||
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
|
|
||||||
async function directOllamaQuery(message, language = 'en') {
|
|
||||||
try {
|
|
||||||
// Всегда используем модель mistral, независимо от языка
|
|
||||||
const modelName = 'mistral';
|
|
||||||
|
|
||||||
console.log(`Отправка запроса к Ollama (модель: ${modelName}, язык: ${language}): ${message}`);
|
|
||||||
|
|
||||||
// Проверяем доступность Ollama
|
|
||||||
console.log('Проверка доступности Ollama...');
|
|
||||||
const ollama = new Ollama();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const models = await ollama.list();
|
|
||||||
console.log('Ollama доступен. Доступные модели:');
|
|
||||||
models.models.forEach((model) => {
|
|
||||||
console.log(`- ${model.name}`);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при проверке доступности Ollama:', error);
|
|
||||||
throw new Error('Ollama недоступен');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Отправка запроса к Ollama...');
|
|
||||||
|
|
||||||
const chatModel = new ChatOllama({
|
|
||||||
baseUrl: 'http://localhost:11434',
|
|
||||||
model: modelName,
|
|
||||||
temperature: 0.7,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await chatModel.invoke([new HumanMessage(message)]);
|
|
||||||
|
|
||||||
return response.content;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при запросе к Ollama:', error);
|
|
||||||
|
|
||||||
// Возвращаем сообщение об ошибке
|
|
||||||
return 'Извините, произошла ошибка при обработке вашего запроса. Пожалуйста, попробуйте позже.';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Функция для создания цепочки 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 };
|
|
||||||
@@ -1,218 +1,143 @@
|
|||||||
const TelegramBot = require('node-telegram-bot-api');
|
const TelegramBot = require('node-telegram-bot-api');
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
const { pool } = require('../db');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
|
|
||||||
// Создаем бота
|
class TelegramBotService {
|
||||||
const token = process.env.TELEGRAM_BOT_TOKEN;
|
constructor(token) {
|
||||||
let bot = null;
|
this.bot = new TelegramBot(token, {
|
||||||
|
polling: true,
|
||||||
/**
|
|
||||||
* Функция для отправки кода подтверждения
|
|
||||||
*/
|
|
||||||
async function sendVerificationCode(chatId) {
|
|
||||||
try {
|
|
||||||
// Генерируем код и токен
|
|
||||||
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
|
||||||
const authToken = crypto.randomBytes(32).toString('hex');
|
|
||||||
|
|
||||||
// Создаем пользователя и сохраняем код в базу данных
|
|
||||||
const result = await pool.query(
|
|
||||||
`WITH new_user AS (
|
|
||||||
INSERT INTO users (created_at)
|
|
||||||
VALUES (NOW())
|
|
||||||
RETURNING id
|
|
||||||
)
|
|
||||||
INSERT INTO telegram_auth_tokens
|
|
||||||
(user_id, token, verification_code, telegram_id, expires_at)
|
|
||||||
VALUES (
|
|
||||||
(SELECT id FROM new_user),
|
|
||||||
$1, $2, $3,
|
|
||||||
NOW() + INTERVAL '5 minutes'
|
|
||||||
)
|
|
||||||
RETURNING user_id`,
|
|
||||||
[authToken, code, chatId.toString()]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Отправляем код с инлайн-кнопкой
|
|
||||||
const sentMessage = await bot.sendMessage(chatId,
|
|
||||||
'Привет! Я бот для аутентификации в DApp for Business.\n\n' +
|
|
||||||
'🔐 Ваш код подтверждения:\n\n' +
|
|
||||||
`<code>${code}</code>\n\n` +
|
|
||||||
'Введите этот код на сайте для завершения авторизации.\n' +
|
|
||||||
'Код действителен в течение 5 минут.',
|
|
||||||
{
|
|
||||||
parse_mode: 'HTML',
|
|
||||||
reply_markup: {
|
|
||||||
inline_keyboard: [
|
|
||||||
[{ text: '🔄 Получить новый код', callback_data: 'new_code' }]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Удаляем сообщение через 30 секунд
|
|
||||||
setTimeout(async () => {
|
|
||||||
try {
|
|
||||||
await bot.deleteMessage(chatId, sentMessage.message_id);
|
|
||||||
await bot.sendMessage(chatId,
|
|
||||||
'Для получения нового кода используйте команду /start или меню команд',
|
|
||||||
{
|
|
||||||
reply_markup: {
|
|
||||||
keyboard: [
|
|
||||||
[{ text: '/start' }]
|
|
||||||
],
|
|
||||||
resize_keyboard: true,
|
|
||||||
persistent: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting message:', error);
|
|
||||||
}
|
|
||||||
}, 30000);
|
|
||||||
|
|
||||||
return { code, token: authToken, userId: result.rows[0].user_id };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error sending verification code:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Функция для проверки кода
|
|
||||||
*/
|
|
||||||
async function verifyCode(code) {
|
|
||||||
try {
|
|
||||||
const result = await pool.query(
|
|
||||||
`SELECT token, telegram_id, user_id
|
|
||||||
FROM telegram_auth_tokens
|
|
||||||
WHERE verification_code = $1
|
|
||||||
AND expires_at > NOW()
|
|
||||||
AND NOT used`,
|
|
||||||
[code]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.rows.length === 0) {
|
|
||||||
return { success: false, error: 'Неверный или истекший код' };
|
|
||||||
}
|
|
||||||
|
|
||||||
const { token, telegram_id, user_id } = result.rows[0];
|
|
||||||
|
|
||||||
// Помечаем токен как использованный
|
|
||||||
await pool.query(
|
|
||||||
'UPDATE telegram_auth_tokens SET used = true WHERE token = $1',
|
|
||||||
[token]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Добавляем Telegram ID в таблицу идентификаторов
|
|
||||||
await pool.query(
|
|
||||||
`INSERT INTO user_identities
|
|
||||||
(user_id, identity_type, identity_value, verified, created_at)
|
|
||||||
VALUES ($1, 'telegram', $2, true, NOW())
|
|
||||||
ON CONFLICT (identity_type, identity_value)
|
|
||||||
DO UPDATE SET verified = true`,
|
|
||||||
[user_id, telegram_id]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
telegramId: telegram_id,
|
|
||||||
userId: user_id
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error verifying code:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Инициализация Telegram бота
|
|
||||||
*/
|
|
||||||
function initTelegramBot() {
|
|
||||||
if (!token) {
|
|
||||||
console.warn('TELEGRAM_BOT_TOKEN not set');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Создаем бота с опцией обработки ошибок
|
|
||||||
bot = new TelegramBot(token, {
|
|
||||||
polling: {
|
|
||||||
autoStart: true,
|
|
||||||
params: {
|
|
||||||
timeout: 10
|
|
||||||
}
|
|
||||||
},
|
|
||||||
request: {
|
request: {
|
||||||
timeout: 30000, // увеличиваем таймаут до 30 секунд
|
timeout: 30000 // 30 секунд таймаут
|
||||||
proxy: process.env.HTTPS_PROXY // используем прокси если есть
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.verificationCodes = new Map();
|
||||||
|
this.setupHandlers();
|
||||||
|
|
||||||
|
logger.info('TelegramBot service initialized');
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Telegram bot initialized');
|
setupHandlers() {
|
||||||
|
this.bot.on('message', this.handleMessage.bind(this));
|
||||||
// Очищаем все предыдущие обработчики
|
this.bot.on('callback_query', this.handleCallbackQuery.bind(this));
|
||||||
bot.removeAllListeners();
|
|
||||||
|
// Обработка ошибок
|
||||||
// Устанавливаем команды бота с обработкой ошибок
|
this.bot.on('polling_error', (error) => {
|
||||||
bot.setMyCommands([
|
logger.error('Telegram polling error:', error);
|
||||||
{ command: '/start', description: 'Получить код подтверждения' },
|
|
||||||
{ command: '/help', description: 'Показать справку' }
|
|
||||||
]).catch(error => {
|
|
||||||
console.warn('Error setting bot commands:', error);
|
|
||||||
// Продолжаем работу даже если не удалось установить команды
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.bot.on('error', (error) => {
|
||||||
|
logger.error('Telegram bot error:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Обработчик команды /start
|
async handleMessage(msg) {
|
||||||
bot.onText(/\/start/, async (msg) => {
|
try {
|
||||||
const chatId = msg.chat.id;
|
const chatId = msg.chat.id;
|
||||||
try {
|
const text = msg.text;
|
||||||
await sendVerificationCode(chatId);
|
|
||||||
} catch (error) {
|
logger.info(`Received message from ${chatId}: ${text}`);
|
||||||
console.error('Error handling /start:', error);
|
|
||||||
await bot.sendMessage(chatId, 'Произошла ошибка. Пожалуйста, попробуйте позже.')
|
if (text.startsWith('/start')) {
|
||||||
.catch(err => console.error('Error sending error message:', err));
|
await this.handleStart(msg);
|
||||||
|
} else if (this.verificationCodes.has(chatId)) {
|
||||||
|
await this.handleVerificationCode(msg);
|
||||||
}
|
}
|
||||||
});
|
} catch (error) {
|
||||||
|
logger.error('Error handling message:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Обработчик ошибок polling
|
async handleCallbackQuery(query) {
|
||||||
bot.on('polling_error', (error) => {
|
try {
|
||||||
console.error('Telegram bot polling error:', error);
|
const chatId = query.message.chat.id;
|
||||||
// Перезапускаем polling при ошибке
|
await this.bot.answerCallbackQuery(query.id);
|
||||||
setTimeout(() => {
|
|
||||||
try {
|
logger.info(`Handled callback query from ${chatId}`);
|
||||||
bot.startPolling();
|
} catch (error) {
|
||||||
} catch (e) {
|
logger.error('Error handling callback query:', error);
|
||||||
console.error('Error restarting polling:', e);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleStart(msg) {
|
||||||
|
const chatId = msg.chat.id;
|
||||||
|
try {
|
||||||
|
await this.bot.sendMessage(
|
||||||
|
chatId,
|
||||||
|
'Добро пожаловать! Используйте этого бота для аутентификации в приложении.'
|
||||||
|
);
|
||||||
|
logger.info(`Sent welcome message to ${chatId}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error sending welcome message to ${chatId}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleVerificationCode(msg) {
|
||||||
|
const chatId = msg.chat.id;
|
||||||
|
const code = msg.text.trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const verificationData = this.verificationCodes.get(chatId);
|
||||||
|
|
||||||
|
if (!verificationData) {
|
||||||
|
await this.bot.sendMessage(chatId, 'Нет активного кода подтверждения.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Date.now() > verificationData.expires) {
|
||||||
|
this.verificationCodes.delete(chatId);
|
||||||
|
await this.bot.sendMessage(chatId, 'Код подтверждения истек. Запросите новый.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verificationData.code === code) {
|
||||||
|
await this.bot.sendMessage(chatId, 'Код подтвержден успешно!');
|
||||||
|
this.verificationCodes.delete(chatId);
|
||||||
|
} else {
|
||||||
|
await this.bot.sendMessage(chatId, 'Неверный код. Попробуйте еще раз.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error handling verification code for ${chatId}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendVerificationCode(chatId, code) {
|
||||||
|
try {
|
||||||
|
// Сохраняем код с временем истечения (15 минут)
|
||||||
|
this.verificationCodes.set(chatId, {
|
||||||
|
code,
|
||||||
|
expires: Date.now() + 15 * 60 * 1000
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.bot.sendMessage(
|
||||||
|
chatId,
|
||||||
|
`Ваш код подтверждения: ${code}\nВведите его в приложении.`
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`Sent verification code to ${chatId}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error sending verification code to ${chatId}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyCode(code) {
|
||||||
|
try {
|
||||||
|
for (const [chatId, data] of this.verificationCodes.entries()) {
|
||||||
|
if (data.code === code) {
|
||||||
|
if (Date.now() > data.expires) {
|
||||||
|
this.verificationCodes.delete(chatId);
|
||||||
|
return { success: false, error: 'Код истек' };
|
||||||
|
}
|
||||||
|
this.verificationCodes.delete(chatId);
|
||||||
|
return { success: true, telegramId: chatId.toString() };
|
||||||
}
|
}
|
||||||
}, 10000); // пробуем перезапустить через 10 секунд
|
}
|
||||||
});
|
return { success: false, error: 'Неверный код' };
|
||||||
|
} catch (error) {
|
||||||
// Обработчик остановки polling
|
logger.error('Error verifying code:', error);
|
||||||
bot.on('stop', () => {
|
return { success: false, error: 'Внутренняя ошибка' };
|
||||||
console.log('Bot polling stopped');
|
}
|
||||||
// Пробуем перезапустить
|
|
||||||
setTimeout(() => {
|
|
||||||
try {
|
|
||||||
bot.startPolling();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error restarting polling after stop:', e);
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
|
|
||||||
return bot;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error initializing Telegram bot:', error);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Экспортируем функции
|
module.exports = TelegramBotService;
|
||||||
module.exports = {
|
|
||||||
initTelegramBot,
|
|
||||||
verifyCode,
|
|
||||||
sendVerificationCode
|
|
||||||
};
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
const { HNSWLib } = require('langchain/vectorstores/hnswlib');
|
|
||||||
const { OllamaEmbeddings } = require('langchain/embeddings/ollama');
|
|
||||||
const { RecursiveCharacterTextSplitter } = require('langchain/text_splitter');
|
|
||||||
const { DirectoryLoader } = require('langchain/document_loaders/fs/directory');
|
|
||||||
const { TextLoader } = require('langchain/document_loaders/fs/text');
|
|
||||||
const { PDFLoader } = require('langchain/document_loaders/fs/pdf');
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
// Путь к директории для хранения векторной базы данных
|
|
||||||
const VECTOR_STORE_PATH = path.join(__dirname, '../data/vector_store');
|
|
||||||
|
|
||||||
// Инициализация embeddings с использованием локальной модели Ollama
|
|
||||||
const embeddings = new OllamaEmbeddings({
|
|
||||||
model: process.env.OLLAMA_EMBEDDINGS_MODEL || 'mistral',
|
|
||||||
baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
|
|
||||||
});
|
|
||||||
|
|
||||||
let vectorStore = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Инициализация векторного хранилища
|
|
||||||
*/
|
|
||||||
async function initializeVectorStore() {
|
|
||||||
try {
|
|
||||||
// Создание директории, если она не существует
|
|
||||||
if (!fs.existsSync(VECTOR_STORE_PATH)) {
|
|
||||||
fs.mkdirSync(VECTOR_STORE_PATH, { recursive: true });
|
|
||||||
console.log(`Created vector store directory at ${VECTOR_STORE_PATH}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверка наличия файлов индекса
|
|
||||||
const indexFiles = fs.readdirSync(VECTOR_STORE_PATH);
|
|
||||||
|
|
||||||
if (indexFiles.length > 0 && indexFiles.includes('hnswlib.index')) {
|
|
||||||
// Загрузка существующего индекса
|
|
||||||
console.log('Loading existing vector store...');
|
|
||||||
try {
|
|
||||||
vectorStore = await HNSWLib.load(VECTOR_STORE_PATH, embeddings);
|
|
||||||
console.log('Vector store loaded successfully');
|
|
||||||
} catch (loadError) {
|
|
||||||
console.error('Error loading existing vector store:', loadError);
|
|
||||||
console.log('Creating new vector store...');
|
|
||||||
await createVectorStore();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Создание нового индекса
|
|
||||||
console.log('Creating new vector store...');
|
|
||||||
await createVectorStore();
|
|
||||||
}
|
|
||||||
|
|
||||||
return vectorStore;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error initializing vector store:', error);
|
|
||||||
// Создаем пустой векторный индекс в случае ошибки
|
|
||||||
vectorStore = new HNSWLib(embeddings, {
|
|
||||||
space: 'cosine',
|
|
||||||
numDimensions: 4096, // Размерность для Ollama embeddings (зависит от модели)
|
|
||||||
});
|
|
||||||
await vectorStore.save(VECTOR_STORE_PATH);
|
|
||||||
return vectorStore;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Создание нового векторного хранилища из документов
|
|
||||||
*/
|
|
||||||
async function createVectorStore() {
|
|
||||||
try {
|
|
||||||
// Проверяем наличие директории documents
|
|
||||||
const docsPath = path.join(__dirname, '../data/documents');
|
|
||||||
|
|
||||||
// Если директория documents не существует, проверяем директорию docs
|
|
||||||
if (!fs.existsSync(docsPath)) {
|
|
||||||
const altDocsPath = path.join(__dirname, '../data/docs');
|
|
||||||
|
|
||||||
// Если директория docs существует, используем ее
|
|
||||||
if (fs.existsSync(altDocsPath)) {
|
|
||||||
console.log(`Using documents directory at ${altDocsPath}`);
|
|
||||||
return await processDocumentsDirectory(altDocsPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Иначе создаем директорию documents
|
|
||||||
fs.mkdirSync(docsPath, { recursive: true });
|
|
||||||
console.log(`Created documents directory at ${docsPath}`);
|
|
||||||
|
|
||||||
// Создание примера документа
|
|
||||||
const sampleDocPath = path.join(docsPath, 'sample.txt');
|
|
||||||
fs.writeFileSync(sampleDocPath, 'Это пример документа для векторного хранилища.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return await processDocumentsDirectory(docsPath);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error creating vector store:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Обработка директории с документами
|
|
||||||
* @param {string} docsPath - Путь к директории с документами
|
|
||||||
*/
|
|
||||||
async function processDocumentsDirectory(docsPath) {
|
|
||||||
try {
|
|
||||||
// Загрузка документов
|
|
||||||
const loader = new DirectoryLoader(docsPath, {
|
|
||||||
'.txt': (path) => new TextLoader(path),
|
|
||||||
'.pdf': (path) => new PDFLoader(path),
|
|
||||||
});
|
|
||||||
|
|
||||||
const docs = await loader.load();
|
|
||||||
console.log(`Loaded ${docs.length} documents`);
|
|
||||||
|
|
||||||
if (docs.length === 0) {
|
|
||||||
// Создаем пустой векторный индекс, если нет документов
|
|
||||||
vectorStore = new HNSWLib(embeddings, {
|
|
||||||
space: 'cosine',
|
|
||||||
numDimensions: 4096, // Размерность для Ollama embeddings (зависит от модели)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Разделение документов на чанки
|
|
||||||
const textSplitter = new RecursiveCharacterTextSplitter({
|
|
||||||
chunkSize: 1000,
|
|
||||||
chunkOverlap: 200,
|
|
||||||
});
|
|
||||||
|
|
||||||
const splitDocs = await textSplitter.splitDocuments(docs);
|
|
||||||
console.log(`Split into ${splitDocs.length} chunks`);
|
|
||||||
|
|
||||||
// Создание векторного хранилища
|
|
||||||
vectorStore = await HNSWLib.fromDocuments(splitDocs, embeddings);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сохранение векторного хранилища
|
|
||||||
await vectorStore.save(VECTOR_STORE_PATH);
|
|
||||||
console.log('Vector store created and saved successfully');
|
|
||||||
|
|
||||||
return vectorStore;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing documents directory:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Получение векторного хранилища
|
|
||||||
* @returns {HNSWLib|null} Векторное хранилище
|
|
||||||
*/
|
|
||||||
function getVectorStore() {
|
|
||||||
return vectorStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Поиск похожих документов
|
|
||||||
* @param {string} query - Запрос для поиска
|
|
||||||
* @param {number} k - Количество результатов
|
|
||||||
* @returns {Promise<Array>} - Массив похожих документов
|
|
||||||
*/
|
|
||||||
async function similaritySearch(query, k = 5) {
|
|
||||||
if (!vectorStore) {
|
|
||||||
await initializeVectorStore();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const results = await vectorStore.similaritySearch(query, k);
|
|
||||||
return results;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error performing similarity search:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Добавление нового документа в векторное хранилище
|
|
||||||
* @param {string} text - Текст документа
|
|
||||||
* @param {Object} metadata - Метаданные документа
|
|
||||||
* @returns {Promise<boolean>} - Успешность добавления
|
|
||||||
*/
|
|
||||||
async function addDocument(text, metadata = {}) {
|
|
||||||
if (!vectorStore) {
|
|
||||||
await initializeVectorStore();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Разделение документа на чанки
|
|
||||||
const textSplitter = new RecursiveCharacterTextSplitter({
|
|
||||||
chunkSize: 1000,
|
|
||||||
chunkOverlap: 200,
|
|
||||||
});
|
|
||||||
|
|
||||||
const docs = await textSplitter.createDocuments([text], [metadata]);
|
|
||||||
|
|
||||||
// Добавление документов в векторное хранилище
|
|
||||||
await vectorStore.addDocuments(docs);
|
|
||||||
|
|
||||||
// Сохранение обновленного векторного хранилища
|
|
||||||
await vectorStore.save(VECTOR_STORE_PATH);
|
|
||||||
|
|
||||||
console.log('Document added to vector store successfully');
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error adding document to vector store:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
initializeVectorStore,
|
|
||||||
getVectorStore,
|
|
||||||
similaritySearch,
|
|
||||||
addDocument,
|
|
||||||
};
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
const db = require('../db');
|
|
||||||
const logger = require('../utils/logger');
|
|
||||||
const authService = require('../services/auth-service');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Проверяет токены всех пользователей и обновляет их роли
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function checkAllUsersTokens() {
|
|
||||||
try {
|
|
||||||
// Получаем всех пользователей с кошельками
|
|
||||||
const walletUsers = await db.query(`
|
|
||||||
SELECT u.id, ui.identity_value as address
|
|
||||||
FROM users u
|
|
||||||
JOIN user_identities ui ON u.id = ui.user_id
|
|
||||||
WHERE ui.identity_type = 'wallet'
|
|
||||||
`);
|
|
||||||
|
|
||||||
logger.info(`Checking tokens for ${walletUsers.rows.length} users`);
|
|
||||||
|
|
||||||
for (const user of walletUsers.rows) {
|
|
||||||
try {
|
|
||||||
// Используем существующий метод для проверки токенов и обновления роли
|
|
||||||
const isAdmin = await authService.checkTokensAndUpdateRole(user.address);
|
|
||||||
logger.info(`Updated user ${user.id} with address ${user.address}: admin=${isAdmin}`);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error checking tokens for user ${user.id}: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('Token check completed');
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error checking all users tokens: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
checkAllUsersTokens
|
|
||||||
};
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
const { ethers } = require('ethers');
|
|
||||||
const db = require('../db');
|
|
||||||
const logger = require('./logger');
|
|
||||||
const authService = require('../services/auth-service');
|
|
||||||
const { USER_ROLES, IDENTITY_TYPES } = require('./constants');
|
|
||||||
|
|
||||||
// Инициализация провайдера
|
|
||||||
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL_ETH);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Проверяет подпись сообщения
|
|
||||||
* @param {string} nonce - Nonce для проверки
|
|
||||||
* @param {string} signature - Подпись
|
|
||||||
* @param {string} address - Адрес кошелька
|
|
||||||
* @returns {Promise<boolean>} - Результат проверки
|
|
||||||
*/
|
|
||||||
async function verifySignature(nonce, signature, address) {
|
|
||||||
try {
|
|
||||||
// Создаем сообщение для проверки
|
|
||||||
const message = `Подпишите это сообщение для аутентификации в DApp for Business. Nonce: ${nonce}`;
|
|
||||||
|
|
||||||
// Восстанавливаем адрес из подписи
|
|
||||||
const recoveredAddress = ethers.verifyMessage(message, signature);
|
|
||||||
|
|
||||||
// Сравниваем адреса (приводим к нижнему регистру для надежности)
|
|
||||||
return recoveredAddress.toLowerCase() === address.toLowerCase();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error verifying signature:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Проверяет, является ли пользователь администратором
|
|
||||||
* @param {string} address - Адрес кошелька
|
|
||||||
* @returns {Promise<boolean>} - Является ли пользователь администратором
|
|
||||||
*/
|
|
||||||
async function checkUserRole(address) {
|
|
||||||
try {
|
|
||||||
// Проверяем наличие токенов администратора
|
|
||||||
const isAdmin = await authService.checkAdminTokens(address);
|
|
||||||
return isAdmin;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking user role:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Проверяет доступ пользователя
|
|
||||||
* @param {string} walletAddress - Адрес кошелька
|
|
||||||
* @returns {Promise<Object>} - Информация о доступе
|
|
||||||
*/
|
|
||||||
async function checkAccess(walletAddress) {
|
|
||||||
try {
|
|
||||||
// Проверяем наличие токенов администратора
|
|
||||||
const isAdmin = await authService.checkAdminTokens(walletAddress);
|
|
||||||
|
|
||||||
// Получаем или создаем пользователя
|
|
||||||
const userId = await findOrCreateUser(walletAddress);
|
|
||||||
|
|
||||||
return {
|
|
||||||
userId,
|
|
||||||
isAdmin,
|
|
||||||
hasAccess: true
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error checking access: ${error.message}`);
|
|
||||||
return {
|
|
||||||
hasAccess: false,
|
|
||||||
error: error.message
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Находит или создает пользователя по адресу кошелька
|
|
||||||
* @param {string} address - Адрес кошелька
|
|
||||||
* @returns {Promise<Object>} - ID пользователя и роль
|
|
||||||
*/
|
|
||||||
async function findOrCreateUser(address) {
|
|
||||||
try {
|
|
||||||
if (!address) {
|
|
||||||
throw new Error('Address is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedAddress = address.toLowerCase();
|
|
||||||
|
|
||||||
// Сначала проверяем в таблице users
|
|
||||||
const userResult = await db.query(
|
|
||||||
'SELECT id FROM users WHERE LOWER(address) = $1',
|
|
||||||
[normalizedAddress]
|
|
||||||
);
|
|
||||||
|
|
||||||
let userId;
|
|
||||||
let isAdmin = false;
|
|
||||||
|
|
||||||
if (userResult.rows.length === 0) {
|
|
||||||
// Если пользователь не найден, создаем его
|
|
||||||
const roleResult = await db.query('SELECT id FROM roles WHERE name = $1', ['user']);
|
|
||||||
if (roleResult.rows.length === 0) {
|
|
||||||
throw new Error('Role "user" not found');
|
|
||||||
}
|
|
||||||
const roleId = roleResult.rows[0].id;
|
|
||||||
|
|
||||||
// Создаем пользователя
|
|
||||||
const newUserResult = await db.query(
|
|
||||||
'INSERT INTO users (address, role_id, created_at) VALUES ($1, $2, NOW()) RETURNING id',
|
|
||||||
[normalizedAddress, roleId]
|
|
||||||
);
|
|
||||||
|
|
||||||
userId = newUserResult.rows[0].id;
|
|
||||||
|
|
||||||
// Добавляем идентификатор кошелька
|
|
||||||
await db.query(
|
|
||||||
'INSERT INTO user_identities (user_id, identity_type, identity_value, created_at) VALUES ($1, $2, $3, NOW())',
|
|
||||||
[userId, 'wallet', normalizedAddress]
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
userId = userResult.rows[0].id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем, является ли пользователь администратором
|
|
||||||
isAdmin = await checkUserRole(normalizedAddress);
|
|
||||||
|
|
||||||
// Обновляем роль пользователя
|
|
||||||
const roleNameToSet = isAdmin ? 'admin' : 'user';
|
|
||||||
const roleToSetResult = await db.query('SELECT id FROM roles WHERE name = $1', [roleNameToSet]);
|
|
||||||
if (roleToSetResult.rows.length > 0) {
|
|
||||||
const roleIdToSet = roleToSetResult.rows[0].id;
|
|
||||||
await db.query('UPDATE users SET role_id = $1 WHERE id = $2', [roleIdToSet, userId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { userId, isAdmin };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error finding or creating user:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
verifySignature,
|
|
||||||
checkAccess,
|
|
||||||
findOrCreateUser,
|
|
||||||
checkUserRole
|
|
||||||
};
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
const { ethers } = require('ethers');
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const logger = require('./logger');
|
|
||||||
|
|
||||||
// Инициализация провайдера
|
|
||||||
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
|
|
||||||
|
|
||||||
// Путь к директории с ABI контрактов
|
|
||||||
const contractsDir = path.join(__dirname, '../artifacts/contracts/AccessToken.sol');
|
|
||||||
|
|
||||||
// Получение ABI контракта
|
|
||||||
const accessTokenJSON = require('../artifacts/contracts/AccessToken.sol/AccessToken.json');
|
|
||||||
const accessTokenABI = accessTokenJSON.abi;
|
|
||||||
|
|
||||||
// Проверка, что ABI является массивом
|
|
||||||
if (!Array.isArray(accessTokenABI)) {
|
|
||||||
console.error('ABI is not an array:', accessTokenABI);
|
|
||||||
// Если ABI не является массивом, создайте массив вручную
|
|
||||||
const manualABI = [
|
|
||||||
"function mintAccessToken(address to, uint8 role) public",
|
|
||||||
"function checkRole(address user) public view returns (uint8)",
|
|
||||||
"function revokeToken(uint256 tokenId) public",
|
|
||||||
// Добавьте другие функции, которые вам нужны
|
|
||||||
];
|
|
||||||
|
|
||||||
// Создание экземпляра контракта с ручным ABI
|
|
||||||
const contractAddress = process.env.ACCESS_TOKEN_ADDRESS;
|
|
||||||
const accessTokenContract = new ethers.Contract(contractAddress, manualABI, provider);
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
accessTokenContract,
|
|
||||||
getContract,
|
|
||||||
provider
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Если ABI является массивом, используйте его
|
|
||||||
const contractAddress = process.env.ACCESS_TOKEN_ADDRESS;
|
|
||||||
const accessTokenContract = new ethers.Contract(contractAddress, accessTokenABI, provider);
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
accessTokenContract,
|
|
||||||
getContract,
|
|
||||||
provider
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Кэш для хранения экземпляров контрактов
|
|
||||||
const contractsCache = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Получает экземпляр контракта по его имени
|
|
||||||
* @param {string} contractName - Имя контракта
|
|
||||||
* @returns {Promise<ethers.Contract>} - Экземпляр контракта
|
|
||||||
*/
|
|
||||||
async function getContract(contractName) {
|
|
||||||
try {
|
|
||||||
console.log(`Getting contract: ${contractName}`);
|
|
||||||
|
|
||||||
// Проверяем, есть ли контракт в кэше
|
|
||||||
if (contractsCache[contractName]) {
|
|
||||||
console.log(`Using cached contract: ${contractName}`);
|
|
||||||
return contractsCache[contractName];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получаем адрес контракта из переменных окружения
|
|
||||||
const contractAddress = process.env.ACCESS_TOKEN_ADDRESS; // или ACCESS_TOKEN_CONTRACT_ADDRESS
|
|
||||||
|
|
||||||
if (!contractAddress) {
|
|
||||||
throw new Error(`Contract address for ${contractName} not found in environment variables`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Путь к файлу с ABI контракта
|
|
||||||
const abiPath = path.join(contractsDir, `${contractName}.json`);
|
|
||||||
|
|
||||||
// Проверяем, существует ли файл с ABI
|
|
||||||
if (!fs.existsSync(abiPath)) {
|
|
||||||
throw new Error(`ABI file for ${contractName} not found at ${abiPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Читаем ABI из файла
|
|
||||||
const abiJson = fs.readFileSync(abiPath, 'utf8');
|
|
||||||
const contractJSON = JSON.parse(abiJson);
|
|
||||||
const abi = contractJSON.abi; // Получаем ABI из свойства abi
|
|
||||||
|
|
||||||
console.log(`ABI for ${contractName}:`, abi);
|
|
||||||
|
|
||||||
// Проверяем, что ABI является массивом
|
|
||||||
if (!Array.isArray(abi)) {
|
|
||||||
console.error(`ABI for ${contractName} is not an array:`, abi);
|
|
||||||
throw new Error(`ABI for ${contractName} is not an array`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создаем экземпляр контракта
|
|
||||||
const contract = new ethers.Contract(contractAddress, abi, provider);
|
|
||||||
|
|
||||||
// Сохраняем контракт в кэше
|
|
||||||
contractsCache[contractName] = contract;
|
|
||||||
|
|
||||||
return contract;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Ошибка при получении контракта ${contractName}: ${error.message}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
const db = require('../db');
|
|
||||||
const logger = require('./logger');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Связывает идентификатор с пользователем
|
|
||||||
* @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 db.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 db.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 db.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 db.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,
|
|
||||||
};
|
|
||||||
@@ -8,34 +8,26 @@
|
|||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
import { useAuthStore } from './stores/auth';
|
import { useAuthStore } from './stores/auth';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
console.log('App.vue: Version with auth check loaded');
|
console.log('App.vue: Version with auth check loaded');
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
onMounted(async () => {
|
async function checkAuth() {
|
||||||
console.log('App.vue: onMounted - checking auth');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Проверяем аутентификацию на сервере
|
const response = await axios.get('/api/auth/check');
|
||||||
const result = await authStore.checkAuth();
|
|
||||||
console.log('Auth check result:', result.authenticated);
|
|
||||||
|
|
||||||
if (result.authenticated) {
|
if (response.data.authenticated) {
|
||||||
// Если пользователь аутентифицирован, восстанавливаем состояние
|
authStore.setAuth(response.data);
|
||||||
console.log('Session restored from server');
|
|
||||||
|
|
||||||
// Загружаем историю чата, если мы на странице чата
|
|
||||||
if (router.currentRoute.value.name === 'home') {
|
|
||||||
console.log('Loading chat history after session restore');
|
|
||||||
// Здесь можно вызвать метод для загрузки истории чата
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking auth:', error);
|
console.error('Error checking auth:', error);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
onMounted(checkAuth);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -2,43 +2,30 @@ import axios from 'axios';
|
|||||||
import { useAuthStore } from '../stores/auth';
|
import { useAuthStore } from '../stores/auth';
|
||||||
|
|
||||||
// Создаем экземпляр axios с базовым URL
|
// Создаем экземпляр axios с базовым URL
|
||||||
const instance = axios.create({
|
const api = axios.create({
|
||||||
baseURL: '/',
|
baseURL: '', // Убираем baseURL
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json'
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Добавляем перехватчик для добавления заголовка авторизации
|
// Перехватчик запросов
|
||||||
instance.interceptors.request.use(
|
api.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
console.log('Axios interceptor running');
|
config.withCredentials = true; // Важно для каждого запроса
|
||||||
const authStore = useAuthStore();
|
|
||||||
|
|
||||||
// Логируем параметры запроса
|
const authStore = useAuthStore();
|
||||||
console.log('Request parameters:', config);
|
|
||||||
|
|
||||||
// Если уже есть заголовок Authorization, не перезаписываем его
|
|
||||||
if (config.headers.Authorization) {
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если пользователь аутентифицирован и есть адрес кошелька
|
|
||||||
if (authStore.isAuthenticated && authStore.address) {
|
if (authStore.isAuthenticated && authStore.address) {
|
||||||
console.log('Adding Authorization header:', `Bearer ${authStore.address}`);
|
|
||||||
config.headers.Authorization = `Bearer ${authStore.address}`;
|
config.headers.Authorization = `Bearer ${authStore.address}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => Promise.reject(error)
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Добавляем перехватчик для обработки ответов
|
// Добавляем перехватчик для обработки ответов
|
||||||
instance.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
console.log('Response from server:', response.data);
|
console.log('Response from server:', response.data);
|
||||||
return response;
|
return response;
|
||||||
@@ -69,4 +56,4 @@ const sendGuestMessageToServer = async (messageText) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default instance;
|
export default api;
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="telegram-auth">
|
|
||||||
<button v-if="!showVerification" class="auth-btn telegram-btn" @click="startTelegramAuth">
|
|
||||||
<span class="auth-icon">📱</span> Подключить Telegram
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div v-else class="verification-form">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
v-model="verificationCode"
|
|
||||||
placeholder="Введите код из Telegram"
|
|
||||||
/>
|
|
||||||
<button class="auth-btn verify-btn" @click="verifyCode">Подтвердить</button>
|
|
||||||
<button class="auth-btn cancel-btn" @click="cancelVerification">Отмена</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { useAuthStore } from '../stores/auth';
|
|
||||||
import axios from '../api/axios';
|
|
||||||
|
|
||||||
const auth = useAuthStore();
|
|
||||||
const showVerification = ref(false);
|
|
||||||
const verificationCode = ref('');
|
|
||||||
|
|
||||||
const startTelegramAuth = () => {
|
|
||||||
// Открываем Telegram бота в новом окне
|
|
||||||
window.open('https://t.me/your_bot_username', '_blank');
|
|
||||||
showVerification.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const verifyCode = async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post('/api/auth/telegram/verify', {
|
|
||||||
code: verificationCode.value
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data.success) {
|
|
||||||
auth.setTelegramAuth(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error verifying Telegram code:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelVerification = () => {
|
|
||||||
showVerification.value = false;
|
|
||||||
verificationCode.value = '';
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.telegram-auth {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.telegram-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: #0088cc;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-icon {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-progress {
|
|
||||||
background-color: #f8f8f8;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 16px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel-btn {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
color: #333;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.check-btn {
|
|
||||||
background-color: #0088cc;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
color: #ff4d4f;
|
|
||||||
margin-top: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-code {
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 12px;
|
|
||||||
background-color: #f1f1f1;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin: 15px 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-btn {
|
|
||||||
background-color: #4CAF50;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: bold;
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-btn:hover {
|
|
||||||
background-color: #45a049;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="wallet-connection">
|
|
||||||
<div v-if="error" class="error-message">
|
|
||||||
{{ error }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="!authStore.isAuthenticated">
|
|
||||||
<button @click="connectWallet" class="connect-button" :disabled="loading">
|
|
||||||
<div v-if="loading" class="spinner"></div>
|
|
||||||
{{ loading ? 'Подключение...' : 'Подключить кошелек' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div v-else class="wallet-info">
|
|
||||||
<span class="address">{{ formatAddress(authStore.user?.address) }}</span>
|
|
||||||
<button @click="disconnectWallet" class="disconnect-btn">Выйти</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted } from 'vue';
|
|
||||||
import { connectWithWallet } from '../utils/wallet';
|
|
||||||
import { useAuthStore } from '../stores/auth';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
const router = useRouter();
|
|
||||||
const loading = ref(false);
|
|
||||||
const error = ref('');
|
|
||||||
const isConnecting = ref(false);
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
onWalletAuth: {
|
|
||||||
type: Function,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
isAuthenticated: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Форматирование адреса кошелька
|
|
||||||
const formatAddress = (address) => {
|
|
||||||
if (!address) return '';
|
|
||||||
return `${address.substring(0, 6)}...${address.substring(address.length - 4)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Функция для подключения кошелька
|
|
||||||
const connectWallet = async () => {
|
|
||||||
loading.value = true;
|
|
||||||
error.value = '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
await props.onWalletAuth();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Ошибка при подключении кошелька:', err);
|
|
||||||
error.value = 'Ошибка подключения кошелька';
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Автоматическое подключение при загрузке компонента
|
|
||||||
onMounted(async () => {
|
|
||||||
console.log('WalletConnection mounted, checking auth state...');
|
|
||||||
|
|
||||||
// Проверяем аутентификацию на сервере
|
|
||||||
const authState = await authStore.checkAuth();
|
|
||||||
console.log('Auth state after check:', authState);
|
|
||||||
|
|
||||||
// Если пользователь уже аутентифицирован, не нужно ничего делать
|
|
||||||
if (authState.authenticated) {
|
|
||||||
console.log('User is already authenticated, no need to reconnect');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем, есть ли сохраненный адрес кошелька
|
|
||||||
const savedAddress = localStorage.getItem('walletAddress');
|
|
||||||
|
|
||||||
if (savedAddress && window.ethereum) {
|
|
||||||
console.log('Found saved wallet address:', savedAddress);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Проверяем, разблокирован ли MetaMask, но не запрашиваем разрешение
|
|
||||||
const accounts = await window.ethereum.request({
|
|
||||||
method: 'eth_accounts' // Используем eth_accounts вместо eth_requestAccounts
|
|
||||||
});
|
|
||||||
|
|
||||||
if (accounts && accounts.length > 0) {
|
|
||||||
console.log('MetaMask is unlocked, connected accounts:', accounts);
|
|
||||||
|
|
||||||
// Если кошелек разблокирован и есть доступные аккаунты, проверяем совпадение адреса
|
|
||||||
if (accounts[0].toLowerCase() === savedAddress.toLowerCase()) {
|
|
||||||
console.log('Current account matches saved address');
|
|
||||||
|
|
||||||
// Не вызываем handleConnectWallet() автоматически,
|
|
||||||
// просто показываем пользователю, что он может подключиться
|
|
||||||
} else {
|
|
||||||
console.log('Current account does not match saved address');
|
|
||||||
localStorage.removeItem('walletAddress');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('MetaMask is locked or no accounts available');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking MetaMask state:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Функция для отключения кошелька
|
|
||||||
const disconnectWallet = async () => {
|
|
||||||
try {
|
|
||||||
// Сначала отключаем MetaMask
|
|
||||||
if (window.ethereum) {
|
|
||||||
try {
|
|
||||||
// Просто очищаем слушатели событий
|
|
||||||
window.ethereum.removeAllListeners();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error disconnecting MetaMask:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Затем выполняем выход из системы
|
|
||||||
await authStore.disconnect(router);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error disconnecting wallet:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.wallet-connection {
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connect-button {
|
|
||||||
background-color: #1976d2;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 16px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connect-button:disabled {
|
|
||||||
background-color: #ccc;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
color: #d32f2f;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
background-color: #ffebee;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
||||||
border-radius: 50%;
|
|
||||||
border-top-color: white;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wallet-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 10px;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.address {
|
|
||||||
font-family: monospace;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.disconnect-btn {
|
|
||||||
background-color: #f44336;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,195 +1,117 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="email-connect">
|
<div class="email-connection">
|
||||||
<p>Подключите свой email для быстрой авторизации.</p>
|
<div v-if="!showVerification">
|
||||||
|
|
||||||
<div class="email-form">
|
|
||||||
<input
|
<input
|
||||||
type="email"
|
v-model="email"
|
||||||
v-model="email"
|
type="email"
|
||||||
placeholder="Введите ваш email"
|
placeholder="Введите email"
|
||||||
:disabled="loading || verificationSent"
|
class="email-input"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@click="sendVerification"
|
@click="requestCode"
|
||||||
class="connect-button"
|
:disabled="isLoading || !isValidEmail"
|
||||||
:disabled="!isValidEmail || loading || verificationSent"
|
class="email-btn"
|
||||||
>
|
>
|
||||||
<span class="email-icon">✉️</span> {{ verificationSent ? 'Код отправлен' : 'Отправить код' }}
|
Получить код
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else>
|
||||||
<div v-if="verificationSent" class="verification-form">
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
v-model="code"
|
||||||
v-model="verificationCode"
|
type="text"
|
||||||
placeholder="Введите код подтверждения"
|
placeholder="Введите код"
|
||||||
:disabled="loading"
|
class="code-input"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@click="verifyEmail"
|
@click="verifyCode"
|
||||||
class="verify-button"
|
:disabled="isLoading"
|
||||||
:disabled="!verificationCode || loading"
|
class="verify-btn"
|
||||||
>
|
>
|
||||||
Подтвердить
|
Подтвердить
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="loading" class="loading">Загрузка...</div>
|
|
||||||
<div v-if="error" class="error">{{ error }}</div>
|
<div v-if="error" class="error">{{ error }}</div>
|
||||||
<div v-if="success" class="success">{{ success }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
const email = ref('');
|
const props = defineProps({
|
||||||
const verificationCode = ref('');
|
onEmailAuth: {
|
||||||
const loading = ref(false);
|
type: Function,
|
||||||
const error = ref('');
|
required: true
|
||||||
const success = ref('');
|
}
|
||||||
const verificationSent = ref(false);
|
|
||||||
|
|
||||||
const isValidEmail = computed(() => {
|
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
||||||
return emailRegex.test(email.value);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function sendVerification() {
|
const email = ref('');
|
||||||
if (!isValidEmail.value) return;
|
const code = ref('');
|
||||||
|
const error = ref('');
|
||||||
try {
|
const isLoading = ref(false);
|
||||||
loading.value = true;
|
const showVerification = ref(false);
|
||||||
error.value = '';
|
|
||||||
success.value = '';
|
|
||||||
|
|
||||||
// Запрос на отправку кода подтверждения
|
|
||||||
const response = await axios.post('/api/auth/email', {
|
|
||||||
email: email.value
|
|
||||||
}, {
|
|
||||||
withCredentials: true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data.error) {
|
|
||||||
error.value = `Ошибка: ${response.data.error}`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
verificationSent.value = true;
|
|
||||||
success.value = `Код подтверждения отправлен на ${email.value}`;
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error sending verification code:', err);
|
|
||||||
error.value = 'Ошибка при отправке кода подтверждения';
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function verifyEmail() {
|
const isValidEmail = computed(() => {
|
||||||
if (!verificationCode.value) return;
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestCode = async () => {
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
isLoading.value = true;
|
||||||
|
await props.onEmailAuth(email.value);
|
||||||
|
showVerification.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
success.value = '';
|
|
||||||
|
|
||||||
// Запрос на проверку кода
|
|
||||||
const response = await axios.post('/api/auth/email/verify', {
|
|
||||||
email: email.value,
|
|
||||||
code: verificationCode.value
|
|
||||||
}, {
|
|
||||||
withCredentials: true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data.error) {
|
|
||||||
error.value = `Ошибка: ${response.data.error}`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
success.value = 'Email успешно подтвержден';
|
|
||||||
|
|
||||||
// Сбрасываем форму
|
|
||||||
setTimeout(() => {
|
|
||||||
email.value = '';
|
|
||||||
verificationCode.value = '';
|
|
||||||
verificationSent.value = false;
|
|
||||||
success.value = '';
|
|
||||||
}, 3000);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error verifying email:', err);
|
error.value = err.message || 'Ошибка отправки кода';
|
||||||
error.value = 'Ошибка при проверке кода подтверждения';
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const verifyCode = async () => {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
await props.onEmailAuth(email.value, code.value);
|
||||||
|
error.value = '';
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err.message || 'Неверный код';
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.email-connect {
|
.email-connection {
|
||||||
display: flex;
|
margin: 10px 0;
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-form, .verification-form {
|
.email-input,
|
||||||
display: flex;
|
.code-input {
|
||||||
gap: 10px;
|
padding: 8px;
|
||||||
}
|
margin-right: 10px;
|
||||||
|
|
||||||
input {
|
|
||||||
flex: 1;
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.connect-button, .verify-button {
|
.email-btn,
|
||||||
display: flex;
|
.verify-btn {
|
||||||
align-items: center;
|
padding: 10px 20px;
|
||||||
justify-content: center;
|
background-color: #48bb78;
|
||||||
padding: 10px 15px;
|
|
||||||
background-color: #4caf50;
|
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
transition: background-color 0.2s;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connect-button:hover, .verify-button:hover {
|
|
||||||
background-color: #45a049;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connect-button:disabled, .verify-button:disabled {
|
|
||||||
background-color: #cccccc;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-icon {
|
|
||||||
margin-right: 10px;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading, .error, .success {
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
background-color: #f8d7da;
|
color: #e53e3e;
|
||||||
color: #721c24;
|
margin-top: 5px;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.success {
|
button:disabled {
|
||||||
background-color: #d4edda;
|
opacity: 0.7;
|
||||||
color: #155724;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, onMounted } from 'vue';
|
|
||||||
import { useAuthStore } from '../../stores/auth';
|
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
const identities = ref({});
|
|
||||||
const newIdentity = ref({ type: 'email', value: '' });
|
|
||||||
const loading = ref(false);
|
|
||||||
const error = ref(null);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
|
||||||
loading.value = true;
|
|
||||||
const response = await fetch('/api/access/tokens', {
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
const data = await response.json();
|
|
||||||
identities.value = data.identities || {};
|
|
||||||
} catch (err) {
|
|
||||||
error.value = 'Ошибка при загрузке идентификаторов';
|
|
||||||
console.error(err);
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function addIdentity() {
|
|
||||||
try {
|
|
||||||
loading.value = true;
|
|
||||||
error.value = null;
|
|
||||||
|
|
||||||
const success = await authStore.linkIdentity(
|
|
||||||
newIdentity.value.type,
|
|
||||||
newIdentity.value.value
|
|
||||||
);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
identities.value = authStore.identities;
|
|
||||||
newIdentity.value.value = '';
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
error.value = 'Ошибка при добавлении идентификатора';
|
|
||||||
console.error(err);
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
84
frontend/src/components/identity/WalletConnection.vue
Normal file
84
frontend/src/components/identity/WalletConnection.vue
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<template>
|
||||||
|
<div class="wallet-connection">
|
||||||
|
<button
|
||||||
|
@click="connectWallet"
|
||||||
|
:disabled="isLoading"
|
||||||
|
class="wallet-btn"
|
||||||
|
>
|
||||||
|
{{ isAuthenticated ? 'Подключено' : 'Подключить кошелек' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { connectWithWallet } from '../../services/wallet';
|
||||||
|
|
||||||
|
// Определяем props
|
||||||
|
const props = defineProps({
|
||||||
|
isAuthenticated: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Определяем состояние
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
|
const emit = defineEmits(['connect']);
|
||||||
|
|
||||||
|
// Метод подключения кошелька
|
||||||
|
const connectWallet = async () => {
|
||||||
|
if (isLoading.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
// Получаем адрес кошелька
|
||||||
|
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||||
|
const address = accounts[0];
|
||||||
|
|
||||||
|
// Получаем nonce
|
||||||
|
const nonceResponse = await api.get(`/api/auth/nonce?address=${address}`);
|
||||||
|
const nonce = nonceResponse.data.nonce;
|
||||||
|
|
||||||
|
// Подписываем сообщение
|
||||||
|
const message = `${window.location.host} wants you to sign in with your Ethereum account:\n${address.slice(0, 42)}...`;
|
||||||
|
const signature = await window.ethereum.request({
|
||||||
|
method: 'personal_sign',
|
||||||
|
params: [message, address]
|
||||||
|
});
|
||||||
|
|
||||||
|
emit('connect', { address, signature, message });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error connecting wallet:', error);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.wallet-connection {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #4a5568;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-btn:hover:not(:disabled) {
|
||||||
|
background-color: #2d3748;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-btn:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
9
frontend/src/components/identity/index.js
Normal file
9
frontend/src/components/identity/index.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import TelegramConnect from './TelegramConnect.vue';
|
||||||
|
import WalletConnection from './WalletConnection.vue';
|
||||||
|
import EmailConnect from './EmailConnect.vue';
|
||||||
|
|
||||||
|
export {
|
||||||
|
TelegramConnect,
|
||||||
|
WalletConnection,
|
||||||
|
EmailConnect
|
||||||
|
};
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { ref } from 'vue';
|
|
||||||
import { ethers } from 'ethers';
|
|
||||||
|
|
||||||
export function useEthereum() {
|
|
||||||
const address = ref('');
|
|
||||||
const isConnected = ref(false);
|
|
||||||
const provider = ref(null);
|
|
||||||
const signer = ref(null);
|
|
||||||
|
|
||||||
async function connect() {
|
|
||||||
if (window.ethereum) {
|
|
||||||
try {
|
|
||||||
// Запрашиваем доступ к кошельку
|
|
||||||
await window.ethereum.request({ method: 'eth_requestAccounts' });
|
|
||||||
// Используем синтаксис ethers.js v6
|
|
||||||
provider.value = new ethers.BrowserProvider(window.ethereum);
|
|
||||||
signer.value = await provider.value.getSigner();
|
|
||||||
address.value = await signer.value.getAddress();
|
|
||||||
isConnected.value = true;
|
|
||||||
|
|
||||||
console.log('Подключение успешно:', address.value);
|
|
||||||
return { success: true, address: address.value };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка подключения к кошельку:', error);
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error('Ethereum wallet not found. Please install MetaMask.');
|
|
||||||
return { success: false, error: 'Ethereum wallet not found. Please install MetaMask.' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getContract(contractAddress, contractABI) {
|
|
||||||
if (!signer.value) {
|
|
||||||
console.error('Подключите кошелек перед получением контракта.');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Используем синтаксис ethers.js v6
|
|
||||||
const contract = new ethers.Contract(contractAddress, contractABI, signer.value);
|
|
||||||
console.log('Контракт получен:', contract);
|
|
||||||
return contract;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка получения контракта:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
address,
|
|
||||||
isConnected,
|
|
||||||
connect,
|
|
||||||
getContract,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,6 @@ const routes = [
|
|||||||
name: 'home',
|
name: 'home',
|
||||||
component: HomeView
|
component: HomeView
|
||||||
}
|
}
|
||||||
// Другие маршруты можно добавить позже, когда будут созданы соответствующие компоненты
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@@ -32,11 +31,10 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
// Проверяем аутентификацию, если маршрут требует авторизации
|
// Проверяем аутентификацию, если маршрут требует авторизации
|
||||||
if (to.matched.some(record => record.meta.requiresAuth)) {
|
if (to.matched.some(record => record.meta.requiresAuth)) {
|
||||||
if (!authStore.isAuthenticated) {
|
if (!authStore.isAuthenticated) {
|
||||||
// Если пользователь не авторизован, перенаправляем на главную
|
|
||||||
return next({ name: 'home' });
|
return next({ name: 'home' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем права администратора, если маршрут требует прав администратора
|
// Проверяем права администратора
|
||||||
if (to.matched.some(record => record.meta.requiresAdmin) && !authStore.isAdmin) {
|
if (to.matched.some(record => record.meta.requiresAdmin) && !authStore.isAdmin) {
|
||||||
return next({ name: 'home' });
|
return next({ name: 'home' });
|
||||||
}
|
}
|
||||||
|
|||||||
80
frontend/src/services/wallet.js
Normal file
80
frontend/src/services/wallet.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { ethers } from 'ethers';
|
||||||
|
import api from '../api/axios';
|
||||||
|
import { useAuthStore } from '../stores/auth';
|
||||||
|
|
||||||
|
export async function connectWithWallet() {
|
||||||
|
try {
|
||||||
|
console.log('Starting wallet connection...');
|
||||||
|
// Проверяем наличие MetaMask
|
||||||
|
if (!window.ethereum) {
|
||||||
|
throw new Error('MetaMask не установлен. Пожалуйста, установите MetaMask');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('MetaMask detected, requesting accounts...');
|
||||||
|
const accounts = await window.ethereum.request({
|
||||||
|
method: 'eth_requestAccounts'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Got accounts:', accounts);
|
||||||
|
if (!accounts || accounts.length === 0) {
|
||||||
|
throw new Error('Нет доступных аккаунтов. Пожалуйста, разблокируйте MetaMask');
|
||||||
|
}
|
||||||
|
|
||||||
|
const address = ethers.getAddress(accounts[0]);
|
||||||
|
console.log('Normalized address:', address);
|
||||||
|
|
||||||
|
console.log('Requesting nonce...');
|
||||||
|
const { data: { nonce } } = await api.get('/api/auth/nonce', {
|
||||||
|
params: { address }
|
||||||
|
});
|
||||||
|
console.log('Got nonce:', nonce);
|
||||||
|
|
||||||
|
// Формируем сообщение в формате SIWE (Sign-In with Ethereum)
|
||||||
|
const domain = window.location.host;
|
||||||
|
const origin = window.location.origin;
|
||||||
|
const statement = "Sign in with Ethereum to the app.";
|
||||||
|
const message = [
|
||||||
|
`${domain} wants you to sign in with your Ethereum account:`,
|
||||||
|
address,
|
||||||
|
"",
|
||||||
|
statement,
|
||||||
|
"",
|
||||||
|
`URI: ${origin}`,
|
||||||
|
"Version: 1",
|
||||||
|
"Chain ID: 1",
|
||||||
|
`Nonce: ${nonce}`,
|
||||||
|
`Issued At: ${new Date().toISOString()}`,
|
||||||
|
"Resources:",
|
||||||
|
`- ${origin}/api/auth/verify`
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
console.log('SIWE message:', message);
|
||||||
|
|
||||||
|
console.log('Requesting signature...');
|
||||||
|
const signature = await window.ethereum.request({
|
||||||
|
method: 'personal_sign',
|
||||||
|
params: [message, address]
|
||||||
|
});
|
||||||
|
console.log('Got signature:', signature);
|
||||||
|
|
||||||
|
console.log('Sending verification request...');
|
||||||
|
const response = await api.post('/api/auth/verify', {
|
||||||
|
address,
|
||||||
|
signature,
|
||||||
|
message
|
||||||
|
});
|
||||||
|
console.log('Verification response:', response.data);
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
if (response.data.authenticated) {
|
||||||
|
authStore.setAuth(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
// Форматируем ошибку для пользователя
|
||||||
|
const message = error.message || 'Ошибка подключения кошелька';
|
||||||
|
console.error('Error connecting wallet:', message);
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,37 +1,22 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import axios from '../api/axios';
|
import axios from '../api/axios';
|
||||||
|
|
||||||
const loadAuthState = () => {
|
|
||||||
const savedAuth = localStorage.getItem('auth');
|
|
||||||
if (savedAuth) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(savedAuth);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error parsing saved auth state:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', {
|
export const useAuthStore = defineStore('auth', {
|
||||||
state: () => {
|
state: () => ({
|
||||||
const savedState = loadAuthState();
|
user: null,
|
||||||
return {
|
isAuthenticated: false,
|
||||||
user: null,
|
isAdmin: false,
|
||||||
isAuthenticated: savedState?.isAuthenticated || false,
|
authType: null,
|
||||||
isAdmin: savedState?.isAdmin || false,
|
identities: {},
|
||||||
authType: savedState?.authType || null,
|
loading: false,
|
||||||
identities: {},
|
error: null,
|
||||||
loading: false,
|
messages: [],
|
||||||
error: null,
|
address: null,
|
||||||
messages: [],
|
wallet: null,
|
||||||
address: null,
|
telegramId: null,
|
||||||
wallet: null,
|
email: null,
|
||||||
telegramId: savedState?.telegramId || null,
|
userId: null
|
||||||
email: null,
|
}),
|
||||||
userId: savedState?.userId || null
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
async connectWallet(address, signature, message) {
|
async connectWallet(address, signature, message) {
|
||||||
@@ -442,24 +427,22 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async disconnect(router) {
|
async disconnect() {
|
||||||
// Проверяем, действительно ли нужно выходить
|
|
||||||
if (!this.isAuthenticated) {
|
|
||||||
console.log('Already logged out, skipping disconnect');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Сначала пробуем очистить сессию на сервере
|
// Очищаем сессию на сервере
|
||||||
await axios.post('/api/auth/clear-session');
|
await axios.post('/api/auth/clear-session');
|
||||||
await axios.post('/api/auth/logout');
|
|
||||||
|
|
||||||
// Очищаем состояние только после успешного выхода
|
// Очищаем состояние
|
||||||
this.clearState();
|
this.isAuthenticated = false;
|
||||||
|
this.userId = null;
|
||||||
if (router) router.push('/');
|
this.address = null;
|
||||||
|
this.isAdmin = false;
|
||||||
|
this.authType = null;
|
||||||
|
|
||||||
|
// Очищаем локальное хранилище
|
||||||
|
localStorage.removeItem('auth');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during logout:', error);
|
console.error('Error during disconnect:', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -509,29 +492,23 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
setAuth(authData) {
|
setAuth(authData) {
|
||||||
console.log('Setting auth state:', authData);
|
console.log('Setting auth state:', authData);
|
||||||
|
|
||||||
// Обновляем все поля состояния
|
// Обновляем только состояние в памяти
|
||||||
this.isAuthenticated = authData.authenticated || authData.isAuthenticated;
|
this.isAuthenticated = authData.authenticated || authData.isAuthenticated;
|
||||||
|
this.user = {
|
||||||
|
id: authData.userId,
|
||||||
|
address: authData.address
|
||||||
|
};
|
||||||
this.userId = authData.userId;
|
this.userId = authData.userId;
|
||||||
this.isAdmin = authData.isAdmin;
|
this.isAdmin = authData.isAdmin;
|
||||||
this.authType = authData.authType;
|
this.authType = authData.authType;
|
||||||
this.address = authData.address;
|
this.address = authData.address;
|
||||||
|
|
||||||
// Сохраняем состояние в localStorage
|
|
||||||
const stateToSave = {
|
|
||||||
isAuthenticated: this.isAuthenticated,
|
|
||||||
userId: this.userId,
|
|
||||||
isAdmin: this.isAdmin,
|
|
||||||
authType: this.authType,
|
|
||||||
address: this.address
|
|
||||||
};
|
|
||||||
|
|
||||||
localStorage.setItem('auth', JSON.stringify(stateToSave));
|
|
||||||
|
|
||||||
console.log('Auth state updated:', {
|
console.log('Auth state updated:', {
|
||||||
isAuthenticated: this.isAuthenticated,
|
isAuthenticated: this.isAuthenticated,
|
||||||
userId: this.userId,
|
userId: this.userId,
|
||||||
authType: this.authType,
|
authType: this.authType,
|
||||||
address: this.address
|
address: this.address,
|
||||||
|
isAdmin: this.isAdmin
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
import { ethers } from 'ethers';
|
|
||||||
import axios from '../api/axios';
|
|
||||||
import { useAuthStore } from '../stores/auth';
|
|
||||||
|
|
||||||
// Переименовываем функцию для соответствия импорту
|
|
||||||
export async function connectWithWallet() {
|
|
||||||
try {
|
|
||||||
// Проверяем, доступен ли MetaMask
|
|
||||||
if (!window.ethereum) {
|
|
||||||
throw new Error('MetaMask не установлен');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Запрашиваем доступ к кошельку
|
|
||||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
|
||||||
const signer = await provider.getSigner();
|
|
||||||
const address = await signer.getAddress();
|
|
||||||
|
|
||||||
// Получаем nonce для подписи
|
|
||||||
const nonceResponse = await axios.get(`/api/auth/nonce?address=${address}`);
|
|
||||||
const nonce = nonceResponse.data.nonce;
|
|
||||||
|
|
||||||
// Формируем сообщение для подписи
|
|
||||||
const message = `Подпишите это сообщение для аутентификации в DApp for Business. Nonce: ${nonce}`;
|
|
||||||
|
|
||||||
// Подписываем сообщение
|
|
||||||
const signature = await signer.signMessage(message);
|
|
||||||
|
|
||||||
// Верифицируем подпись на сервере
|
|
||||||
const response = await axios.post('/api/auth/verify', {
|
|
||||||
address,
|
|
||||||
signature,
|
|
||||||
message: nonce
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Wallet verification response:', response.data);
|
|
||||||
|
|
||||||
// Обновляем состояние в хранилище auth
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
authStore.isAuthenticated = response.data.authenticated;
|
|
||||||
authStore.user = {
|
|
||||||
id: response.data.userId,
|
|
||||||
address: response.data.address
|
|
||||||
};
|
|
||||||
authStore.isAdmin = response.data.isAdmin;
|
|
||||||
authStore.authType = 'wallet';
|
|
||||||
|
|
||||||
// Сохраняем адрес кошелька в локальном хранилище
|
|
||||||
localStorage.setItem('walletAddress', address);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
authenticated: response.data.authenticated,
|
|
||||||
userId: response.data.userId,
|
|
||||||
address: response.data.address,
|
|
||||||
isAdmin: response.data.isAdmin,
|
|
||||||
authType: 'wallet'
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error connecting wallet:', error);
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function disconnectWallet() {
|
|
||||||
try {
|
|
||||||
// Отправляем запрос на выход
|
|
||||||
await axios.post(
|
|
||||||
'/api/auth/logout',
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
withCredentials: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Удаляем адрес кошелька из локального хранилища
|
|
||||||
localStorage.removeItem('walletAddress');
|
|
||||||
|
|
||||||
// Обновляем состояние в хранилище auth
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
authStore.isAuthenticated = false;
|
|
||||||
authStore.user = null;
|
|
||||||
authStore.isAdmin = false;
|
|
||||||
authStore.authType = null;
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при отключении кошелька:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,29 +5,39 @@
|
|||||||
<div class="auth-section" v-if="!auth.isAuthenticated">
|
<div class="auth-section" v-if="!auth.isAuthenticated">
|
||||||
<h3>Венчурный фонд и поставщик программного обеспечения</h3>
|
<h3>Венчурный фонд и поставщик программного обеспечения</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chat-container">
|
<div class="chat-container">
|
||||||
<div class="chat-header">
|
<div class="chat-header">
|
||||||
<WalletConnection
|
<!-- Используем тот же компонент, что и в сообщениях -->
|
||||||
:onWalletAuth="handleWalletAuth"
|
<div v-if="!auth.isAuthenticated" class="auth-buttons">
|
||||||
:isAuthenticated="auth.isAuthenticated"
|
<button class="auth-btn wallet-btn" @click="handleWalletAuth">
|
||||||
/>
|
<span class="auth-icon">👛</span> Подключить кошелек
|
||||||
<div class="user-info" v-if="auth.isAuthenticated">
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-else class="wallet-info">
|
||||||
|
<span>{{ truncateAddress(auth.address) }}</span>
|
||||||
|
<button class="disconnect-btn" @click="disconnectWallet">
|
||||||
|
Отключить кошелек
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Кнопка загрузки предыдущих сообщений -->
|
<!-- Кнопка загрузки предыдущих сообщений -->
|
||||||
<div v-if="hasMoreMessages" class="load-more-container">
|
<div v-if="auth.isAuthenticated && hasMoreMessages" class="load-more">
|
||||||
<button @click="loadMoreMessages" class="load-more-btn" :disabled="isLoadingMore">
|
<button @click="loadMoreMessages" :disabled="isLoadingMore">
|
||||||
{{ isLoadingMore ? 'Загрузка...' : 'Показать предыдущие сообщения' }}
|
{{ isLoadingMore ? 'Загрузка...' : 'Показать предыдущие сообщения' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chat-messages" ref="messagesContainer">
|
<div class="chat-messages" ref="messagesContainer">
|
||||||
|
<div v-if="isLoadingMore" class="loading">
|
||||||
|
Загрузка...
|
||||||
|
</div>
|
||||||
<div v-for="message in messages" :key="message.id" :class="['message', message.role === 'assistant' ? 'ai-message' : 'user-message']">
|
<div v-for="message in messages" :key="message.id" :class="['message', message.role === 'assistant' ? 'ai-message' : 'user-message']">
|
||||||
<div class="message-content">
|
<div class="message-content">
|
||||||
{{ message.content }}
|
{{ message.content }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Кнопки аутентификации -->
|
<!-- Кнопки аутентификации -->
|
||||||
<div v-if="message.showAuthButtons && !auth.isAuthenticated" class="auth-buttons">
|
<div v-if="message.showAuthButtons && !auth.isAuthenticated" class="auth-buttons">
|
||||||
<button class="auth-btn wallet-btn" @click="handleWalletAuth">
|
<button class="auth-btn wallet-btn" @click="handleWalletAuth">
|
||||||
@@ -39,32 +49,32 @@
|
|||||||
<button class="auth-btn email-btn" @click="handleEmailAuth">
|
<button class="auth-btn email-btn" @click="handleEmailAuth">
|
||||||
<span class="auth-icon">✉️</span> Подключить Email
|
<span class="auth-icon">✉️</span> Подключить Email
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Email форма -->
|
<!-- Email форма -->
|
||||||
<div v-if="showEmailForm" class="auth-form">
|
<div v-if="showEmailForm" class="auth-form">
|
||||||
<input
|
<input
|
||||||
v-model="emailInput"
|
v-model="emailInput"
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="Введите ваш email"
|
placeholder="Введите ваш email"
|
||||||
class="auth-input"
|
class="auth-input"
|
||||||
/>
|
/>
|
||||||
<button @click="submitEmail" class="auth-btn">
|
<button @click="submitEmail" class="auth-btn">
|
||||||
Отправить код
|
Отправить код
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Форма верификации email -->
|
<!-- Форма верификации email -->
|
||||||
<div v-if="showEmailVerification" class="auth-form">
|
<div v-if="showEmailVerification" class="auth-form">
|
||||||
<input
|
<input
|
||||||
v-model="emailCode"
|
v-model="emailCode"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Введите код из email"
|
placeholder="Введите код из email"
|
||||||
class="auth-input"
|
class="auth-input"
|
||||||
/>
|
/>
|
||||||
<button @click="verifyEmailCode" class="auth-btn">
|
<button @click="verifyEmailCode" class="auth-btn">
|
||||||
Подтвердить
|
Подтвердить
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Telegram верификация -->
|
<!-- Telegram верификация -->
|
||||||
@@ -77,9 +87,9 @@
|
|||||||
/>
|
/>
|
||||||
<button @click="verifyTelegramCode" class="auth-btn">
|
<button @click="verifyTelegramCode" class="auth-btn">
|
||||||
Подтвердить
|
Подтвердить
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="emailError" class="error-message">
|
<div v-if="emailError" class="error-message">
|
||||||
{{ emailError }}
|
{{ emailError }}
|
||||||
</div>
|
</div>
|
||||||
@@ -106,17 +116,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch, nextTick } from 'vue';
|
import { ref, computed, onMounted, watch, nextTick, onBeforeUnmount } from 'vue';
|
||||||
import { useAuthStore } from '../stores/auth';
|
import { useAuthStore } from '../stores/auth';
|
||||||
import WalletConnection from '../components/WalletConnection.vue';
|
import WalletConnection from '../components/identity/WalletConnection.vue';
|
||||||
import TelegramConnect from '../components/TelegramConnect.vue';
|
import TelegramConnect from '../components/identity/TelegramConnect.vue';
|
||||||
import axios from '../api/axios';
|
import api from '../api/axios';
|
||||||
import { connectWithWallet } from '../utils/wallet';
|
import { connectWithWallet } from '../services/wallet';
|
||||||
|
|
||||||
console.log('HomeView.vue: Version with chat loaded');
|
console.log('HomeView.vue: Version with chat loaded');
|
||||||
|
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const messages = ref([]);
|
const messages = ref([]);
|
||||||
|
const guestMessages = ref([]);
|
||||||
const newMessage = ref('');
|
const newMessage = ref('');
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const messagesContainer = ref(null);
|
const messagesContainer = ref(null);
|
||||||
@@ -124,7 +135,6 @@ const userLanguage = ref('ru');
|
|||||||
const email = ref('');
|
const email = ref('');
|
||||||
const isValidEmail = ref(true);
|
const isValidEmail = ref(true);
|
||||||
const hasShownAuthMessage = ref(false);
|
const hasShownAuthMessage = ref(false);
|
||||||
const guestMessages = ref([]);
|
|
||||||
const hasShownAuthOptions = ref(false);
|
const hasShownAuthOptions = ref(false);
|
||||||
|
|
||||||
// Email аутентификация
|
// Email аутентификация
|
||||||
@@ -144,8 +154,10 @@ const emailError = ref('');
|
|||||||
const PAGE_SIZE = 2; // Показываем только последнее сообщение и ответ
|
const PAGE_SIZE = 2; // Показываем только последнее сообщение и ответ
|
||||||
const allMessages = ref([]); // Все загруженные сообщения
|
const allMessages = ref([]); // Все загруженные сообщения
|
||||||
const currentPage = ref(1); // Текущая страница
|
const currentPage = ref(1); // Текущая страница
|
||||||
const hasMoreMessages = ref(false); // Есть ли еще сообщения
|
const hasMoreMessages = ref(true); // Есть ли еще сообщения
|
||||||
const isLoadingMore = ref(false); // Загружаются ли дополнительные сообщения
|
const isLoadingMore = ref(false); // Загружаются ли дополнительные сообщения
|
||||||
|
const offset = ref(0);
|
||||||
|
const limit = ref(20);
|
||||||
|
|
||||||
// Вычисляемое свойство для отображаемых сообщений
|
// Вычисляемое свойство для отображаемых сообщений
|
||||||
const displayedMessages = computed(() => {
|
const displayedMessages = computed(() => {
|
||||||
@@ -153,48 +165,30 @@ const displayedMessages = computed(() => {
|
|||||||
return allMessages.value.slice(startIndex);
|
return allMessages.value.slice(startIndex);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Функция загрузки истории чата
|
// Функция для сокращения адреса кошелька
|
||||||
const loadChatHistory = async () => {
|
const truncateAddress = (address) => {
|
||||||
try {
|
if (!address) return '';
|
||||||
if (!auth.isAuthenticated || !auth.userId) {
|
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||||
return;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const response = await axios.get('/api/chat/history', {
|
// Функция прокрутки к последнему сообщению
|
||||||
headers: { Authorization: `Bearer ${auth.address}` },
|
const scrollToBottom = () => {
|
||||||
params: { limit: PAGE_SIZE, offset: 0 }
|
if (messagesContainer.value) {
|
||||||
});
|
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
|
||||||
|
|
||||||
if (response.data.success) {
|
|
||||||
messages.value = response.data.messages.map(msg => ({
|
|
||||||
id: msg.id,
|
|
||||||
content: msg.content,
|
|
||||||
role: msg.role || (msg.sender_type === 'assistant' ? 'assistant' : 'user'),
|
|
||||||
timestamp: msg.created_at,
|
|
||||||
showAuthOptions: false
|
|
||||||
}));
|
|
||||||
|
|
||||||
hasMoreMessages.value = response.data.total > PAGE_SIZE;
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
scrollToBottom();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading chat history:', error);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Функция загрузки дополнительных сообщений
|
// Загрузка сообщений
|
||||||
const loadMoreMessages = async () => {
|
const loadMoreMessages = async () => {
|
||||||
if (isLoadingMore.value) return;
|
if (!auth.isAuthenticated) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isLoadingMore.value = true;
|
isLoadingMore.value = true;
|
||||||
const offset = messages.value.length;
|
const response = await api.get('/api/chat/history', {
|
||||||
|
params: {
|
||||||
const response = await axios.get('/api/chat/history', {
|
limit: limit.value,
|
||||||
headers: { Authorization: `Bearer ${auth.address}` },
|
offset: offset.value
|
||||||
params: { limit: PAGE_SIZE, offset }
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
@@ -205,83 +199,80 @@ const loadMoreMessages = async () => {
|
|||||||
timestamp: msg.created_at,
|
timestamp: msg.created_at,
|
||||||
showAuthOptions: false
|
showAuthOptions: false
|
||||||
}));
|
}));
|
||||||
|
|
||||||
messages.value = [...newMessages, ...messages.value];
|
messages.value = [...messages.value, ...newMessages];
|
||||||
hasMoreMessages.value = response.data.total > messages.value.length;
|
hasMoreMessages.value = response.data.total > messages.value.length;
|
||||||
|
offset.value += newMessages.length;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading more messages:', error);
|
console.error('Error loading chat history:', error);
|
||||||
} finally {
|
} finally {
|
||||||
isLoadingMore.value = false;
|
isLoadingMore.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Функция прокрутки к последнему сообщению
|
// Загружаем сообщения при изменении аутентификации
|
||||||
const scrollToBottom = () => {
|
watch(() => auth.isAuthenticated, async (newValue) => {
|
||||||
if (messagesContainer.value) {
|
if (newValue) {
|
||||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
|
messages.value = [];
|
||||||
}
|
offset.value = 0;
|
||||||
};
|
hasMoreMessages.value = true;
|
||||||
|
|
||||||
// Инициализация при монтировании
|
try {
|
||||||
onMounted(async () => {
|
// Сначала загружаем историю из messages
|
||||||
console.log('HomeView.vue: onMounted called');
|
await loadMoreMessages();
|
||||||
console.log('Auth state:', auth.isAuthenticated);
|
|
||||||
|
// Связываем гостевые сообщения (копируем из guest_messages в messages)
|
||||||
// Определяем язык
|
await api.post('/api/chat/link-guest-messages');
|
||||||
const cyrillicPattern = /[а-яА-ЯёЁ]/;
|
console.log('Guest messages linked to authenticated user');
|
||||||
userLanguage.value = cyrillicPattern.test(newMessage.value) ? 'ru' : 'en';
|
|
||||||
console.log('Detected language:', userLanguage.value);
|
// Перезагружаем сообщения, чтобы получить все, включая перенесенные
|
||||||
|
messages.value = [];
|
||||||
// Если пользователь уже аутентифицирован, загружаем историю
|
offset.value = 0;
|
||||||
if (auth.isAuthenticated && auth.userId) {
|
await loadMoreMessages();
|
||||||
console.log('User authenticated, loading chat history...');
|
|
||||||
await loadChatHistory();
|
await nextTick();
|
||||||
|
scrollToBottom();
|
||||||
|
} catch (linkError) {
|
||||||
|
console.error('Error linking guest messages:', linkError);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
messages.value = [];
|
||||||
|
offset.value = 0;
|
||||||
|
hasMoreMessages.value = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Наблюдатель за изменением состояния аутентификации
|
|
||||||
watch(() => auth.isAuthenticated, async (newValue, oldValue) => {
|
|
||||||
console.log('Auth state changed in HomeView:', newValue);
|
|
||||||
|
|
||||||
if (newValue && auth.userId) {
|
|
||||||
// Пользователь только что аутентифицировался
|
|
||||||
await loadChatHistory();
|
|
||||||
} else {
|
|
||||||
// Пользователь вышел из системы
|
|
||||||
messages.value = []; // Очищаем историю сообщений
|
|
||||||
hasMoreMessages.value = false; // Сбрасываем флаг наличия дополнительных сообщений
|
|
||||||
console.log('Chat history cleared after logout');
|
|
||||||
}
|
|
||||||
}, { immediate: true });
|
|
||||||
|
|
||||||
// Функция для подключения кошелька
|
// Функция для подключения кошелька
|
||||||
const handleWalletAuth = async () => {
|
const handleWalletAuth = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await connectWithWallet();
|
const result = await connectWithWallet();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log('Wallet auth result:', result);
|
// Сохраняем гостевые сообщения перед очисткой
|
||||||
|
const guestMessages = [...messages.value];
|
||||||
|
messages.value = [];
|
||||||
|
offset.value = 0;
|
||||||
|
hasMoreMessages.value = true;
|
||||||
|
|
||||||
// Обновляем состояние аутентификации
|
try {
|
||||||
auth.setAuth({
|
await api.post('/api/chat/link-guest-messages');
|
||||||
authenticated: true,
|
console.log('Guest messages linked to authenticated user');
|
||||||
isAuthenticated: true,
|
await loadMoreMessages();
|
||||||
userId: result.userId,
|
|
||||||
address: result.address,
|
const filteredGuestMessages = guestMessages
|
||||||
isAdmin: result.isAdmin,
|
.filter(msg => !msg.showAuthButtons)
|
||||||
authType: 'wallet'
|
.reverse();
|
||||||
});
|
messages.value = [...messages.value, ...filteredGuestMessages];
|
||||||
|
|
||||||
// Добавляем задержку для синхронизации сессии
|
await nextTick();
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
scrollToBottom();
|
||||||
|
} catch (linkError) {
|
||||||
// Загружаем историю чата
|
console.error('Error linking guest messages:', linkError);
|
||||||
await loadChatHistory();
|
}
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error connecting wallet:', error);
|
console.error('Error connecting wallet:', error);
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -295,7 +286,7 @@ const saveGuestMessagesToServer = async () => {
|
|||||||
|
|
||||||
// Отправляем каждое сообщение на сервер
|
// Отправляем каждое сообщение на сервер
|
||||||
for (const msg of userMessages) {
|
for (const msg of userMessages) {
|
||||||
await axios.post('/api/chat/message', {
|
await api.post('/api/chat/message', {
|
||||||
message: msg.content,
|
message: msg.content,
|
||||||
language: userLanguage.value
|
language: userLanguage.value
|
||||||
});
|
});
|
||||||
@@ -311,7 +302,7 @@ const saveGuestMessagesToServer = async () => {
|
|||||||
async function connectTelegram() {
|
async function connectTelegram() {
|
||||||
try {
|
try {
|
||||||
// Отправляем запрос на получение ссылки для авторизации через Telegram
|
// Отправляем запрос на получение ссылки для авторизации через Telegram
|
||||||
const response = await axios.get('/api/auth/telegram', {
|
const response = await api.get('/api/auth/telegram', {
|
||||||
withCredentials: true
|
withCredentials: true
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -376,7 +367,7 @@ async function requestEmailCode() {
|
|||||||
// Функция проверки кода
|
// Функция проверки кода
|
||||||
const verifyEmailCode = async () => {
|
const verifyEmailCode = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/api/auth/email/verify-code', {
|
const response = await api.post('/api/auth/email/verify-code', {
|
||||||
email: emailInput.value,
|
email: emailInput.value,
|
||||||
code: emailCode.value
|
code: emailCode.value
|
||||||
});
|
});
|
||||||
@@ -387,7 +378,7 @@ const verifyEmailCode = async () => {
|
|||||||
emailError.value = '';
|
emailError.value = '';
|
||||||
|
|
||||||
// Загружаем историю чата после успешной аутентификации
|
// Загружаем историю чата после успешной аутентификации
|
||||||
await loadChatHistory();
|
await loadMoreMessages();
|
||||||
} else {
|
} else {
|
||||||
emailError.value = response.data.error || 'Неверный код';
|
emailError.value = response.data.error || 'Неверный код';
|
||||||
}
|
}
|
||||||
@@ -404,12 +395,6 @@ function cancelEmailVerification() {
|
|||||||
emailErrorMessage.value = '';
|
emailErrorMessage.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавьте эту функцию в <script setup>
|
|
||||||
const formatAddress = (address) => {
|
|
||||||
if (!address) return '';
|
|
||||||
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Форматирование времени
|
// Форматирование времени
|
||||||
const formatTime = (timestamp) => {
|
const formatTime = (timestamp) => {
|
||||||
if (!timestamp) return '';
|
if (!timestamp) return '';
|
||||||
@@ -438,101 +423,73 @@ const formatTime = (timestamp) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Функция для отправки сообщения
|
// Функция для отправки сообщения
|
||||||
const handleMessage = async (messageText) => {
|
const handleMessage = async (text) => {
|
||||||
if (!messageText.trim() || isLoading.value) return;
|
|
||||||
|
|
||||||
console.log('Handling message:', messageText);
|
|
||||||
isLoading.value = true;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const messageContent = text.trim();
|
||||||
|
if (!messageContent) return;
|
||||||
|
|
||||||
|
newMessage.value = '';
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
if (!auth.isAuthenticated) {
|
if (!auth.isAuthenticated) {
|
||||||
await sendGuestMessage(messageText);
|
// Сохраняем в таблицу guest_messages
|
||||||
|
const response = await api.post('/api/chat/guest-message', {
|
||||||
|
message: messageContent,
|
||||||
|
language: userLanguage.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
const userMessage = {
|
||||||
|
id: response.data.messageId,
|
||||||
|
content: messageContent,
|
||||||
|
role: 'user',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
showAuthButtons: false
|
||||||
|
};
|
||||||
|
messages.value.push(userMessage);
|
||||||
|
|
||||||
|
messages.value.push({
|
||||||
|
id: Date.now() + 1,
|
||||||
|
content: 'Для получения ответа от ассистента, пожалуйста, авторизуйтесь одним из способов:',
|
||||||
|
role: 'assistant',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
showAuthButtons: true
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await sendMessage(messageText);
|
// Для авторизованного пользователя сохраняем в messages
|
||||||
|
const response = await api.post('/api/chat/message', {
|
||||||
|
message: messageContent,
|
||||||
|
language: userLanguage.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
const message = {
|
||||||
|
id: response.data.messageId,
|
||||||
|
content: messageContent,
|
||||||
|
role: 'user',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
hasResponse: true
|
||||||
|
};
|
||||||
|
messages.value.push(message);
|
||||||
|
|
||||||
|
const aiMessage = {
|
||||||
|
id: response.data.aiMessageId,
|
||||||
|
content: response.data.message,
|
||||||
|
role: 'assistant',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
messages.value.push(aiMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error handling message:', error);
|
console.error('Error sending message:', error);
|
||||||
messages.value.push({
|
messages.value.push({
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
content: 'Произошла ошибка при отправке сообщения.',
|
content: 'Произошла ошибка при отправке сообщения.',
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
} finally {
|
|
||||||
newMessage.value = '';
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Функция для отправки сообщения аутентифицированного пользователя
|
|
||||||
const sendMessage = async (messageText) => {
|
|
||||||
try {
|
|
||||||
const userMessage = {
|
|
||||||
id: Date.now(),
|
|
||||||
content: messageText,
|
|
||||||
role: 'user',
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
};
|
|
||||||
messages.value.push(userMessage);
|
|
||||||
|
|
||||||
const response = await axios.post('/api/chat/message', {
|
|
||||||
message: messageText,
|
|
||||||
language: userLanguage.value
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data.success) {
|
|
||||||
messages.value.push({
|
|
||||||
id: Date.now() + 1,
|
|
||||||
content: response.data.message,
|
|
||||||
role: 'assistant',
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error sending message:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Функция для отправки гостевого сообщения
|
|
||||||
const sendGuestMessage = async (messageText) => {
|
|
||||||
try {
|
|
||||||
// Добавляем сообщение пользователя
|
|
||||||
const userMessage = {
|
|
||||||
id: Date.now(),
|
|
||||||
content: messageText,
|
|
||||||
role: 'user',
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
showAuthButtons: false
|
|
||||||
};
|
|
||||||
messages.value.push(userMessage);
|
|
||||||
|
|
||||||
// Очищаем поле ввода
|
|
||||||
newMessage.value = '';
|
|
||||||
|
|
||||||
// Сохраняем сообщение на сервере без получения ответа от Ollama
|
|
||||||
await axios.post('/api/chat/guest-message', {
|
|
||||||
message: messageText,
|
|
||||||
language: userLanguage.value
|
|
||||||
});
|
|
||||||
|
|
||||||
// Добавляем сообщение с кнопками аутентификации
|
|
||||||
messages.value.push({
|
|
||||||
id: Date.now() + 1,
|
|
||||||
content: 'Для получения ответа, пожалуйста, авторизуйтесь одним из способов:',
|
|
||||||
role: 'assistant',
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
showAuthButtons: true
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error sending guest message:', error);
|
|
||||||
messages.value.push({
|
|
||||||
id: Date.now() + 2,
|
|
||||||
content: 'Произошла ошибка. Пожалуйста, попробуйте позже.',
|
|
||||||
role: 'assistant',
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
showAuthButtons: true
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
@@ -554,7 +511,7 @@ const handleEmailAuth = async () => {
|
|||||||
// Функция отправки email
|
// Функция отправки email
|
||||||
const submitEmail = async () => {
|
const submitEmail = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/api/auth/email/request', {
|
const response = await api.post('/api/auth/email/request', {
|
||||||
email: emailInput.value
|
email: emailInput.value
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -573,7 +530,7 @@ const submitEmail = async () => {
|
|||||||
// Функция верификации кода Telegram
|
// Функция верификации кода Telegram
|
||||||
const verifyTelegramCode = async () => {
|
const verifyTelegramCode = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/api/auth/telegram/verify', {
|
const response = await api.post('/api/auth/telegram/verify', {
|
||||||
code: telegramCode.value
|
code: telegramCode.value
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -602,7 +559,7 @@ const verifyTelegramCode = async () => {
|
|||||||
|
|
||||||
// Загружаем историю чата после небольшой задержки
|
// Загружаем историю чата после небольшой задержки
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await loadChatHistory();
|
await loadMoreMessages();
|
||||||
}, 100);
|
}, 100);
|
||||||
} else {
|
} else {
|
||||||
messages.value.push({
|
messages.value.push({
|
||||||
@@ -622,6 +579,43 @@ const verifyTelegramCode = async () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const disconnectWallet = async () => {
|
||||||
|
try {
|
||||||
|
await auth.disconnect();
|
||||||
|
messages.value = [];
|
||||||
|
offset.value = 0;
|
||||||
|
hasMoreMessages.value = true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error disconnecting wallet:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработка прокрутки
|
||||||
|
const handleScroll = async () => {
|
||||||
|
const element = messagesContainer.value;
|
||||||
|
if (
|
||||||
|
!isLoadingMore.value &&
|
||||||
|
hasMoreMessages.value &&
|
||||||
|
element.scrollTop === 0
|
||||||
|
) {
|
||||||
|
await loadMoreMessages();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Добавляем слушатель прокрутки
|
||||||
|
if (messagesContainer.value) {
|
||||||
|
messagesContainer.value.addEventListener('scroll', handleScroll);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
// Удаляем слушатель
|
||||||
|
if (messagesContainer.value) {
|
||||||
|
messagesContainer.value.removeEventListener('scroll', handleScroll);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -665,64 +659,49 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-header {
|
.chat-header {
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
background-color: #f8f9fa;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px 20px;
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
border-bottom: 1px solid #ccc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Адаптивный заголовок чата */
|
.wallet-info {
|
||||||
@media (max-width: 768px) {
|
|
||||||
.chat-header {
|
|
||||||
padding: 8px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-header h2 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 1rem;
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Адаптивная информация о пользователе */
|
.disconnect-btn {
|
||||||
@media (max-width: 768px) {
|
padding: 0.5rem 1rem;
|
||||||
.user-info {
|
background-color: #ff4444;
|
||||||
font-size: 0.7rem;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info span {
|
|
||||||
max-width: 120px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.logout-btn {
|
|
||||||
padding: 5px 10px;
|
|
||||||
background-color: #f44336;
|
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Адаптивная кнопка выхода */
|
.disconnect-btn:hover {
|
||||||
@media (max-width: 768px) {
|
background-color: #cc0000;
|
||||||
.logout-btn {
|
}
|
||||||
padding: 4px 8px;
|
|
||||||
font-size: 0.8rem;
|
.load-more {
|
||||||
}
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-messages {
|
.chat-messages {
|
||||||
@@ -915,32 +894,27 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.auth-btn {
|
.auth-btn {
|
||||||
display: flex;
|
padding: 8px 16px;
|
||||||
align-items: center;
|
border: none;
|
||||||
justify-content: center;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 1rem;
|
font-size: 14px;
|
||||||
border: none;
|
display: flex;
|
||||||
width: 100%;
|
align-items: center;
|
||||||
font-weight: 500;
|
gap: 8px;
|
||||||
transition: opacity 0.2s;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-btn:hover {
|
.wallet-btn {
|
||||||
opacity: 0.9;
|
background-color: #4a5568;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-btn:disabled {
|
.wallet-btn:hover {
|
||||||
opacity: 0.7;
|
background-color: #2d3748;
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-icon {
|
.auth-icon {
|
||||||
margin-right: 0.75rem;
|
font-size: 16px;
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.telegram-btn {
|
.telegram-btn {
|
||||||
@@ -1080,4 +1054,48 @@ h1 {
|
|||||||
background-color: #cbd5e0;
|
background-color: #cbd5e0;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wallet-section {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disconnect-btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background-color: #ff4444;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disconnect-btn:hover {
|
||||||
|
background-color: #cc0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history {
|
||||||
|
height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Добавим индикатор загрузки */
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,138 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="profile">
|
|
||||||
<h1>Профиль пользователя</h1>
|
|
||||||
|
|
||||||
<div class="profile-info">
|
|
||||||
<div class="profile-section">
|
|
||||||
<h2>Основная информация</h2>
|
|
||||||
<div v-if="loading">Загрузка...</div>
|
|
||||||
<div v-else-if="error">{{ error }}</div>
|
|
||||||
<div v-else>
|
|
||||||
<p><strong>ID:</strong> {{ profile.id }}</p>
|
|
||||||
<p><strong>Имя пользователя:</strong> {{ profile.username || 'Не указано' }}</p>
|
|
||||||
<p><strong>Роль:</strong> {{ profile.role === 'admin' ? 'Администратор' : 'Пользователь' }}</p>
|
|
||||||
<p><strong>Язык интерфейса:</strong>
|
|
||||||
<select v-model="selectedLanguage" @change="updateLanguage">
|
|
||||||
<option value="ru">Русский</option>
|
|
||||||
<option value="en">English</option>
|
|
||||||
</select>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="profile-section">
|
|
||||||
<h2>Связанные аккаунты</h2>
|
|
||||||
<LinkedAccounts />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="profile-section" v-if="isAdmin">
|
|
||||||
<h2>Управление ролями</h2>
|
|
||||||
<RoleManager />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { ref, onMounted, computed } from 'vue';
|
|
||||||
import axios from 'axios';
|
|
||||||
import LinkedAccounts from '../components/LinkedAccounts.vue';
|
|
||||||
import RoleManager from '../components/RoleManager.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
LinkedAccounts,
|
|
||||||
RoleManager
|
|
||||||
},
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
const profile = ref({});
|
|
||||||
const loading = ref(true);
|
|
||||||
const error = ref(null);
|
|
||||||
const selectedLanguage = ref('ru');
|
|
||||||
const isAdmin = ref(false);
|
|
||||||
|
|
||||||
// Загрузка профиля пользователя
|
|
||||||
const loadProfile = async () => {
|
|
||||||
try {
|
|
||||||
loading.value = true;
|
|
||||||
const response = await axios.get('/api/users/profile', {
|
|
||||||
withCredentials: true
|
|
||||||
});
|
|
||||||
profile.value = response.data;
|
|
||||||
selectedLanguage.value = response.data.preferred_language || 'ru';
|
|
||||||
isAdmin.value = response.data.role === 'admin';
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error loading profile:', err);
|
|
||||||
error.value = 'Ошибка при загрузке профиля';
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Обновление языка пользователя
|
|
||||||
const updateLanguage = async () => {
|
|
||||||
try {
|
|
||||||
await axios.post('/api/users/update-language', {
|
|
||||||
language: selectedLanguage.value
|
|
||||||
}, {
|
|
||||||
withCredentials: true
|
|
||||||
});
|
|
||||||
// Обновляем язык в профиле
|
|
||||||
profile.value.preferred_language = selectedLanguage.value;
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error updating language:', err);
|
|
||||||
error.value = 'Ошибка при обновлении языка';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await loadProfile();
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
profile,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
selectedLanguage,
|
|
||||||
isAdmin,
|
|
||||||
updateLanguage
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.profile {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-info {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.profile-info {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-section {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 5px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2 {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
padding: 5px;
|
|
||||||
border-radius: 3px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="token-access">
|
|
||||||
<h1>Настройка прав доступа</h1>
|
|
||||||
<div>
|
|
||||||
<label for="blockchain">Выберите блокчейн:</label>
|
|
||||||
<select v-model="selectedBlockchain" id="blockchain">
|
|
||||||
<option v-for="(blockchain, index) in blockchains" :key="index" :value="blockchain.value">
|
|
||||||
{{ blockchain.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<form @submit.prevent="checkTokenBalance">
|
|
||||||
<div>
|
|
||||||
<label for="contractAddress">Адрес смарт-контракта:</label>
|
|
||||||
<input v-model="contractAddress" id="contractAddress" placeholder="Введите адрес смарт-контракта" required />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="requiredAmount">Объем токенов:</label>
|
|
||||||
<input v-model="requiredAmount" type="number" id="requiredAmount" placeholder="Введите объем токенов" required />
|
|
||||||
</div>
|
|
||||||
<button type="submit">Проверить баланс</button>
|
|
||||||
</form>
|
|
||||||
<p v-if="message">{{ message }}</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
selectedBlockchain: 'mainnet', // Значение по умолчанию
|
|
||||||
blockchains: [
|
|
||||||
{ name: 'Ethereum', value: 'mainnet' },
|
|
||||||
{ name: 'Polygon', value: 'polygon' },
|
|
||||||
{ name: 'Binance Smart Chain', value: 'bsc' },
|
|
||||||
{ name: 'Arbitrum', value: 'arbitrum' }
|
|
||||||
],
|
|
||||||
contractAddress: '',
|
|
||||||
requiredAmount: '',
|
|
||||||
message: ''
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async checkTokenBalance() {
|
|
||||||
const response = await fetch(`/api/admin/check-balance`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
walletAddress: this.$store.state.walletAddress, // Предполагается, что адрес кошелька хранится в хранилище
|
|
||||||
contractAddress: this.contractAddress,
|
|
||||||
requiredAmount: this.requiredAmount,
|
|
||||||
blockchain: this.selectedBlockchain
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
this.message = data.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.token-access {
|
|
||||||
max-width: 400px;
|
|
||||||
margin: auto;
|
|
||||||
padding: 20px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 5px;
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@@ -38,11 +38,15 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
|
host: 'localhost',
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:8000',
|
target: 'http://localhost:8000',
|
||||||
changeOrigin: true
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
credentials: true,
|
||||||
|
rewrite: (path) => path
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user