Описание изменений
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 logger = require('./utils/logger');
|
||||
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 accessRoutes = require('./routes/access');
|
||||
const usersRoutes = require('./routes/users');
|
||||
const contractsRoutes = require('./routes/contracts');
|
||||
const rolesRoutes = require('./routes/roles');
|
||||
const identitiesRoutes = require('./routes/identities');
|
||||
// const conversationsRoutes = require('./routes/conversations');
|
||||
const messagesRoutes = require('./routes/messages');
|
||||
const chatRoutes = require('./routes/chat');
|
||||
const healthRoutes = require('./routes/health');
|
||||
const debugRoutes = require('./routes/debug');
|
||||
const adminRoutes = require('./routes/admin');
|
||||
|
||||
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({
|
||||
store: new pgSession({
|
||||
pool,
|
||||
tableName: 'session',
|
||||
createTableIfMissing: true,
|
||||
}),
|
||||
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
||||
secret: process.env.SESSION_SECRET || 'hb3atoken',
|
||||
name: 'sessionId',
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
cookie: {
|
||||
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 дней
|
||||
maxAge: 30 * 24 * 60 * 60 * 1000,
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
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.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({
|
||||
contentSecurityPolicy: false // Отключаем CSP для разработки
|
||||
@@ -79,34 +112,51 @@ app.use((req, res, 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
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/access', accessRoutes);
|
||||
app.use('/api/users', usersRoutes);
|
||||
app.use('/api/contracts', contractsRoutes);
|
||||
app.use('/api/roles', rolesRoutes);
|
||||
app.use('/api/identities', identitiesRoutes);
|
||||
// app.use('/api/conversations', conversationsRoutes);
|
||||
app.use('/api/messages', messagesRoutes);
|
||||
app.use('/api/chat', chatRoutes);
|
||||
app.use('/api/health', healthRoutes);
|
||||
|
||||
// Маршруты для отладки (только в режиме разработки)
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
app.use('/api/debug', debugRoutes);
|
||||
}
|
||||
app.use('/api/admin', adminRoutes);
|
||||
|
||||
const nonceStore = new Map(); // или любая другая реализация хранилища nonce
|
||||
|
||||
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 };
|
||||
|
||||
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() {
|
||||
try {
|
||||
// ... существующий код ...
|
||||
const pool = new Pool({
|
||||
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,
|
||||
port: process.env.DB_PORT || 5432,
|
||||
});
|
||||
|
||||
// Выполняем миграции
|
||||
await pool.query(createUsersTable);
|
||||
await pool.query(createSessionTable);
|
||||
await pool.query(createNoncesTable);
|
||||
await pool.query(createMessagesTable);
|
||||
await pool.query(createConversationsTable);
|
||||
await pool.query(createGuestMessagesTable);
|
||||
|
||||
// ... существующий код ...
|
||||
} catch (error) {
|
||||
// ... существующий код ...
|
||||
// Проверка подключения
|
||||
pool.query('SELECT NOW()', (err, res) => {
|
||||
if (err) {
|
||||
logger.error('Error connecting to database:', err);
|
||||
} else {
|
||||
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
|
||||
async function initRoles() {
|
||||
try {
|
||||
// Проверяем, существует ли таблица roles
|
||||
const tableExists = await db.query(`
|
||||
const tableExists = await pool.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'roles'
|
||||
@@ -11,7 +16,7 @@ async function initRoles() {
|
||||
|
||||
if (!tableExists.rows[0].exists) {
|
||||
// Создаем таблицу roles
|
||||
await db.query(`
|
||||
await pool.query(`
|
||||
CREATE TABLE roles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
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
|
||||
(3, 'user', 'Обычный пользователь'),
|
||||
(4, 'admin', 'Администратор с полным доступом');
|
||||
@@ -30,24 +35,24 @@ async function initRoles() {
|
||||
console.log('Таблица roles создана и заполнена');
|
||||
} else {
|
||||
// Проверяем наличие ролей
|
||||
const rolesExist = await db.query(`
|
||||
const rolesExist = await pool.query(`
|
||||
SELECT COUNT(*) FROM roles WHERE id IN (3, 4);
|
||||
`);
|
||||
|
||||
if (rolesExist.rows[0].count < 2) {
|
||||
// Добавляем недостающие роли
|
||||
const userRoleExists = await db.query(`SELECT EXISTS (SELECT FROM roles WHERE name = 'user');`);
|
||||
const adminRoleExists = await db.query(`SELECT EXISTS (SELECT FROM roles WHERE name = 'admin');`);
|
||||
const userRoleExists = await pool.query(`SELECT EXISTS (SELECT FROM roles WHERE name = 'user');`);
|
||||
const adminRoleExists = await pool.query(`SELECT EXISTS (SELECT FROM roles WHERE name = 'admin');`);
|
||||
|
||||
if (!userRoleExists.rows[0].exists) {
|
||||
await db.query(`
|
||||
await pool.query(`
|
||||
INSERT INTO roles (id, name, description) VALUES
|
||||
(3, 'user', 'Обычный пользователь');
|
||||
`);
|
||||
}
|
||||
|
||||
if (!adminRoleExists.rows[0].exists) {
|
||||
await db.query(`
|
||||
await pool.query(`
|
||||
INSERT INTO roles (id, name, description) VALUES
|
||||
(4, 'admin', 'Администратор с полным доступом');
|
||||
`);
|
||||
@@ -61,3 +66,54 @@ async function initRoles() {
|
||||
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) => {
|
||||
try {
|
||||
console.log('Session in requireAuth:', req.session);
|
||||
console.log('Cookies received:', req.headers.cookie);
|
||||
console.log('Authorization header:', req.headers.authorization);
|
||||
console.log('Session in requireAuth:', {
|
||||
id: req.sessionID,
|
||||
userId: req.session?.userId,
|
||||
authenticated: req.session?.authenticated
|
||||
});
|
||||
|
||||
// Проверяем сессию
|
||||
if (req.session?.authenticated && req.session?.userId) {
|
||||
// Обновляем время жизни сессии
|
||||
req.session.touch();
|
||||
|
||||
// Проверяем, что пользователь аутентифицирован через сессию
|
||||
if (req.session && req.session.authenticated && req.session.userId) {
|
||||
// Добавляем информацию о пользователе в запрос
|
||||
req.user = {
|
||||
userId: req.session.userId,
|
||||
address: req.session.address || null,
|
||||
email: req.session.email || null,
|
||||
telegramId: req.session.telegramId || null,
|
||||
isAdmin: req.session.isAdmin || false,
|
||||
authType: req.session.authType || 'unknown'
|
||||
address: req.session.address,
|
||||
isAdmin: req.session.isAdmin,
|
||||
authType: req.session.authType
|
||||
};
|
||||
return next();
|
||||
}
|
||||
|
||||
// Проверяем заголовок авторизации
|
||||
// Проверяем Bearer токен
|
||||
const authHeader = req.headers.authorization;
|
||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||
const token = authHeader.split(' ')[1];
|
||||
if (authHeader?.startsWith('Bearer ')) {
|
||||
const address = authHeader.split(' ')[1];
|
||||
|
||||
// Проверяем, это адрес кошелька или JWT-токен
|
||||
if (token.startsWith('0x')) {
|
||||
// Это адрес кошелька
|
||||
const address = token;
|
||||
console.log('Found address in Authorization header:', address);
|
||||
if (address.startsWith('0x')) {
|
||||
const result = await db.query(`
|
||||
SELECT u.id, u.is_admin
|
||||
FROM users u
|
||||
JOIN user_identities ui ON u.id = ui.user_id
|
||||
WHERE ui.identity_type = 'wallet'
|
||||
AND LOWER(ui.identity_value) = LOWER($1)
|
||||
`, [address]);
|
||||
|
||||
try {
|
||||
// Проверяем, существует ли пользователь с таким адресом
|
||||
const result = await db.query(`
|
||||
SELECT u.id, u.is_admin
|
||||
FROM users u
|
||||
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));
|
||||
|
||||
if (result.rows.length > 0) {
|
||||
const user = result.rows[0];
|
||||
req.user = {
|
||||
userId: user.id,
|
||||
address: address,
|
||||
isAdmin: user.is_admin,
|
||||
authType: 'wallet'
|
||||
};
|
||||
return next();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error finding user by address:', error);
|
||||
next();
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Здесь можно добавить логику проверки JWT, если используется
|
||||
}
|
||||
}
|
||||
|
||||
// Если пользователь не аутентифицирован, возвращаем ошибку
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
} 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' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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:fix": "eslint . --fix",
|
||||
"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": {
|
||||
"@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 router = express.Router();
|
||||
const db = require('../db');
|
||||
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 {
|
||||
const result = await db.query('SELECT * FROM users');
|
||||
res.json(result.rows);
|
||||
const roles = await authService.getAllRoles();
|
||||
res.json({ success: true, roles });
|
||||
} 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' });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { ethers } = require('ethers');
|
||||
const crypto = require('crypto');
|
||||
const db = require('../db');
|
||||
const logger = require('../utils/logger');
|
||||
@@ -8,11 +7,11 @@ const helmet = require('helmet');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const { checkRole, requireAuth } = require('../middleware/auth');
|
||||
const { pool } = require('../db');
|
||||
const { verifySignature, checkAccess, findOrCreateUser } = require('../utils/auth');
|
||||
const authService = require('../services/auth-service');
|
||||
const { SiweMessage } = require('siwe');
|
||||
const { sendEmail } = require('../services/emailBot');
|
||||
const { verificationCodes } = require('../services/telegramBot');
|
||||
const { checkTokensAndUpdateRole } = require('../services/auth-service');
|
||||
|
||||
// Создайте лимитер для попыток аутентификации
|
||||
const authLimiter = rateLimit({
|
||||
@@ -87,85 +86,64 @@ async function checkUserRole(address, req) {
|
||||
router.post('/verify', async (req, res) => {
|
||||
try {
|
||||
const { address, signature, message } = req.body;
|
||||
console.log('Received verification request:', { address, message });
|
||||
console.log('Signature:', signature);
|
||||
|
||||
console.log('Verify request: address=' + address + ', signature=' + signature.substring(0, 10) + '...');
|
||||
// Проверяем подпись через SIWE
|
||||
const siwe = new SiweMessage(message);
|
||||
console.log('Created SIWE message object');
|
||||
|
||||
// Получаем nonce из базы данных
|
||||
const nonceResult = await db.query(`
|
||||
SELECT nonce FROM nonces
|
||||
WHERE identity_value = $1 AND expires_at > NOW() AND used = false
|
||||
`, [address.toLowerCase()]);
|
||||
const fields = await siwe.verify({ signature });
|
||||
console.log('SIWE validation result:', fields);
|
||||
|
||||
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) {
|
||||
if (!fields || !fields.success) {
|
||||
console.log('SIWE validation failed');
|
||||
return res.status(401).json({ error: 'Invalid signature' });
|
||||
}
|
||||
|
||||
// Помечаем nonce как использованный
|
||||
await db.query(`
|
||||
UPDATE nonces
|
||||
SET used = true
|
||||
WHERE identity_value = $1
|
||||
`, [address.toLowerCase()]);
|
||||
// Проверяем nonce
|
||||
const nonceResult = await db.query(
|
||||
`SELECT nonce FROM nonces
|
||||
WHERE identity_value = $1
|
||||
AND expires_at > NOW()`,
|
||||
[address.toLowerCase()]
|
||||
);
|
||||
console.log('Nonce check result:', nonceResult.rows);
|
||||
|
||||
// Находим или создаем пользователя
|
||||
console.log('Finding or creating user for address:', address);
|
||||
const { userId, isAdmin } = await findOrCreateUser(address);
|
||||
console.log('User found/created:', { userId, isAdmin });
|
||||
|
||||
// Сохраняем 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;
|
||||
if (!nonceResult.rows.length) {
|
||||
console.log('Invalid or expired nonce');
|
||||
return res.status(401).json({ error: 'Invalid or expired nonce' });
|
||||
}
|
||||
|
||||
// Сохраняем сессию ПЕРЕД отправкой ответа
|
||||
await new Promise((resolve, reject) => {
|
||||
req.session.save(err => {
|
||||
if (err) {
|
||||
console.error('Error saving session:', err);
|
||||
reject(err);
|
||||
} else {
|
||||
console.log('Session saved successfully');
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
// Проверяем соответствие nonce
|
||||
if (nonceResult.rows[0].nonce !== fields.data.nonce) {
|
||||
console.log('Nonce mismatch');
|
||||
return res.status(401).json({ error: 'Invalid nonce' });
|
||||
}
|
||||
|
||||
console.log('Authentication successful for user:', {
|
||||
// Получаем или создаем пользователя
|
||||
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,
|
||||
guestId: currentGuestId
|
||||
authType: 'wallet',
|
||||
guestId: req.session.guestId
|
||||
});
|
||||
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({
|
||||
res.json({
|
||||
authenticated: true,
|
||||
userId,
|
||||
address,
|
||||
@@ -173,7 +151,7 @@ router.post('/verify', async (req, res) => {
|
||||
authType: 'wallet'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error during wallet verification:', error);
|
||||
console.error('Error:', 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 {
|
||||
// Получаем информацию о пользователе
|
||||
const userData = {
|
||||
address: req.session.address,
|
||||
isAdmin: req.session.isAdmin || false,
|
||||
roles: req.session.roles || [],
|
||||
authenticated: true,
|
||||
};
|
||||
const userId = req.session.userId;
|
||||
const address = req.session.address;
|
||||
|
||||
// Проверяем доступ к различным разделам
|
||||
const access = {
|
||||
dashboard: true, // Все аутентифицированные пользователи имеют доступ к панели управления
|
||||
admin: userData.isAdmin, // Только администраторы имеют доступ к админке
|
||||
contracts: userData.roles.includes('CONTRACT_MANAGER') || userData.isAdmin,
|
||||
users: userData.roles.includes('USER_MANAGER') || userData.isAdmin,
|
||||
};
|
||||
if (address) {
|
||||
const isAdmin = await checkTokensAndUpdateRole(address);
|
||||
|
||||
res.json({
|
||||
user: userData,
|
||||
access: access,
|
||||
// Обновляем сессию
|
||||
req.session.isAdmin = isAdmin;
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
isAdmin,
|
||||
userId,
|
||||
address
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
isAdmin: false,
|
||||
userId,
|
||||
address: null
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при проверке прав доступа:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
logger.error('Error checking access:', error);
|
||||
return res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { ChatOllama } = require('@langchain/ollama');
|
||||
const { getVectorStore } = require('../services/vectorStore');
|
||||
const aiAssistant = require('../services/ai-assistant');
|
||||
const db = require('../db');
|
||||
const { requireAuth, requireAdmin } = require('../middleware/auth');
|
||||
const logger = require('../utils/logger');
|
||||
@@ -105,7 +104,7 @@ async function processGuestMessages(userId, guestId) {
|
||||
|
||||
// Получаем ответ от AI
|
||||
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 в ту же беседу
|
||||
await db.query(
|
||||
@@ -128,54 +127,44 @@ async function processGuestMessages(userId, guestId) {
|
||||
|
||||
// Обработчик для гостевых сообщений
|
||||
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 {
|
||||
// Создаем запись в conversations для гостя
|
||||
const conversationResult = await db.query(
|
||||
`INSERT INTO conversations (created_at)
|
||||
VALUES (NOW())
|
||||
RETURNING id`
|
||||
const { message, language } = req.body;
|
||||
const guestId = req.session.guestId || crypto.randomBytes(16).toString('hex');
|
||||
|
||||
// Сохраняем ID гостя в сессии
|
||||
req.session.guestId = guestId;
|
||||
await req.session.save();
|
||||
|
||||
console.log('Saving guest message:', { guestId, message });
|
||||
|
||||
// Сохраняем сообщение пользователя
|
||||
const result = await db.query(
|
||||
'INSERT INTO guest_messages (guest_id, content, language, is_ai) VALUES ($1, $2, $3, false) RETURNING id',
|
||||
[guestId, message, language]
|
||||
);
|
||||
|
||||
const conversationId = conversationResult.rows[0].id;
|
||||
console.log('Guest message saved:', result.rows[0]);
|
||||
|
||||
// Создаем метаданные
|
||||
const metadata = {
|
||||
guest_id: req.session.guestId,
|
||||
language: language || 'en'
|
||||
};
|
||||
|
||||
// Сохраняем только сообщение пользователя
|
||||
await db.query(
|
||||
`INSERT INTO messages
|
||||
(conversation_id, sender_type, content, channel, metadata, created_at)
|
||||
VALUES ($1, 'guest', $2, 'chat', $3, NOW())`,
|
||||
[
|
||||
conversationId,
|
||||
message,
|
||||
JSON.stringify(metadata)
|
||||
]
|
||||
);
|
||||
|
||||
res.json({ success: true });
|
||||
res.json({
|
||||
success: true,
|
||||
messageId: result.rows[0].id
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error processing message:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
console.error('Error saving guest message:', error);
|
||||
res.status(500).json({ success: false, error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Маршрут для обычных сообщений (для аутентифицированных пользователей)
|
||||
router.post('/message', requireAuth, async (req, res) => {
|
||||
const { message, language } = req.body;
|
||||
const userId = req.session.userId;
|
||||
|
||||
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(
|
||||
`INSERT INTO conversations (user_id, created_at)
|
||||
@@ -189,20 +178,25 @@ router.post('/message', requireAuth, async (req, res) => {
|
||||
// Сохраняем сообщение пользователя
|
||||
await db.query(
|
||||
`INSERT INTO messages
|
||||
(conversation_id, sender_type, content, channel, created_at)
|
||||
VALUES ($1, 'user', $2, 'chat', NOW())`,
|
||||
[conversationId, message]
|
||||
(conversation_id, sender_type, content, channel, metadata, created_at)
|
||||
VALUES ($1, 'user', $2, 'chat', $3, NOW())`,
|
||||
[
|
||||
conversationId,
|
||||
message,
|
||||
JSON.stringify({ language: language || 'ru' })
|
||||
]
|
||||
);
|
||||
|
||||
// Получаем ответ от AI
|
||||
const aiResponse = await getAIResponse(message, language);
|
||||
|
||||
// Сохраняем ответ AI
|
||||
await db.query(
|
||||
`INSERT INTO messages
|
||||
(conversation_id, sender_type, content, channel, created_at)
|
||||
VALUES ($1, 'assistant', $2, 'chat', NOW())`,
|
||||
[conversationId, aiResponse]
|
||||
(conversation_id, sender_type, content, channel, metadata, created_at)
|
||||
VALUES ($1, 'assistant', $2, 'chat', $3, NOW())`,
|
||||
[
|
||||
conversationId,
|
||||
aiResponse,
|
||||
JSON.stringify({ language: language || 'ru' })
|
||||
]
|
||||
);
|
||||
|
||||
res.json({
|
||||
@@ -210,7 +204,7 @@ router.post('/message', requireAuth, async (req, res) => {
|
||||
message: aiResponse
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error processing message:', error);
|
||||
logger.error('Error processing message:', 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) => {
|
||||
const limit = parseInt(req.query.limit) || 2; // По умолчанию только последнее сообщение и ответ
|
||||
const offset = parseInt(req.query.offset) || 0;
|
||||
|
||||
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) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
// Получаем общее количество сообщений
|
||||
const countResult = await db.query(
|
||||
`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 limit = parseInt(req.query.limit) || 50;
|
||||
const offset = parseInt(req.query.offset) || 0;
|
||||
|
||||
// Получаем сообщения с пагинацией
|
||||
const result = await db.query(
|
||||
`SELECT m.id, m.content, m.sender_type as role, m.created_at,
|
||||
c.user_id, m.metadata
|
||||
`SELECT
|
||||
m.id,
|
||||
m.content,
|
||||
m.sender_type as role,
|
||||
m.created_at,
|
||||
c.user_id
|
||||
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')
|
||||
ORDER BY m.created_at DESC
|
||||
LIMIT $3 OFFSET $4`,
|
||||
[req.session.userId, req.session.guestId, limit, offset]
|
||||
LIMIT $2 OFFSET $3`,
|
||||
[req.session.userId, limit, offset]
|
||||
);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
messages: result.rows.reverse(),
|
||||
total
|
||||
messages: result.rows.reverse()
|
||||
});
|
||||
|
||||
} 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' });
|
||||
}
|
||||
});
|
||||
@@ -314,75 +305,41 @@ router.get('/admin/history', requireAdmin, async (req, res) => {
|
||||
// Обработчик для связывания гостевых сообщений с пользователем
|
||||
router.post('/link-guest-messages', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const userId = req.session.userId;
|
||||
const { userId } = req.session;
|
||||
const guestId = req.session.guestId;
|
||||
|
||||
console.log('Linking messages:', { userId, guestId });
|
||||
|
||||
if (!guestId) {
|
||||
console.log('No guestId in session');
|
||||
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]);
|
||||
// Проверяем наличие гостевых сообщений
|
||||
const guestMessages = await db.query(
|
||||
'SELECT EXISTS(SELECT 1 FROM guest_messages WHERE guest_id = $1)',
|
||||
[guestId]
|
||||
);
|
||||
|
||||
// Удаляем гостевые сообщения
|
||||
await db.query(`
|
||||
DELETE FROM guest_messages
|
||||
WHERE guest_id = $1
|
||||
`, [guestId]);
|
||||
console.log('Guest messages check:', guestMessages.rows[0]);
|
||||
|
||||
// Удаляем временный 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 { address, signature, message } = req.body;
|
||||
|
||||
try {
|
||||
// ... существующий код верификации ...
|
||||
|
||||
// После успешной верификации и создания пользователя
|
||||
if (req.session.guestId) {
|
||||
console.log('Found guest messages, processing...');
|
||||
await processGuestMessages(userId, req.session.guestId);
|
||||
if (!guestMessages.rows[0].exists) {
|
||||
console.log('No guest messages found for guestId:', guestId);
|
||||
return res.json({ success: true, message: 'No guest messages to link' });
|
||||
}
|
||||
|
||||
// Сохраняем данные в сессии
|
||||
req.session.userId = userId;
|
||||
req.session.address = address;
|
||||
req.session.isAdmin = isAdmin;
|
||||
req.session.authenticated = true;
|
||||
// Связываем сообщения
|
||||
console.log('Calling link_guest_messages function');
|
||||
await db.query('SELECT link_guest_messages($1, $2)', [userId, guestId]);
|
||||
|
||||
console.log('Authentication successful for user:', {
|
||||
userId,
|
||||
address,
|
||||
isAdmin,
|
||||
guestId: req.session.guestId
|
||||
});
|
||||
|
||||
res.json({
|
||||
authenticated: true,
|
||||
userId: userId,
|
||||
address: address,
|
||||
isAdmin: isAdmin,
|
||||
authType: 'wallet'
|
||||
});
|
||||
// Очищаем guestId из сессии после связывания
|
||||
delete req.session.guestId;
|
||||
|
||||
console.log('Messages linked successfully');
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error during wallet verification:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
console.error('Error linking guest messages:', 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;
|
||||
|
||||
@@ -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 router = express.Router();
|
||||
const { linkIdentity, getUserIdentities } = require('../utils/identity-linker');
|
||||
const db = require('../db');
|
||||
const { requireAuth } = require('../middleware/auth');
|
||||
const authService = require('../services/auth-service');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// Получение связанных идентификаторов пользователя
|
||||
// Получение всех идентификаторов пользователя
|
||||
router.get('/', requireAuth, async (req, res) => {
|
||||
try {
|
||||
// Получаем ID пользователя по Ethereum-адресу
|
||||
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' });
|
||||
}
|
||||
|
||||
const userId = result.rows[0].id;
|
||||
|
||||
// Получаем все идентификаторы пользователя
|
||||
const identities = await getUserIdentities(userId);
|
||||
|
||||
res.json({ identities });
|
||||
const userId = req.session.userId;
|
||||
const identities = await authService.getUserIdentities(userId);
|
||||
res.json({ success: true, identities });
|
||||
} catch (error) {
|
||||
console.error('Error getting user identities:', error);
|
||||
logger.error('Error getting identities:', 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 {
|
||||
const { type, value } = req.params;
|
||||
const { type, value } = req.body;
|
||||
const userId = req.session.userId;
|
||||
|
||||
// Получаем ID пользователя по Ethereum-адресу
|
||||
const result = await db.query('SELECT id FROM users WHERE address = $1', [
|
||||
req.session.address,
|
||||
]);
|
||||
await authService.linkIdentity(userId, type, value);
|
||||
|
||||
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;
|
||||
|
||||
// Удаляем идентификатор
|
||||
await db.query(
|
||||
'DELETE FROM user_identities WHERE user_id = $1 AND identity_type = $2 AND identity_value = $3',
|
||||
[userId, type, value]
|
||||
);
|
||||
|
||||
res.json({ success: true });
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Identity linked successfully',
|
||||
isAdmin: req.session.isAdmin
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting user identity:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
logger.error('Error linking identity:', 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');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
require('dotenv').config();
|
||||
|
||||
// Подключение к БД
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
|
||||
});
|
||||
const { pool } = require('../db');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
async function runMigrations() {
|
||||
try {
|
||||
@@ -18,39 +13,64 @@ async function runMigrations() {
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
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 appliedMigrations = rows.map((row) => row.name);
|
||||
const executedMigrations = new Set(rows.map(row => row.name));
|
||||
|
||||
// Получаем список файлов миграций
|
||||
const migrationsDir = path.join(__dirname, '../migrations');
|
||||
const migrationFiles = fs
|
||||
.readdirSync(migrationsDir)
|
||||
.filter((file) => file.endsWith('.sql'))
|
||||
.sort(); // Сортируем файлы по имени
|
||||
// Читаем файлы миграций
|
||||
const migrationsDir = path.join(__dirname, '../db/migrations');
|
||||
const files = await fs.readdir(migrationsDir);
|
||||
|
||||
// Применяем миграции, которые еще не были применены
|
||||
// Сортируем файлы по номеру
|
||||
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) {
|
||||
if (!appliedMigrations.includes(file)) {
|
||||
console.log(`Применение миграции: ${file}`);
|
||||
|
||||
// Читаем содержимое файла миграции
|
||||
if (!executedMigrations.has(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(sql);
|
||||
await pool.query('BEGIN');
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Записываем информацию о примененной миграции
|
||||
await pool.query('INSERT INTO migrations (name) VALUES ($1)', [file]);
|
||||
// Выполняем SQL-функции
|
||||
const functionsDir = path.join(migrationsDir, 'functions');
|
||||
if (await fs.stat(functionsDir).then(() => true).catch(() => false)) {
|
||||
const functionFiles = await fs.readdir(functionsDir);
|
||||
|
||||
console.log(`Миграция ${file} успешно применена`);
|
||||
} 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();
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const { SiweMessage, generateNonce } = require('siwe');
|
||||
const { ethers } = require('ethers');
|
||||
// const TelegramBotService = require('./services/telegramBot');
|
||||
const EmailBotService = require('./services/emailBot');
|
||||
const { initializeVectorStore } = require('./services/vectorStore');
|
||||
const session = require('express-session');
|
||||
const { app, nonceStore } = require('./app');
|
||||
const usersRouter = require('./routes/users');
|
||||
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 { 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 csrf = require('csurf');
|
||||
// const cookieParser = require('cookie-parser');
|
||||
const messagesRouter = require('./routes/messages');
|
||||
const sessionMiddleware = require('./middleware/session');
|
||||
|
||||
// Импорт сервисов
|
||||
const telegramService = require('./services/telegramBot');
|
||||
const TelegramBotService = require('./services/telegramBot');
|
||||
const pgSession = require('connect-pg-simple')(session);
|
||||
const authService = require('./services/auth-service');
|
||||
const logger = require('./utils/logger');
|
||||
|
||||
const PORT = process.env.PORT || 8000;
|
||||
|
||||
@@ -42,83 +22,21 @@ console.log('Переменная окружения PORT:', process.env.PORT);
|
||||
console.log('Используемый порт:', process.env.PORT || 8000);
|
||||
|
||||
// Инициализация сервисов
|
||||
let telegramBot;
|
||||
let emailBot;
|
||||
async function initServices() {
|
||||
try {
|
||||
console.log('Инициализация сервисов...');
|
||||
|
||||
// Проверяем, что библиотека ethers.js правильно импортирована
|
||||
console.log('Ethers.js version:', ethers.version);
|
||||
if (process.env.TELEGRAM_BOT_TOKEN) {
|
||||
const telegramBot = new TelegramBotService(process.env.TELEGRAM_BOT_TOKEN);
|
||||
global.telegramBot = telegramBot; // Сохраняем экземпляр глобально
|
||||
console.log('Telegram бот инициализирован');
|
||||
}
|
||||
|
||||
// Порядок middleware важен!
|
||||
// 1. CORS должен быть первым
|
||||
app.use(
|
||||
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' });
|
||||
console.log('Все сервисы успешно инициализированы');
|
||||
} catch (error) {
|
||||
console.error('Ошибка при инициализации сервисов:', error);
|
||||
}
|
||||
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({
|
||||
@@ -136,551 +54,34 @@ app.use(session({
|
||||
}
|
||||
}));
|
||||
|
||||
async function initServices() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Маршруты API
|
||||
app.use('/api/users', usersRouter);
|
||||
app.use('/api/auth', authRouter);
|
||||
app.use('/api/contracts', contractsRouter);
|
||||
app.use('/api/access', accessRouter);
|
||||
// app.use('/api/chat', chatRouter);
|
||||
// app.use('/api/debug', debugRoutes);
|
||||
app.use('/api/identities', identitiesRouter);
|
||||
app.use('/api/messages', messagesRouter);
|
||||
|
||||
// Добавьте простой эндпоинт для проверки состояния сервера
|
||||
// Эндпоинт для проверки состояния сервера
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Добавьте после настройки маршрутов
|
||||
app.post('/api/verify', async (req, res) => {
|
||||
// Запуск сервера
|
||||
app.listen(PORT, async () => {
|
||||
try {
|
||||
// Перенаправляем запрос на /api/auth/verify
|
||||
const { message, signature } = req.body;
|
||||
console.log('Перенаправление запроса на /api/auth/verify:', { message, signature });
|
||||
|
||||
// Проверяем наличие необходимых данных
|
||||
if (!message || !message.address || !signature) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Отсутствуют необходимые данные для верификации',
|
||||
});
|
||||
}
|
||||
|
||||
const address = message.address.toLowerCase();
|
||||
console.log('Адрес из сообщения:', address);
|
||||
|
||||
// Проверяем, является ли пользователь администратором
|
||||
const isAdmin = true; // Для примера всегда true
|
||||
|
||||
try {
|
||||
const siweMessage = new SiweMessage(message);
|
||||
const fields = await siweMessage.validate(signature);
|
||||
|
||||
if (fields.address.toLowerCase() !== address.toLowerCase()) {
|
||||
return res.status(401).json({ success: false, error: 'Invalid signature' });
|
||||
}
|
||||
|
||||
// Только после проверки устанавливаем сессию
|
||||
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,
|
||||
});
|
||||
});
|
||||
await initServices();
|
||||
console.log('Server is running on port', PORT);
|
||||
} catch (error) {
|
||||
console.error('Ошибка верификации:', 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);
|
||||
console.error('Error starting server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка сигналов завершения
|
||||
process.on('SIGINT', () => {
|
||||
console.log('Получен сигнал SIGINT, завершаем работу...');
|
||||
server.close(() => {
|
||||
console.log('Сервер остановлен');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('Получен сигнал SIGTERM, завершаем работу...');
|
||||
server.close(() => {
|
||||
console.log('Сервер остановлен');
|
||||
process.exit(0);
|
||||
});
|
||||
// Обработка ошибок
|
||||
process.on('unhandledRejection', (err) => {
|
||||
logger.error('Unhandled Rejection:', err);
|
||||
});
|
||||
|
||||
// Обработка необработанных исключений
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('Необработанное исключение:', error);
|
||||
// Не завершаем процесс, чтобы nodemon мог перезапустить сервер
|
||||
process.on('uncaughtException', (err) => {
|
||||
logger.error('Uncaught Exception:', err);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
module.exports = app;
|
||||
|
||||
@@ -1,158 +1,109 @@
|
||||
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
|
||||
const model = new ChatOllama({
|
||||
baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
|
||||
model: process.env.OLLAMA_MODEL || 'llama2',
|
||||
});
|
||||
|
||||
/**
|
||||
* Обработка сообщения пользователя и получение ответа от ИИ
|
||||
* @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 'Извините, произошла ошибка при обработке вашего сообщения. Пожалуйста, попробуйте еще раз позже.';
|
||||
class AIAssistant {
|
||||
constructor() {
|
||||
this.baseUrl = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
|
||||
this.defaultModel = process.env.OLLAMA_MODEL || 'mistral';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение информации о пользователе
|
||||
* @param {number} userId - ID пользователя
|
||||
* @returns {Promise<Object>} - Информация о пользователе
|
||||
*/
|
||||
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]
|
||||
);
|
||||
// Создание экземпляра ChatOllama с нужными параметрами
|
||||
createChat(language = 'ru') {
|
||||
const systemPrompt = language === 'ru'
|
||||
? 'Вы - полезный ассистент. Отвечайте на русском языке.'
|
||||
: 'You are a helpful assistant. Respond in English.';
|
||||
|
||||
if (userResult.rows.length === 0) {
|
||||
return { id: userId };
|
||||
return new ChatOllama({
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение истории диалога
|
||||
* @param {number} userId - ID пользователя
|
||||
* @param {number} limit - Максимальное количество сообщений
|
||||
* @returns {Promise<string>} - История диалога в текстовом формате
|
||||
*/
|
||||
async function getConversationHistory(userId, limit = 10) {
|
||||
try {
|
||||
// Получение последнего активного диалога пользователя
|
||||
const conversationResult = await pool.query(
|
||||
`SELECT id FROM conversations
|
||||
WHERE user_id = $1
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 1`,
|
||||
[userId]
|
||||
);
|
||||
// Альтернативный метод запроса через прямой API
|
||||
async fallbackRequest(message, language) {
|
||||
try {
|
||||
logger.info('Using fallback request method');
|
||||
|
||||
if (conversationResult.rows.length === 0) {
|
||||
return '';
|
||||
const systemPrompt = language === 'ru'
|
||||
? 'Вы - полезный ассистент. Отвечайте на русском языке.'
|
||||
: 'You are a helpful assistant. Respond in English.';
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/api/generate`, {
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
|
||||
// Получение последних сообщений из диалога
|
||||
const messagesResult = await pool.query(
|
||||
`SELECT sender_type, content, created_at
|
||||
FROM messages
|
||||
WHERE conversation_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2`,
|
||||
[conversationId, limit]
|
||||
);
|
||||
// Добавляем методы из vectorStore.js
|
||||
async initVectorStore() {
|
||||
// ... код инициализации ...
|
||||
}
|
||||
|
||||
// Формирование истории в текстовом формате
|
||||
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 '';
|
||||
async findSimilarDocuments(query, k = 3) {
|
||||
// ... код поиска документов ...
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
processMessage,
|
||||
getUserInfo,
|
||||
getConversationHistory,
|
||||
};
|
||||
// Создаем и экспортируем единственный экземпляр
|
||||
const aiAssistant = new AIAssistant();
|
||||
module.exports = aiAssistant;
|
||||
|
||||
@@ -1,69 +1,54 @@
|
||||
const db = require('../db');
|
||||
const logger = require('../utils/logger');
|
||||
const { ethers } = require('ethers');
|
||||
const crypto = require('crypto');
|
||||
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 {
|
||||
/**
|
||||
* Проверяет наличие токенов на кошельке и обновляет роль
|
||||
* @param {string} walletAddress - Адрес кошелька
|
||||
* @returns {Promise<boolean>} - Имеет ли пользователь права администратора
|
||||
*/
|
||||
async checkTokensAndUpdateRole(walletAddress) {
|
||||
try {
|
||||
// Получаем ID пользователя по адресу кошелька
|
||||
const userResult = await db.query(`
|
||||
SELECT u.id FROM users u
|
||||
JOIN user_identities ui ON u.id = ui.user_id
|
||||
WHERE ui.identity_type = 'wallet' AND ui.identity_value = $1
|
||||
`, [walletAddress]);
|
||||
constructor() {
|
||||
// Инициализация провайдеров для разных сетей
|
||||
this.providers = {
|
||||
eth: new ethers.JsonRpcProvider(process.env.RPC_URL_ETH),
|
||||
polygon: new ethers.JsonRpcProvider(process.env.RPC_URL_POLYGON),
|
||||
bsc: new ethers.JsonRpcProvider(process.env.RPC_URL_BSC),
|
||||
arbitrum: new ethers.JsonRpcProvider(process.env.RPC_URL_ARBITRUM)
|
||||
};
|
||||
|
||||
if (userResult.rows.length === 0) {
|
||||
logger.warn(`User with wallet ${walletAddress} not found`);
|
||||
// Конфигурация токенов для разных сетей
|
||||
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 {
|
||||
logger.info('Verifying signature:', {
|
||||
message: message.substring(0, 100) + '...',
|
||||
signature: signature.substring(0, 10) + '...',
|
||||
address
|
||||
});
|
||||
|
||||
if (!message || !signature || !address) {
|
||||
logger.error('Missing parameters for signature verification');
|
||||
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;
|
||||
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;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error checking tokens: ${error.message}`);
|
||||
logger.error('Error in verifySignature:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -75,80 +60,195 @@ class AuthService {
|
||||
*/
|
||||
async checkAdminTokens(walletAddress) {
|
||||
try {
|
||||
const 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) {
|
||||
for (const contract of this.tokenContracts) {
|
||||
try {
|
||||
const provider = providers[contract.network];
|
||||
if (!provider) {
|
||||
logger.warn(`Provider not found for network: ${contract.network}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Проверка доступности провайдера
|
||||
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 provider = this.providers[contract.network];
|
||||
const tokenContract = new ethers.Contract(
|
||||
contract.address,
|
||||
['function balanceOf(address) view returns (uint256)'],
|
||||
provider
|
||||
);
|
||||
|
||||
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}`);
|
||||
return true; // Если найден хотя бы один токен, возвращаем true
|
||||
return true;
|
||||
}
|
||||
} 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}`);
|
||||
return false; // Если не найдено ни одного токена, возвращаем false
|
||||
return false;
|
||||
} catch (error) {
|
||||
logger.error(`Error in checkAdminTokens: ${error.message}`);
|
||||
logger.error('Error in checkAdminTokens:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет роль пользователя в базе данных
|
||||
* @param {number} userId - ID пользователя
|
||||
* @param {string} role - Новая роль ('admin' или 'user')
|
||||
* @returns {Promise<boolean>} - Успешно ли обновлена роль
|
||||
* Проверяет баланс токенов и обновляет роль пользователя
|
||||
* @param {string} address - Адрес кошелька
|
||||
* @returns {Promise<boolean>} - Является ли пользователь админом
|
||||
*/
|
||||
async updateUserRole(userId, role) {
|
||||
async checkTokensAndUpdateRole(address) {
|
||||
try {
|
||||
// Получаем ID роли
|
||||
const roleResult = await db.query('SELECT id FROM roles WHERE name = $1', [role]);
|
||||
const isAdmin = await this.checkAdminTokens(address);
|
||||
|
||||
if (roleResult.rows.length === 0) {
|
||||
logger.error(`Role ${role} not found`);
|
||||
return false;
|
||||
// Обновляем роль в базе данных
|
||||
await this.updateUserRole(address, isAdmin);
|
||||
|
||||
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',
|
||||
[]
|
||||
);
|
||||
const userId = result.rows[0].id;
|
||||
|
||||
// Обновляем роль пользователя
|
||||
await db.query('UPDATE users SET role_id = $1 WHERE id = $2', [roleId, userId]);
|
||||
// Добавляем 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);
|
||||
}
|
||||
|
||||
logger.info(`Updated role for user ${userId} to ${role}`);
|
||||
return true;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
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 logger = require('../utils/logger');
|
||||
const { pool } = require('../db');
|
||||
const crypto = require('crypto');
|
||||
|
||||
// Создаем бота
|
||||
const token = process.env.TELEGRAM_BOT_TOKEN;
|
||||
let bot = null;
|
||||
|
||||
/**
|
||||
* Функция для отправки кода подтверждения
|
||||
*/
|
||||
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
|
||||
}
|
||||
},
|
||||
class TelegramBotService {
|
||||
constructor(token) {
|
||||
this.bot = new TelegramBot(token, {
|
||||
polling: true,
|
||||
request: {
|
||||
timeout: 30000, // увеличиваем таймаут до 30 секунд
|
||||
proxy: process.env.HTTPS_PROXY // используем прокси если есть
|
||||
timeout: 30000 // 30 секунд таймаут
|
||||
}
|
||||
});
|
||||
this.verificationCodes = new Map();
|
||||
this.setupHandlers();
|
||||
|
||||
console.log('Telegram bot initialized');
|
||||
logger.info('TelegramBot service initialized');
|
||||
}
|
||||
|
||||
// Очищаем все предыдущие обработчики
|
||||
bot.removeAllListeners();
|
||||
setupHandlers() {
|
||||
this.bot.on('message', this.handleMessage.bind(this));
|
||||
this.bot.on('callback_query', this.handleCallbackQuery.bind(this));
|
||||
|
||||
// Устанавливаем команды бота с обработкой ошибок
|
||||
bot.setMyCommands([
|
||||
{ command: '/start', description: 'Получить код подтверждения' },
|
||||
{ command: '/help', description: 'Показать справку' }
|
||||
]).catch(error => {
|
||||
console.warn('Error setting bot commands:', error);
|
||||
// Продолжаем работу даже если не удалось установить команды
|
||||
// Обработка ошибок
|
||||
this.bot.on('polling_error', (error) => {
|
||||
logger.error('Telegram polling error:', error);
|
||||
});
|
||||
|
||||
// Обработчик команды /start
|
||||
bot.onText(/\/start/, async (msg) => {
|
||||
this.bot.on('error', (error) => {
|
||||
logger.error('Telegram bot error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
async handleMessage(msg) {
|
||||
try {
|
||||
const chatId = msg.chat.id;
|
||||
try {
|
||||
await sendVerificationCode(chatId);
|
||||
} catch (error) {
|
||||
console.error('Error handling /start:', error);
|
||||
await bot.sendMessage(chatId, 'Произошла ошибка. Пожалуйста, попробуйте позже.')
|
||||
.catch(err => console.error('Error sending error message:', err));
|
||||
const text = msg.text;
|
||||
|
||||
logger.info(`Received message from ${chatId}: ${text}`);
|
||||
|
||||
if (text.startsWith('/start')) {
|
||||
await this.handleStart(msg);
|
||||
} else if (this.verificationCodes.has(chatId)) {
|
||||
await this.handleVerificationCode(msg);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error handling message:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Обработчик ошибок polling
|
||||
bot.on('polling_error', (error) => {
|
||||
console.error('Telegram bot polling error:', error);
|
||||
// Перезапускаем polling при ошибке
|
||||
setTimeout(() => {
|
||||
try {
|
||||
bot.startPolling();
|
||||
} catch (e) {
|
||||
console.error('Error restarting polling:', e);
|
||||
async handleCallbackQuery(query) {
|
||||
try {
|
||||
const chatId = query.message.chat.id;
|
||||
await this.bot.answerCallbackQuery(query.id);
|
||||
|
||||
logger.info(`Handled callback query from ${chatId}`);
|
||||
} catch (error) {
|
||||
logger.error('Error handling callback query:', error);
|
||||
}
|
||||
}
|
||||
|
||||
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 секунд
|
||||
});
|
||||
|
||||
// Обработчик остановки polling
|
||||
bot.on('stop', () => {
|
||||
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;
|
||||
}
|
||||
return { success: false, error: 'Неверный код' };
|
||||
} catch (error) {
|
||||
logger.error('Error verifying code:', error);
|
||||
return { success: false, error: 'Внутренняя ошибка' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Экспортируем функции
|
||||
module.exports = {
|
||||
initTelegramBot,
|
||||
verifyCode,
|
||||
sendVerificationCode
|
||||
};
|
||||
module.exports = TelegramBotService;
|
||||
@@ -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 { useAuthStore } from './stores/auth';
|
||||
import { useRouter } from 'vue-router';
|
||||
import axios from 'axios';
|
||||
|
||||
console.log('App.vue: Version with auth check loaded');
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const router = useRouter();
|
||||
|
||||
onMounted(async () => {
|
||||
console.log('App.vue: onMounted - checking auth');
|
||||
|
||||
async function checkAuth() {
|
||||
try {
|
||||
// Проверяем аутентификацию на сервере
|
||||
const result = await authStore.checkAuth();
|
||||
console.log('Auth check result:', result.authenticated);
|
||||
const response = await axios.get('/api/auth/check');
|
||||
|
||||
if (result.authenticated) {
|
||||
// Если пользователь аутентифицирован, восстанавливаем состояние
|
||||
console.log('Session restored from server');
|
||||
|
||||
// Загружаем историю чата, если мы на странице чата
|
||||
if (router.currentRoute.value.name === 'home') {
|
||||
console.log('Loading chat history after session restore');
|
||||
// Здесь можно вызвать метод для загрузки истории чата
|
||||
}
|
||||
if (response.data.authenticated) {
|
||||
authStore.setAuth(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking auth:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(checkAuth);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -2,43 +2,30 @@ import axios from 'axios';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
|
||||
// Создаем экземпляр axios с базовым URL
|
||||
const instance = axios.create({
|
||||
baseURL: '/',
|
||||
const api = axios.create({
|
||||
baseURL: '', // Убираем baseURL
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
// Добавляем перехватчик для добавления заголовка авторизации
|
||||
instance.interceptors.request.use(
|
||||
// Перехватчик запросов
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
console.log('Axios interceptor running');
|
||||
config.withCredentials = true; // Важно для каждого запроса
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// Логируем параметры запроса
|
||||
console.log('Request parameters:', config);
|
||||
|
||||
// Если уже есть заголовок Authorization, не перезаписываем его
|
||||
if (config.headers.Authorization) {
|
||||
return config;
|
||||
}
|
||||
|
||||
// Если пользователь аутентифицирован и есть адрес кошелька
|
||||
if (authStore.isAuthenticated && authStore.address) {
|
||||
console.log('Adding Authorization header:', `Bearer ${authStore.address}`);
|
||||
config.headers.Authorization = `Bearer ${authStore.address}`;
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
// Добавляем перехватчик для обработки ответов
|
||||
instance.interceptors.response.use(
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
console.log('Response from server:', response.data);
|
||||
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>
|
||||
<div class="email-connect">
|
||||
<p>Подключите свой email для быстрой авторизации.</p>
|
||||
|
||||
<div class="email-form">
|
||||
<div class="email-connection">
|
||||
<div v-if="!showVerification">
|
||||
<input
|
||||
type="email"
|
||||
v-model="email"
|
||||
placeholder="Введите ваш email"
|
||||
:disabled="loading || verificationSent"
|
||||
type="email"
|
||||
placeholder="Введите email"
|
||||
class="email-input"
|
||||
/>
|
||||
<button
|
||||
@click="sendVerification"
|
||||
class="connect-button"
|
||||
:disabled="!isValidEmail || loading || verificationSent"
|
||||
@click="requestCode"
|
||||
:disabled="isLoading || !isValidEmail"
|
||||
class="email-btn"
|
||||
>
|
||||
<span class="email-icon">✉️</span> {{ verificationSent ? 'Код отправлен' : 'Отправить код' }}
|
||||
Получить код
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="verificationSent" class="verification-form">
|
||||
<div v-else>
|
||||
<input
|
||||
v-model="code"
|
||||
type="text"
|
||||
v-model="verificationCode"
|
||||
placeholder="Введите код подтверждения"
|
||||
:disabled="loading"
|
||||
placeholder="Введите код"
|
||||
class="code-input"
|
||||
/>
|
||||
<button
|
||||
@click="verifyEmail"
|
||||
class="verify-button"
|
||||
:disabled="!verificationCode || loading"
|
||||
@click="verifyCode"
|
||||
:disabled="isLoading"
|
||||
class="verify-btn"
|
||||
>
|
||||
Подтвердить
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading">Загрузка...</div>
|
||||
<div v-if="error" class="error">{{ error }}</div>
|
||||
<div v-if="success" class="success">{{ success }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import axios from 'axios';
|
||||
|
||||
const email = ref('');
|
||||
const verificationCode = ref('');
|
||||
const loading = ref(false);
|
||||
const error = ref('');
|
||||
const success = ref('');
|
||||
const verificationSent = ref(false);
|
||||
|
||||
const isValidEmail = computed(() => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email.value);
|
||||
const props = defineProps({
|
||||
onEmailAuth: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
async function sendVerification() {
|
||||
if (!isValidEmail.value) return;
|
||||
const email = ref('');
|
||||
const code = ref('');
|
||||
const error = ref('');
|
||||
const isLoading = ref(false);
|
||||
const showVerification = ref(false);
|
||||
|
||||
const isValidEmail = computed(() => {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value);
|
||||
});
|
||||
|
||||
const requestCode = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
isLoading.value = true;
|
||||
await props.onEmailAuth(email.value);
|
||||
showVerification.value = true;
|
||||
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 = 'Ошибка при отправке кода подтверждения';
|
||||
error.value = err.message || 'Ошибка отправки кода';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyEmail() {
|
||||
if (!verificationCode.value) return;
|
||||
};
|
||||
|
||||
const verifyCode = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
isLoading.value = true;
|
||||
await props.onEmailAuth(email.value, code.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) {
|
||||
console.error('Error verifying email:', err);
|
||||
error.value = 'Ошибка при проверке кода подтверждения';
|
||||
error.value = err.message || 'Неверный код';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.email-connect {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
.email-connection {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.email-form, .verification-form {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
.email-input,
|
||||
.code-input {
|
||||
padding: 8px;
|
||||
margin-right: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.connect-button, .verify-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 15px;
|
||||
background-color: #4caf50;
|
||||
.email-btn,
|
||||
.verify-btn {
|
||||
padding: 10px 20px;
|
||||
background-color: #48bb78;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
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;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
color: #e53e3e;
|
||||
margin-top: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
button:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</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',
|
||||
component: HomeView
|
||||
}
|
||||
// Другие маршруты можно добавить позже, когда будут созданы соответствующие компоненты
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
@@ -32,11 +31,10 @@ router.beforeEach(async (to, from, next) => {
|
||||
// Проверяем аутентификацию, если маршрут требует авторизации
|
||||
if (to.matched.some(record => record.meta.requiresAuth)) {
|
||||
if (!authStore.isAuthenticated) {
|
||||
// Если пользователь не авторизован, перенаправляем на главную
|
||||
return next({ name: 'home' });
|
||||
}
|
||||
|
||||
// Проверяем права администратора, если маршрут требует прав администратора
|
||||
// Проверяем права администратора
|
||||
if (to.matched.some(record => record.meta.requiresAdmin) && !authStore.isAdmin) {
|
||||
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 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', {
|
||||
state: () => {
|
||||
const savedState = loadAuthState();
|
||||
return {
|
||||
user: null,
|
||||
isAuthenticated: savedState?.isAuthenticated || false,
|
||||
isAdmin: savedState?.isAdmin || false,
|
||||
authType: savedState?.authType || null,
|
||||
identities: {},
|
||||
loading: false,
|
||||
error: null,
|
||||
messages: [],
|
||||
address: null,
|
||||
wallet: null,
|
||||
telegramId: savedState?.telegramId || null,
|
||||
email: null,
|
||||
userId: savedState?.userId || null
|
||||
};
|
||||
},
|
||||
state: () => ({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
isAdmin: false,
|
||||
authType: null,
|
||||
identities: {},
|
||||
loading: false,
|
||||
error: null,
|
||||
messages: [],
|
||||
address: null,
|
||||
wallet: null,
|
||||
telegramId: null,
|
||||
email: null,
|
||||
userId: null
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async connectWallet(address, signature, message) {
|
||||
@@ -442,24 +427,22 @@ export const useAuthStore = defineStore('auth', {
|
||||
}
|
||||
},
|
||||
|
||||
async disconnect(router) {
|
||||
// Проверяем, действительно ли нужно выходить
|
||||
if (!this.isAuthenticated) {
|
||||
console.log('Already logged out, skipping disconnect');
|
||||
return;
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
try {
|
||||
// Сначала пробуем очистить сессию на сервере
|
||||
// Очищаем сессию на сервере
|
||||
await axios.post('/api/auth/clear-session');
|
||||
await axios.post('/api/auth/logout');
|
||||
|
||||
// Очищаем состояние только после успешного выхода
|
||||
this.clearState();
|
||||
// Очищаем состояние
|
||||
this.isAuthenticated = false;
|
||||
this.userId = null;
|
||||
this.address = null;
|
||||
this.isAdmin = false;
|
||||
this.authType = null;
|
||||
|
||||
if (router) router.push('/');
|
||||
// Очищаем локальное хранилище
|
||||
localStorage.removeItem('auth');
|
||||
} 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) {
|
||||
console.log('Setting auth state:', authData);
|
||||
|
||||
// Обновляем все поля состояния
|
||||
// Обновляем только состояние в памяти
|
||||
this.isAuthenticated = authData.authenticated || authData.isAuthenticated;
|
||||
this.user = {
|
||||
id: authData.userId,
|
||||
address: authData.address
|
||||
};
|
||||
this.userId = authData.userId;
|
||||
this.isAdmin = authData.isAdmin;
|
||||
this.authType = authData.authType;
|
||||
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:', {
|
||||
isAuthenticated: this.isAuthenticated,
|
||||
userId: this.userId,
|
||||
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,24 +5,34 @@
|
||||
<div class="auth-section" v-if="!auth.isAuthenticated">
|
||||
<h3>Венчурный фонд и поставщик программного обеспечения</h3>
|
||||
</div>
|
||||
|
||||
<div class="chat-container">
|
||||
<div class="chat-header">
|
||||
<WalletConnection
|
||||
:onWalletAuth="handleWalletAuth"
|
||||
:isAuthenticated="auth.isAuthenticated"
|
||||
/>
|
||||
<div class="user-info" v-if="auth.isAuthenticated">
|
||||
<!-- Используем тот же компонент, что и в сообщениях -->
|
||||
<div v-if="!auth.isAuthenticated" class="auth-buttons">
|
||||
<button class="auth-btn wallet-btn" @click="handleWalletAuth">
|
||||
<span class="auth-icon">👛</span> Подключить кошелек
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="wallet-info">
|
||||
<span>{{ truncateAddress(auth.address) }}</span>
|
||||
<button class="disconnect-btn" @click="disconnectWallet">
|
||||
Отключить кошелек
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Кнопка загрузки предыдущих сообщений -->
|
||||
<div v-if="hasMoreMessages" class="load-more-container">
|
||||
<button @click="loadMoreMessages" class="load-more-btn" :disabled="isLoadingMore">
|
||||
<div v-if="auth.isAuthenticated && hasMoreMessages" class="load-more">
|
||||
<button @click="loadMoreMessages" :disabled="isLoadingMore">
|
||||
{{ isLoadingMore ? 'Загрузка...' : 'Показать предыдущие сообщения' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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 class="message-content">
|
||||
{{ message.content }}
|
||||
@@ -39,32 +49,32 @@
|
||||
<button class="auth-btn email-btn" @click="handleEmailAuth">
|
||||
<span class="auth-icon">✉️</span> Подключить Email
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email форма -->
|
||||
<div v-if="showEmailForm" class="auth-form">
|
||||
<input
|
||||
<input
|
||||
v-model="emailInput"
|
||||
type="email"
|
||||
placeholder="Введите ваш email"
|
||||
type="email"
|
||||
placeholder="Введите ваш email"
|
||||
class="auth-input"
|
||||
/>
|
||||
/>
|
||||
<button @click="submitEmail" class="auth-btn">
|
||||
Отправить код
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Форма верификации email -->
|
||||
<div v-if="showEmailVerification" class="auth-form">
|
||||
<input
|
||||
<input
|
||||
v-model="emailCode"
|
||||
type="text"
|
||||
type="text"
|
||||
placeholder="Введите код из email"
|
||||
class="auth-input"
|
||||
/>
|
||||
/>
|
||||
<button @click="verifyEmailCode" class="auth-btn">
|
||||
Подтвердить
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Telegram верификация -->
|
||||
@@ -77,8 +87,8 @@
|
||||
/>
|
||||
<button @click="verifyTelegramCode" class="auth-btn">
|
||||
Подтвердить
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="emailError" class="error-message">
|
||||
{{ emailError }}
|
||||
@@ -106,17 +116,18 @@
|
||||
</template>
|
||||
|
||||
<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 WalletConnection from '../components/WalletConnection.vue';
|
||||
import TelegramConnect from '../components/TelegramConnect.vue';
|
||||
import axios from '../api/axios';
|
||||
import { connectWithWallet } from '../utils/wallet';
|
||||
import WalletConnection from '../components/identity/WalletConnection.vue';
|
||||
import TelegramConnect from '../components/identity/TelegramConnect.vue';
|
||||
import api from '../api/axios';
|
||||
import { connectWithWallet } from '../services/wallet';
|
||||
|
||||
console.log('HomeView.vue: Version with chat loaded');
|
||||
|
||||
const auth = useAuthStore();
|
||||
const messages = ref([]);
|
||||
const guestMessages = ref([]);
|
||||
const newMessage = ref('');
|
||||
const isLoading = ref(false);
|
||||
const messagesContainer = ref(null);
|
||||
@@ -124,7 +135,6 @@ const userLanguage = ref('ru');
|
||||
const email = ref('');
|
||||
const isValidEmail = ref(true);
|
||||
const hasShownAuthMessage = ref(false);
|
||||
const guestMessages = ref([]);
|
||||
const hasShownAuthOptions = ref(false);
|
||||
|
||||
// Email аутентификация
|
||||
@@ -144,8 +154,10 @@ const emailError = ref('');
|
||||
const PAGE_SIZE = 2; // Показываем только последнее сообщение и ответ
|
||||
const allMessages = ref([]); // Все загруженные сообщения
|
||||
const currentPage = ref(1); // Текущая страница
|
||||
const hasMoreMessages = ref(false); // Есть ли еще сообщения
|
||||
const hasMoreMessages = ref(true); // Есть ли еще сообщения
|
||||
const isLoadingMore = ref(false); // Загружаются ли дополнительные сообщения
|
||||
const offset = ref(0);
|
||||
const limit = ref(20);
|
||||
|
||||
// Вычисляемое свойство для отображаемых сообщений
|
||||
const displayedMessages = computed(() => {
|
||||
@@ -153,48 +165,30 @@ const displayedMessages = computed(() => {
|
||||
return allMessages.value.slice(startIndex);
|
||||
});
|
||||
|
||||
// Функция загрузки истории чата
|
||||
const loadChatHistory = async () => {
|
||||
try {
|
||||
if (!auth.isAuthenticated || !auth.userId) {
|
||||
return;
|
||||
}
|
||||
// Функция для сокращения адреса кошелька
|
||||
const truncateAddress = (address) => {
|
||||
if (!address) return '';
|
||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||
};
|
||||
|
||||
const response = await axios.get('/api/chat/history', {
|
||||
headers: { Authorization: `Bearer ${auth.address}` },
|
||||
params: { limit: PAGE_SIZE, offset: 0 }
|
||||
});
|
||||
|
||||
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 scrollToBottom = () => {
|
||||
if (messagesContainer.value) {
|
||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
// Функция загрузки дополнительных сообщений
|
||||
// Загрузка сообщений
|
||||
const loadMoreMessages = async () => {
|
||||
if (isLoadingMore.value) return;
|
||||
if (!auth.isAuthenticated) return;
|
||||
|
||||
try {
|
||||
isLoadingMore.value = true;
|
||||
const offset = messages.value.length;
|
||||
|
||||
const response = await axios.get('/api/chat/history', {
|
||||
headers: { Authorization: `Bearer ${auth.address}` },
|
||||
params: { limit: PAGE_SIZE, offset }
|
||||
const response = await api.get('/api/chat/history', {
|
||||
params: {
|
||||
limit: limit.value,
|
||||
offset: offset.value
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
@@ -206,82 +200,79 @@ const loadMoreMessages = async () => {
|
||||
showAuthOptions: false
|
||||
}));
|
||||
|
||||
messages.value = [...newMessages, ...messages.value];
|
||||
messages.value = [...messages.value, ...newMessages];
|
||||
hasMoreMessages.value = response.data.total > messages.value.length;
|
||||
offset.value += newMessages.length;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading more messages:', error);
|
||||
console.error('Error loading chat history:', error);
|
||||
} finally {
|
||||
isLoadingMore.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Функция прокрутки к последнему сообщению
|
||||
const scrollToBottom = () => {
|
||||
if (messagesContainer.value) {
|
||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
|
||||
}
|
||||
};
|
||||
// Загружаем сообщения при изменении аутентификации
|
||||
watch(() => auth.isAuthenticated, async (newValue) => {
|
||||
if (newValue) {
|
||||
messages.value = [];
|
||||
offset.value = 0;
|
||||
hasMoreMessages.value = true;
|
||||
|
||||
// Инициализация при монтировании
|
||||
onMounted(async () => {
|
||||
console.log('HomeView.vue: onMounted called');
|
||||
console.log('Auth state:', auth.isAuthenticated);
|
||||
try {
|
||||
// Сначала загружаем историю из messages
|
||||
await loadMoreMessages();
|
||||
|
||||
// Определяем язык
|
||||
const cyrillicPattern = /[а-яА-ЯёЁ]/;
|
||||
userLanguage.value = cyrillicPattern.test(newMessage.value) ? 'ru' : 'en';
|
||||
console.log('Detected language:', userLanguage.value);
|
||||
// Связываем гостевые сообщения (копируем из guest_messages в messages)
|
||||
await api.post('/api/chat/link-guest-messages');
|
||||
console.log('Guest messages linked to authenticated user');
|
||||
|
||||
// Если пользователь уже аутентифицирован, загружаем историю
|
||||
if (auth.isAuthenticated && auth.userId) {
|
||||
console.log('User authenticated, loading chat history...');
|
||||
await loadChatHistory();
|
||||
// Перезагружаем сообщения, чтобы получить все, включая перенесенные
|
||||
messages.value = [];
|
||||
offset.value = 0;
|
||||
await loadMoreMessages();
|
||||
|
||||
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 () => {
|
||||
try {
|
||||
const result = await connectWithWallet();
|
||||
|
||||
if (result.success) {
|
||||
console.log('Wallet auth result:', result);
|
||||
// Сохраняем гостевые сообщения перед очисткой
|
||||
const guestMessages = [...messages.value];
|
||||
messages.value = [];
|
||||
offset.value = 0;
|
||||
hasMoreMessages.value = true;
|
||||
|
||||
// Обновляем состояние аутентификации
|
||||
auth.setAuth({
|
||||
authenticated: true,
|
||||
isAuthenticated: true,
|
||||
userId: result.userId,
|
||||
address: result.address,
|
||||
isAdmin: result.isAdmin,
|
||||
authType: 'wallet'
|
||||
});
|
||||
try {
|
||||
await api.post('/api/chat/link-guest-messages');
|
||||
console.log('Guest messages linked to authenticated user');
|
||||
await loadMoreMessages();
|
||||
|
||||
// Добавляем задержку для синхронизации сессии
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
const filteredGuestMessages = guestMessages
|
||||
.filter(msg => !msg.showAuthButtons)
|
||||
.reverse();
|
||||
messages.value = [...messages.value, ...filteredGuestMessages];
|
||||
|
||||
// Загружаем историю чата
|
||||
await loadChatHistory();
|
||||
await nextTick();
|
||||
scrollToBottom();
|
||||
} catch (linkError) {
|
||||
console.error('Error linking guest messages:', linkError);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error connecting wallet:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -295,7 +286,7 @@ const saveGuestMessagesToServer = async () => {
|
||||
|
||||
// Отправляем каждое сообщение на сервер
|
||||
for (const msg of userMessages) {
|
||||
await axios.post('/api/chat/message', {
|
||||
await api.post('/api/chat/message', {
|
||||
message: msg.content,
|
||||
language: userLanguage.value
|
||||
});
|
||||
@@ -311,7 +302,7 @@ const saveGuestMessagesToServer = async () => {
|
||||
async function connectTelegram() {
|
||||
try {
|
||||
// Отправляем запрос на получение ссылки для авторизации через Telegram
|
||||
const response = await axios.get('/api/auth/telegram', {
|
||||
const response = await api.get('/api/auth/telegram', {
|
||||
withCredentials: true
|
||||
});
|
||||
|
||||
@@ -376,7 +367,7 @@ async function requestEmailCode() {
|
||||
// Функция проверки кода
|
||||
const verifyEmailCode = async () => {
|
||||
try {
|
||||
const response = await axios.post('/api/auth/email/verify-code', {
|
||||
const response = await api.post('/api/auth/email/verify-code', {
|
||||
email: emailInput.value,
|
||||
code: emailCode.value
|
||||
});
|
||||
@@ -387,7 +378,7 @@ const verifyEmailCode = async () => {
|
||||
emailError.value = '';
|
||||
|
||||
// Загружаем историю чата после успешной аутентификации
|
||||
await loadChatHistory();
|
||||
await loadMoreMessages();
|
||||
} else {
|
||||
emailError.value = response.data.error || 'Неверный код';
|
||||
}
|
||||
@@ -404,12 +395,6 @@ function cancelEmailVerification() {
|
||||
emailErrorMessage.value = '';
|
||||
}
|
||||
|
||||
// Добавьте эту функцию в <script setup>
|
||||
const formatAddress = (address) => {
|
||||
if (!address) return '';
|
||||
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
|
||||
};
|
||||
|
||||
// Форматирование времени
|
||||
const formatTime = (timestamp) => {
|
||||
if (!timestamp) return '';
|
||||
@@ -438,101 +423,73 @@ const formatTime = (timestamp) => {
|
||||
};
|
||||
|
||||
// Функция для отправки сообщения
|
||||
const handleMessage = async (messageText) => {
|
||||
if (!messageText.trim() || isLoading.value) return;
|
||||
|
||||
console.log('Handling message:', messageText);
|
||||
isLoading.value = true;
|
||||
|
||||
const handleMessage = async (text) => {
|
||||
try {
|
||||
const messageContent = text.trim();
|
||||
if (!messageContent) return;
|
||||
|
||||
newMessage.value = '';
|
||||
isLoading.value = true;
|
||||
|
||||
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 {
|
||||
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) {
|
||||
console.error('Error handling message:', error);
|
||||
console.error('Error sending message:', error);
|
||||
messages.value.push({
|
||||
id: Date.now(),
|
||||
content: 'Произошла ошибка при отправке сообщения.',
|
||||
role: 'assistant',
|
||||
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 {
|
||||
isLoading.value = false;
|
||||
}
|
||||
@@ -554,7 +511,7 @@ const handleEmailAuth = async () => {
|
||||
// Функция отправки email
|
||||
const submitEmail = async () => {
|
||||
try {
|
||||
const response = await axios.post('/api/auth/email/request', {
|
||||
const response = await api.post('/api/auth/email/request', {
|
||||
email: emailInput.value
|
||||
});
|
||||
|
||||
@@ -573,7 +530,7 @@ const submitEmail = async () => {
|
||||
// Функция верификации кода Telegram
|
||||
const verifyTelegramCode = async () => {
|
||||
try {
|
||||
const response = await axios.post('/api/auth/telegram/verify', {
|
||||
const response = await api.post('/api/auth/telegram/verify', {
|
||||
code: telegramCode.value
|
||||
});
|
||||
|
||||
@@ -602,7 +559,7 @@ const verifyTelegramCode = async () => {
|
||||
|
||||
// Загружаем историю чата после небольшой задержки
|
||||
setTimeout(async () => {
|
||||
await loadChatHistory();
|
||||
await loadMoreMessages();
|
||||
}, 100);
|
||||
} else {
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
@@ -665,64 +659,49 @@ h1 {
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #ddd;
|
||||
background-color: #f8f9fa;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
background-color: #f0f0f0;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
/* Адаптивный заголовок чата */
|
||||
@media (max-width: 768px) {
|
||||
.chat-header {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.chat-header h2 {
|
||||
font-size: 1.2rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
.wallet-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 0.9rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Адаптивная информация о пользователе */
|
||||
@media (max-width: 768px) {
|
||||
.user-info {
|
||||
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;
|
||||
.disconnect-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: #ff4444;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Адаптивная кнопка выхода */
|
||||
@media (max-width: 768px) {
|
||||
.logout-btn {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.disconnect-btn:hover {
|
||||
background-color: #cc0000;
|
||||
}
|
||||
|
||||
.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 {
|
||||
@@ -915,32 +894,27 @@ h1 {
|
||||
}
|
||||
|
||||
.auth-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.75rem 1rem;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
transition: opacity 0.2s;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.auth-btn:hover {
|
||||
opacity: 0.9;
|
||||
.wallet-btn {
|
||||
background-color: #4a5568;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.auth-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
.wallet-btn:hover {
|
||||
background-color: #2d3748;
|
||||
}
|
||||
|
||||
.auth-icon {
|
||||
margin-right: 0.75rem;
|
||||
font-size: 1.2rem;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.telegram-btn {
|
||||
@@ -1080,4 +1054,48 @@ h1 {
|
||||
background-color: #cbd5e0;
|
||||
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>
|
||||
|
||||
@@ -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: {
|
||||
port: 5173,
|
||||
host: 'localhost',
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
credentials: true,
|
||||
rewrite: (path) => path
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user