Тестовый коммит после удаления husky

This commit is contained in:
2025-03-05 01:02:09 +03:00
parent b0f7a64a96
commit d90e1b93a9
120 changed files with 8191 additions and 8530 deletions

16
.cursor/settings.json Normal file
View File

@@ -0,0 +1,16 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

7
backend/.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100
}

View File

@@ -1,7 +1,7 @@
const express = require('express');
const cors = require('cors');
const session = require('express-session');
const { verifySignature } = require('./utils/auth');
const { verifySignature, findOrCreateUser } = require('./utils/auth');
const pgSession = require('connect-pg-simple')(session);
const { requireRole } = require('./middleware/auth');
const crypto = require('crypto');
@@ -9,6 +9,13 @@ const path = require('path');
const fs = require('fs');
const { router: authRouter } = require('./routes/auth');
const { pool } = require('./db');
const FileStore = require('session-file-store')(session);
const helmet = require('helmet');
const sessionMiddleware = require('./middleware/session');
const chatRouter = require('./routes/chat');
const usersRoutes = require('./routes/users');
const contractsRoutes = require('./routes/contracts');
const rolesRoutes = require('./routes/roles');
const app = express();
@@ -20,74 +27,88 @@ function generateNonce() {
// Парсинг JSON - должен быть до всех роутов
app.use(express.json());
// Настройка CORS - должна быть первой после парсинга JSON
app.use(cors({
origin: ['http://127.0.0.1:5173', 'http://localhost:5173'],
// Настройка CORS
app.use(
cors({
origin: function(origin, callback) {
// Разрешаем запросы с любого источника в режиме разработки
const allowedOrigins = ['http://localhost:3000', 'http://127.0.0.1:5173', 'http://localhost:5173'];
if (!origin || allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'OPTIONS', 'DELETE', 'PUT', 'HEAD'],
allowedHeaders: [
'Content-Type',
'X-Wallet-Address',
'X-Wallet-Signature',
'Cookie',
'Authorization'
],
exposedHeaders: ['Set-Cookie']
}));
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Auth-Nonce'],
})
);
// Настройка сессий
app.use(session({
app.use(
session({
store: new pgSession({
pool: pool,
tableName: 'session',
createTableIfMissing: true
tableName: 'sessions',
createTableIfMissing: false,
}),
secret: process.env.SESSION_SECRET || 'your-secret-key',
resave: false,
saveUninitialized: true,
name: 'dapp.sid',
cookie: {
httpOnly: true,
secure: false,
httpOnly: true,
maxAge: 30 * 24 * 60 * 60 * 1000,
sameSite: 'lax',
maxAge: 24 * 60 * 60 * 1000 // 24 часа
}
}));
},
})
);
// Middleware для логирования сессий
app.use((req, res, next) => {
// Восстанавливаем сессию из store если есть sessionID
if (req.sessionID && !req.session.authenticated) {
req.sessionStore.get(req.sessionID, (err, session) => {
if (err) {
console.error('Session restore error:', err);
} else if (session) {
req.session.authenticated = session.authenticated;
req.session.address = session.address;
req.session.lastSignature = session.lastSignature;
}
next();
});
} else {
next();
}
});
// Middleware для безопасности
app.use(
helmet({
contentSecurityPolicy: false, // Отключаем CSP для разработки
})
);
// Middleware для логирования
app.use((req, res, next) => {
console.log(`${req.method} ${req.path}`, {
headers: req.headers,
body: req.body,
session: req.session
session: req.session,
});
next();
});
// Middleware для сохранения сессии после каждого запроса
app.use((req, res, next) => {
const originalEnd = res.end;
res.end = function () {
if (req.session && req.session.save) {
req.session.save((err) => {
if (err) {
console.error('Ошибка при сохранении сессии:', err);
}
originalEnd.apply(res, arguments);
});
} else {
originalEnd.apply(res, arguments);
}
};
next();
});
// Middleware для проверки авторизации
const requireAuth = (req, res, next) => {
console.log('Auth check:', {
session: req.session,
authenticated: req.session.authenticated,
address: req.session.address
address: req.session.address,
});
if (!req.session.authenticated || !req.session.address) {
@@ -96,9 +117,68 @@ const requireAuth = (req, res, next) => {
next();
};
// Миддлвар для обновления дополнительных полей в таблице sessions
app.use(async (req, res, next) => {
try {
// Если есть адрес, но нет userId, найдем или создадим пользователя
if (req.session && req.session.authenticated && req.session.address && !req.session.userId) {
try {
const user = await findOrCreateUser(req.session.address, 'wallet');
req.session.userId = user.id;
req.session.role = user.role;
req.session.isAdmin = user.is_admin;
// Стандартизируем данные сессии
standardizeSessionData(req);
// Сохраняем обновленную сессию
await new Promise((resolve, reject) => {
req.session.save(err => {
if (err) reject(err);
else resolve();
});
});
} catch (err) {
console.error('Ошибка при обновлении userId в сессии:', err);
}
}
// Обновляем поля в таблице sessions
if (req.session && (req.session.userId || req.session.address)) {
db.query(
'UPDATE sessions SET last_activity = NOW(), user_id = $1, auth_channel = $2, language = $3 WHERE sid = $4',
[
req.session.userId || null,
req.session.authChannel || 'web',
req.session.language || 'en',
req.sessionID
]
).catch(err => console.error('Error updating session data:', err));
}
} catch (err) {
console.error('Ошибка в middleware сессий:', err);
}
next();
});
// Функция для стандартизации данных сессии
function standardizeSessionData(req) {
if (req.session.authenticated) {
// Убедимся, что все необходимые поля присутствуют
req.session.authType = req.session.authType || 'wallet';
req.session.role = req.session.role || 'USER';
req.session.isAdmin = !!req.session.isAdmin;
req.session.authChannel = req.session.authChannel || 'web';
req.session.language = req.session.language || 'en';
}
}
// API роуты
const apiRouter = express.Router();
// Удалите или закомментируйте этот блок кода
/*
apiRouter.post('/refresh-session', async (req, res) => {
try {
const { address, signature } = req.body;
@@ -143,17 +223,18 @@ apiRouter.post('/refresh-session', async (req, res) => {
res.status(500).json({ error: 'Internal server error' });
}
});
*/
apiRouter.get('/session', (req, res) => {
console.log('Session check:', {
session: req.session,
authenticated: req.session.authenticated,
address: req.session.address
address: req.session.address,
});
res.json({
authenticated: !!req.session.authenticated,
address: req.session.address || null
address: req.session.address || null,
});
});
@@ -180,11 +261,7 @@ apiRouter.get('/admin/check', async (req, res) => {
const ethers = require('ethers');
const provider = new ethers.JsonRpcProvider(process.env.ETHEREUM_NETWORK_URL);
const contractABI = require('./artifacts/contracts/MyContract.sol/MyContract.json').abi;
const contract = new ethers.Contract(
process.env.CONTRACT_ADDRESS,
contractABI,
provider
);
const contract = new ethers.Contract(process.env.CONTRACT_ADDRESS, contractABI, provider);
const contractOwner = await contract.owner();
const isAdmin = req.session.address.toLowerCase() === contractOwner.toLowerCase();
@@ -218,7 +295,10 @@ apiRouter.post('/verify', async (req, res) => {
const { message, signature } = req.body;
if (!message || !signature) {
console.error('Отсутствуют необходимые поля:', { message: !!message, signature: !!signature });
console.error('Отсутствуют необходимые поля:', {
message: !!message,
signature: !!signature,
});
return res.status(400).json({ success: false, error: 'Missing required fields' });
}
@@ -244,7 +324,7 @@ apiRouter.post('/verify', async (req, res) => {
return res.status(500).json({
success: false,
error: 'Session save failed',
message: err.message
message: err.message,
});
}
@@ -257,7 +337,7 @@ apiRouter.post('/verify', async (req, res) => {
res.status(500).json({
success: false,
error: 'Verification failed',
message: error.message || 'Unknown error'
message: error.message || 'Unknown error',
});
}
});
@@ -267,6 +347,10 @@ app.use('/api', apiRouter);
// Подключаем маршруты аутентификации
app.use('/api/auth', authRouter);
app.use('/api/users', usersRoutes);
app.use('/api/contracts', contractsRoutes);
app.use('/api/chat', chatRouter);
app.use('/api/roles', rolesRoutes);
apiRouter.get('/nonce', (req, res) => {
const nonce = generateNonce();
@@ -288,6 +372,9 @@ apiRouter.get('/nonce', (req, res) => {
res.json({ nonce });
});
// Добавьте после настройки сессий
app.use(sessionMiddleware);
// Обработка ошибок сессий
app.use((err, req, res, next) => {
if (err.code === 'ENOENT' && err.message.includes('sessions')) {
@@ -311,4 +398,8 @@ app.use((err, req, res, next) => {
res.status(500).json({ error: 'Something broke!' });
});
// Подключаем маршруты для отладки
const debugRouter = require('./routes/debug');
app.use('/api/debug', debugRouter);
module.exports = { app };

View File

@@ -1,16 +0,0 @@
# Пример документа для RAG
Это пример документа, который будет использоваться в RAG системе.
## Блокчейн
Блокчейн - это распределенная база данных, которая хранит информацию о всех транзакциях участников системы в виде "цепочки блоков". Каждый блок содержит набор транзакций и ссылку на предыдущий блок.
## Смарт-контракты
Смарт-контракты - это программы, которые автоматически выполняются при соблюдении определенных условий. Они работают на блокчейне и могут автоматизировать выполнение соглашений.
## Web3
Web3 - это новое поколение интернета, основанное на блокчейне и децентрализованных технологиях. Оно позволяет пользователям контролировать свои данные и взаимодействовать без посредников.

View File

@@ -4,7 +4,7 @@ require('dotenv').config();
// Создаем пул соединений с базой данных
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
});
// Проверяем подключение к базе данных
@@ -28,7 +28,7 @@ const query = (text, params) => {
// Экспортируем функции для работы с базой данных
module.exports = {
query,
pool
pool,
};
// Функция для создания временного хранилища данных в памяти
@@ -45,14 +45,14 @@ function createInMemoryStorage() {
// Эмуляция запроса SELECT * FROM users WHERE address = $1
if (text.includes('SELECT * FROM users WHERE address = $1')) {
const address = params[0];
const user = users.find(u => u.address === address);
const user = users.find((u) => u.address === address);
return { rows: user ? [user] : [] };
}
// Эмуляция запроса SELECT * FROM users WHERE email = $1
if (text.includes('SELECT * FROM users WHERE email = $1')) {
const email = params[0];
const user = users.find(u => u.email === email);
const user = users.find((u) => u.email === email);
return { rows: user ? [user] : [] };
}
@@ -89,7 +89,42 @@ function createInMemoryStorage() {
} else {
return inMemoryQuery(text, params);
}
}
}
},
},
};
}
// Проверка и создание таблицы session, если она не существует
async function checkSessionTable() {
try {
const result = await pool.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'session'
);
`);
const tableExists = result.rows[0].exists;
if (!tableExists) {
console.log('Таблица session не существует, создаем...');
await pool.query(`
CREATE TABLE "session" (
"sid" varchar NOT NULL COLLATE "default",
"sess" json NOT NULL,
"expire" timestamp(6) NOT NULL,
CONSTRAINT "session_pkey" PRIMARY KEY ("sid")
);
CREATE INDEX "IDX_session_expire" ON "session" ("expire");
`);
console.log('Таблица session успешно создана');
} else {
console.log('Таблица session уже существует');
}
} catch (error) {
console.error('Ошибка при проверке/создании таблицы session:', error);
}
}

9
backend/docs/api.md Normal file
View File

@@ -0,0 +1,9 @@
# API Documentation
## Authentication
### POST /api/auth/refresh-session
Refreshes the user session.
**Request:**

30
backend/eslint.config.js Normal file
View File

@@ -0,0 +1,30 @@
import globals from 'globals';
export default [
{
ignores: ['node_modules/**', 'artifacts/**', 'sessions/**', 'logs/**', 'data/**'],
},
{
files: ['**/*.js'],
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
globals: {
...globals.node,
...globals.es2021,
describe: 'readonly',
it: 'readonly',
beforeEach: 'readonly',
before: 'readonly',
after: 'readonly',
afterEach: 'readonly',
},
},
rules: {
'no-unused-vars': 'off',
'no-console': 'off',
'no-undef': 'error',
'no-duplicate-imports': 'error',
},
},
];

View File

@@ -1,12 +1,12 @@
require("@nomicfoundation/hardhat-toolbox");
require('@nomicfoundation/hardhat-toolbox');
require('dotenv').config();
module.exports = {
solidity: "0.8.20",
solidity: '0.8.20',
networks: {
sepolia: {
url: process.env.ETHEREUM_NETWORK_URL,
accounts: [process.env.PRIVATE_KEY]
}
}
accounts: [process.env.PRIVATE_KEY],
},
},
};

View File

@@ -0,0 +1,10 @@
{"level":"info","message":"Running scheduled token balance check","timestamp":"2025-03-04T20:30:00.970Z"}
{"level":"info","message":"Checking token balances for 1 users","timestamp":"2025-03-04T20:30:00.982Z"}
{"level":"error","message":"Ошибка при получении контракта AccessToken: Адрес контракта AccessToken не найден в переменных окружения","timestamp":"2025-03-04T20:30:00.983Z"}
{"level":"error","message":"Error checking token balance for 0x0000000000000000000000000000000000000000: Адрес контракта AccessToken не найден в переменных окружения","timestamp":"2025-03-04T20:30:00.983Z"}
{"level":"info","message":"User 10 with address 0x0000000000000000000000000000000000000000: admin=false","timestamp":"2025-03-04T20:30:00.984Z"}
{"level":"info","message":"Running scheduled token balance check","timestamp":"2025-03-04T21:30:00.777Z"}
{"level":"info","message":"Checking token balances for 1 users","timestamp":"2025-03-04T21:30:00.793Z"}
{"level":"error","message":"Ошибка при получении контракта AccessToken: Адрес контракта AccessToken не найден в переменных окружения","timestamp":"2025-03-04T21:30:00.794Z"}
{"level":"error","message":"Error checking token balance for 0x0000000000000000000000000000000000000000: Адрес контракта AccessToken не найден в переменных окружения","timestamp":"2025-03-04T21:30:00.795Z"}
{"level":"info","message":"User 10 with address 0x0000000000000000000000000000000000000000: admin=false","timestamp":"2025-03-04T21:30:00.795Z"}

View File

@@ -0,0 +1,4 @@
{"level":"error","message":"Ошибка при получении контракта AccessToken: Адрес контракта AccessToken не найден в переменных окружения","timestamp":"2025-03-04T20:30:00.983Z"}
{"level":"error","message":"Error checking token balance for 0x0000000000000000000000000000000000000000: Адрес контракта AccessToken не найден в переменных окружения","timestamp":"2025-03-04T20:30:00.983Z"}
{"level":"error","message":"Ошибка при получении контракта AccessToken: Адрес контракта AccessToken не найден в переменных окружения","timestamp":"2025-03-04T21:30:00.794Z"}
{"level":"error","message":"Error checking token balance for 0x0000000000000000000000000000000000000000: Адрес контракта AccessToken не найден в переменных окружения","timestamp":"2025-03-04T21:30:00.795Z"}

View File

@@ -1,32 +1,130 @@
const { checkAccess } = require('../utils/access-check');
const logger = require('../utils/logger');
const { getUserInfo } = require('../utils/access-check');
// Добавьте в начало файла
const isMiddleware = true;
// Middleware для проверки роли
const requireRole = (requiredRole) => async (req, res, next) => {
const requireRole = (allowedRoles) => async (req, res, next) => {
if (!req.session || !req.session.authenticated || !req.session.userId) {
return res.status(401).json({ error: 'Требуется аутентификация' });
}
try {
const address = req.headers['x-wallet-address'];
if (!address) {
return res.status(401).json({ error: 'No wallet address' });
// Получение информации о пользователе
const userInfo = await getUserInfo(req.session.userId);
if (!userInfo) {
return res.status(401).json({ error: 'Пользователь не найден' });
}
const { hasAccess, role } = await checkAccess(address);
if (!hasAccess) {
return res.status(403).json({ error: 'No access token' });
// Проверка роли
if (!allowedRoles.includes(userInfo.role)) {
return res.status(403).json({ error: 'Недостаточно прав' });
}
if (requiredRole && role !== requiredRole) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
// Добавляем информацию о роли в request
req.userRole = role;
next();
} catch (error) {
console.error('Auth check error:', error);
res.status(500).json({ error: 'Auth check failed' });
logger.error('Error checking user role:', error);
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
}
};
// Проверка роли пользователя
const checkRole = async (req, res, next) => {
try {
// Если функция вызвана как middleware
const isMiddleware = typeof next === 'function';
if (!req.session.authenticated) {
return isMiddleware ? res.status(401).json({ error: 'Не авторизован' }) : false;
}
// Если роль администратора уже проверена в сессии
if (req.session.isAdmin === true) {
return isMiddleware ? next() : true;
}
const db = require('../db');
// Проверка наличия токенов доступа в смарт-контракте
if (req.session.address) {
const address = req.session.address.toLowerCase();
// Проверка в базе данных
const userRole = await db.query(
'SELECT r.name FROM users u JOIN roles r ON u.role_id = r.id WHERE LOWER(u.address) = $1',
[address]
);
if (userRole.rows.length > 0 && userRole.rows[0].name === 'admin') {
req.session.isAdmin = true;
return isMiddleware ? next() : true;
}
// Проверка токенов в смарт-контракте через сервис
const { ethers } = require('ethers');
const provider = new ethers.JsonRpcProvider(process.env.PROVIDER_URL);
const accessTokenABI = require('../artifacts/contracts/AccessToken.sol/AccessToken.json').abi;
const accessTokenContract = new ethers.Contract(
process.env.ACCESS_TOKEN_ADDRESS,
accessTokenABI,
provider
);
try {
const hasAdminRole = await accessTokenContract.hasRole(
ethers.keccak256(ethers.toUtf8Bytes('ADMIN_ROLE')),
address
);
if (hasAdminRole) {
// Обновляем роль в базе данных
await db.query(
'UPDATE users SET role_id = (SELECT id FROM roles WHERE name = $1) WHERE LOWER(address) = $2',
['admin', address]
);
req.session.isAdmin = true;
return isMiddleware ? next() : true;
}
} catch (error) {
console.error('Ошибка при проверке роли в контракте:', error);
}
}
// Если пользователь не администратор
req.session.isAdmin = false;
return isMiddleware ? res.status(403).json({ error: 'Недостаточно прав' }) : false;
} catch (error) {
console.error('Ошибка при проверке роли:', error);
return isMiddleware ? res.status(500).json({ error: 'Внутренняя ошибка сервера' }) : false;
}
};
// Middleware для проверки аутентификации
const requireAuth = (req, res, next) => {
if (!req.session || !req.session.authenticated) {
return res.status(401).json({ error: 'Требуется аутентификация' });
}
next();
};
// Middleware для проверки прав администратора
const requireAdmin = (req, res, next) => {
if (!req.session || !req.session.authenticated) {
return res.status(401).json({ error: 'Требуется аутентификация' });
}
if (!req.session.isAdmin) {
return res.status(403).json({ error: 'Требуются права администратора' });
}
next();
};
module.exports = {
requireRole
requireRole,
requireAuth,
requireAdmin,
checkRole,
};

View File

@@ -0,0 +1,12 @@
const logger = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.originalUrl} - ${res.statusCode} - ${duration}ms`);
});
next();
};
module.exports = logger;

View File

@@ -0,0 +1,22 @@
const session = require('express-session');
const pgSession = require('connect-pg-simple')(session);
const { pool } = require('../db');
const sessionMiddleware = session({
store: new pgSession({
pool: pool,
tableName: 'session',
createTableIfMissing: true,
}),
secret: process.env.SESSION_SECRET || 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production', // В production должно быть true
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000, // 24 часа
sameSite: 'none', // Для работы между разными доменами
},
});
module.exports = sessionMiddleware;

View File

@@ -1,44 +0,0 @@
-- Создание таблицы для связи идентификаторов пользователей
CREATE TABLE IF NOT EXISTS user_identities (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
identity_type VARCHAR(20) NOT NULL, -- 'ethereum', 'telegram', 'email'
identity_value VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(identity_type, identity_value)
);
-- Создание таблицы для предпочтений пользователей
CREATE TABLE IF NOT EXISTS user_preferences (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
preference_key VARCHAR(50) NOT NULL,
preference_value TEXT,
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(user_id, preference_key)
);
-- Создание таблицы для взаимодействий пользователей
CREATE TABLE IF NOT EXISTS user_interactions (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
interaction_type VARCHAR(50) NOT NULL,
interaction_data JSONB,
created_at TIMESTAMP DEFAULT NOW()
);
-- Создание таблицы для тем пользователей
CREATE TABLE IF NOT EXISTS user_topics (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
topic VARCHAR(100) NOT NULL,
relevance_score FLOAT DEFAULT 1.0,
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(user_id, topic)
);
-- Индексы для оптимизации запросов
CREATE INDEX IF NOT EXISTS idx_user_identities_user_id ON user_identities(user_id);
CREATE INDEX IF NOT EXISTS idx_user_preferences_user_id ON user_preferences(user_id);
CREATE INDEX IF NOT EXISTS idx_user_interactions_user_id ON user_interactions(user_id);
CREATE INDEX IF NOT EXISTS idx_user_topics_user_id ON user_topics(user_id);

View File

@@ -1,64 +0,0 @@
-- Таблица для Канбан-досок
CREATE TABLE IF NOT EXISTS kanban_boards (
id SERIAL PRIMARY KEY,
title VARCHAR(100) NOT NULL,
description TEXT,
owner_id INTEGER REFERENCES users(id),
is_public BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Таблица для колонок Канбан-доски
CREATE TABLE IF NOT EXISTS kanban_columns (
id SERIAL PRIMARY KEY,
board_id INTEGER REFERENCES kanban_boards(id) ON DELETE CASCADE,
title VARCHAR(100) NOT NULL,
position INTEGER NOT NULL,
wip_limit INTEGER DEFAULT NULL, -- Лимит задач в работе (Work In Progress)
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Таблица для карточек (задач) Канбан-доски
CREATE TABLE IF NOT EXISTS kanban_cards (
id SERIAL PRIMARY KEY,
column_id INTEGER REFERENCES kanban_columns(id) ON DELETE CASCADE,
title VARCHAR(200) NOT NULL,
description TEXT,
position INTEGER NOT NULL,
assigned_to INTEGER REFERENCES users(id),
due_date TIMESTAMP,
labels JSONB DEFAULT '[]',
created_by INTEGER REFERENCES users(id),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Таблица для комментариев к карточкам
CREATE TABLE IF NOT EXISTS kanban_comments (
id SERIAL PRIMARY KEY,
card_id INTEGER REFERENCES kanban_cards(id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(id),
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Таблица для доступа к доскам
CREATE TABLE IF NOT EXISTS kanban_board_access (
id SERIAL PRIMARY KEY,
board_id INTEGER REFERENCES kanban_boards(id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(id),
access_level VARCHAR(20) NOT NULL, -- 'read', 'write', 'admin'
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(board_id, user_id)
);
-- Индексы для оптимизации запросов
CREATE INDEX IF NOT EXISTS idx_kanban_columns_board_id ON kanban_columns(board_id);
CREATE INDEX IF NOT EXISTS idx_kanban_cards_column_id ON kanban_cards(column_id);
CREATE INDEX IF NOT EXISTS idx_kanban_comments_card_id ON kanban_comments(card_id);
CREATE INDEX IF NOT EXISTS idx_kanban_board_access_board_id ON kanban_board_access(board_id);
CREATE INDEX IF NOT EXISTS idx_kanban_board_access_user_id ON kanban_board_access(user_id);

View File

@@ -0,0 +1,26 @@
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);

View File

@@ -0,0 +1,40 @@
-- Таблица идентификаторов пользователей
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;

View File

@@ -0,0 +1,14 @@
-- Создание таблицы для хранения истории диалогов
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);

View File

@@ -1,18 +1,7 @@
{
"verbose": true,
"ignore": [
".git",
"node_modules/**/node_modules",
"sessions",
"data/vector_store"
],
"watch": [
"*.js",
"routes/",
"services/",
"utils/",
"middleware/"
],
"ignore": [".git", "node_modules/**/node_modules", "sessions", "data/vector_store"],
"watch": ["*.js", "routes/", "services/", "utils/", "middleware/"],
"env": {
"NODE_ENV": "development"
},

View File

@@ -12,34 +12,46 @@
"server": "nodemon server.js --signal SIGUSR2",
"migrate": "node scripts/run-migrations.js",
"prod": "NODE_ENV=production node server.js",
"test": "mocha test/**/*.test.js"
"test": "mocha test/**/*.test.js",
"check-ollama": "node scripts/check-ollama-models.js",
"check-ethers": "node scripts/check-ethers-v6-compatibility.js",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write \"**/*.{js,vue,json,md}\"",
"format:check": "prettier --check \"**/*.{js,vue,json,md}\""
},
"dependencies": {
"@langchain/community": "^0.0.32",
"@langchain/community": "^0.3.34",
"@langchain/core": "0.3.0",
"@langchain/ollama": "^0.2.0",
"axios": "^1.6.7",
"connect-pg-simple": "^10.0.0",
"cors": "^2.8.5",
"cron": "^4.1.0",
"csurf": "^1.11.0",
"dotenv": "^16.0.3",
"ethers": "^6.7.1",
"ethers": "6.13.5",
"express": "^4.18.2",
"express-rate-limit": "^7.5.0",
"express-session": "^1.17.3",
"helmet": "^8.0.0",
"hnswlib-node": "^3.0.0",
"imap": "^0.8.19",
"langchain": "^0.1.21",
"langchain": "0.0.200",
"mailparser": "^3.7.2",
"node-telegram-bot-api": "^0.64.0",
"nodemailer": "^6.9.9",
"node-cron": "^3.0.3",
"node-telegram-bot-api": "^0.66.0",
"nodemailer": "^6.10.0",
"pg": "^8.10.0",
"session-file-store": "^1.5.0",
"siwe": "^2.1.4",
"winston": "^3.17.0"
},
"devDependencies": {
"nodemon": "^2.0.22"
"eslint": "^9.21.0",
"eslint-config-prettier": "^10.0.2",
"globals": "^16.0.0",
"nodemon": "^3.1.9",
"prettier": "^3.5.3"
}
}

View File

@@ -1,11 +1,14 @@
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 pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
});
// Проверка доступа
@@ -33,7 +36,7 @@ router.get('/check', async (req, res) => {
hasAccess: true,
tokenId: token.id,
role: token.role,
expiresAt: token.expires_at
expiresAt: token.expires_at,
});
} catch (error) {
console.error('Access check error:', error);
@@ -80,17 +83,17 @@ router.get('/tokens', async (req, res) => {
}
// Получаем список всех токенов
const result = await pool.query(
'SELECT * FROM access_tokens ORDER BY created_at DESC'
);
const result = await pool.query('SELECT * FROM access_tokens ORDER BY created_at DESC');
res.json(result.rows.map(token => ({
res.json(
result.rows.map((token) => ({
id: token.id,
walletAddress: token.wallet_address,
role: token.role,
createdAt: token.created_at,
expiresAt: token.expires_at
})));
expiresAt: token.expires_at,
}))
);
} catch (error) {
console.error('Tokens list error:', error);
res.status(500).json({ error: error.message });
@@ -137,7 +140,7 @@ router.post('/tokens', async (req, res) => {
walletAddress: result.rows[0].wallet_address,
role: result.rows[0].role,
createdAt: result.rows[0].created_at,
expiresAt: result.rows[0].expires_at
expiresAt: result.rows[0].expires_at,
});
} catch (error) {
console.error('Token creation error:', error);
@@ -167,10 +170,7 @@ router.delete('/tokens/:id', async (req, res) => {
const { id } = req.params;
// Удаляем токен
await pool.query(
'DELETE FROM access_tokens WHERE id = $1',
[id]
);
await pool.query('DELETE FROM access_tokens WHERE id = $1', [id]);
res.json({ success: true });
} catch (error) {
@@ -179,4 +179,123 @@ router.delete('/tokens/:id', async (req, res) => {
}
});
// Получение информации о роли текущего пользователя
router.get('/role', requireAuth, async (req, res) => {
try {
const address = req.session.address.toLowerCase();
const result = await db.query(
'SELECT r.name as role FROM users u JOIN roles r ON u.role_id = r.id WHERE LOWER(u.address) = $1',
[address]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Пользователь не найден' });
}
return res.json({ role: result.rows[0].role });
} catch (error) {
console.error('Ошибка при получении роли:', error);
return res.status(500).json({ error: 'Внутренняя ошибка сервера' });
}
});
// Получение списка всех пользователей (только для администраторов)
router.get('/users', requireAdmin, async (req, res) => {
try {
const result = await db.query(
'SELECT u.id, u.wallet_address, r.name as role, u.created_at FROM users u JOIN roles r ON u.role_id = r.id'
);
return res.json(result.rows);
} catch (error) {
console.error('Ошибка при получении списка пользователей:', error);
return res.status(500).json({ error: 'Внутренняя ошибка сервера' });
}
});
// Изменение роли пользователя (только для администраторов)
router.post('/users/:userId/role', requireAdmin, async (req, res) => {
try {
const { userId } = req.params;
const { role } = req.body;
if (!role || !['admin', 'user'].includes(role)) {
return res.status(400).json({ error: 'Некорректная роль' });
}
await db.query(
'UPDATE users SET role_id = (SELECT id FROM roles WHERE name = $1) WHERE id = $2',
[role, userId]
);
return res.json({ success: true });
} catch (error) {
console.error('Ошибка при изменении роли пользователя:', error);
return res.status(500).json({ error: 'Внутренняя ошибка сервера' });
}
});
// Получение информации о токенах доступа текущего пользователя
router.get('/tokens', requireAuth, async (req, res) => {
try {
// Логирование для отладки
console.log('GET /api/access/tokens запрос получен');
console.log('Сессия пользователя:', req.session);
// Получаем адрес из сессии, а не из заголовков
if (!req.session || !req.session.address) {
return res.status(400).json({ error: 'No wallet address in session' });
}
const address = req.session.address.toLowerCase();
// Используем правильное имя таблицы и полей
const result = await db.query(
'SELECT id, wallet_address, role, created_at, expires_at FROM access_tokens WHERE LOWER(wallet_address) = $1',
[address]
);
return res.json(result.rows);
} catch (error) {
console.error('Ошибка при получении токенов:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
router.post('/mint', requireAuth, requireAdmin, async (req, res) => {
try {
// Логирование для отладки
console.log('POST /api/access/mint запрос получен');
console.log('Данные запроса:', req.body);
const { walletAddress, role, expiresInDays } = req.body;
if (!walletAddress || !role || !expiresInDays) {
return res.status(400).json({ error: 'Missing required fields' });
}
// Вычисляем дату истечения
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + parseInt(expiresInDays));
// Создаем токен
const result = await pool.query(
'INSERT INTO access_tokens (wallet_address, role, expires_at) VALUES ($1, $2, $3) RETURNING *',
[walletAddress.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('Ошибка при создании токена:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
module.exports = router;

123
backend/routes/admin.js Normal file
View File

@@ -0,0 +1,123 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
const { checkIfAdmin } = require('../utils/access-check');
// Middleware для проверки прав администратора
const requireAdmin = async (req, res, next) => {
console.log('Проверка прав администратора:', {
session: req.session
? {
authenticated: req.session.authenticated,
address: req.session.address,
isAdmin: req.session.isAdmin,
}
: null,
headers: {
authorization: req.headers.authorization,
},
});
// Проверка аутентификации через сессию
if (req.session && req.session.authenticated && req.session.isAdmin) {
console.log('Пользователь авторизован как администратор через сессию');
return next();
}
// Проверка через заголовок авторизации
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
console.log('Отсутствует заголовок авторизации');
return res.status(401).json({ error: 'Unauthorized' });
}
const address = authHeader.split(' ')[1];
console.log('Проверка адреса из заголовка:', address);
try {
// Проверяем напрямую в базе данных
const userResult = await db.query('SELECT is_admin FROM users WHERE address = $1', [
address.toLowerCase(),
]);
if (userResult.rows.length === 0) {
console.log(`Пользователь с адресом ${address} не найден`);
return res.status(404).json({ error: 'User not found' });
}
const isAdmin = userResult.rows[0].is_admin;
console.log(`Пользователь с адресом ${address} имеет статус администратора:`, isAdmin);
if (!isAdmin) {
console.log(`Пользователь с адресом ${address} не является администратором`);
return res.status(403).json({ error: 'Forbidden' });
}
// Обновляем сессию
if (req.session) {
req.session.authenticated = true;
req.session.address = address;
req.session.isAdmin = true;
console.log('Сессия обновлена из middleware:', {
address,
isAdmin: true,
});
}
next();
} catch (error) {
console.error('Ошибка при проверке прав администратора:', error);
return res.status(500).json({ error: 'Internal server error' });
}
};
// Применяем middleware ко всем маршрутам
router.use(requireAdmin);
// Маршрут для получения списка пользователей
router.get('/users', async (req, res) => {
try {
const result = await db.query('SELECT * FROM users');
res.json(result.rows);
} catch (error) {
console.error('Ошибка при получении списка пользователей:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Маршрут для получения статистики
router.get('/stats', async (req, res) => {
try {
// Получаем количество пользователей
const usersCount = await db.query('SELECT COUNT(*) FROM users');
// Получаем количество досок
const boardsCount = await db.query('SELECT COUNT(*) FROM kanban_boards');
// Получаем количество задач
const tasksCount = await db.query('SELECT COUNT(*) FROM kanban_tasks');
res.json({
userCount: parseInt(usersCount.rows[0].count),
boardCount: parseInt(boardsCount.rows[0].count),
taskCount: parseInt(tasksCount.rows[0].count),
});
} catch (error) {
console.error('Ошибка при получении статистики:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Маршрут для получения логов
router.get('/logs', async (req, res) => {
try {
const result = await db.query('SELECT * FROM logs ORDER BY created_at DESC LIMIT 100');
res.json(result.rows);
} catch (error) {
console.error('Ошибка при получении логов:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
module.exports = router;

View File

@@ -6,6 +6,10 @@ const db = require('../db');
const logger = require('../utils/logger');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const { checkIfAdmin } = require('../utils/access-check');
const { checkRole, requireAuth } = require('../middleware/auth');
const { pool } = require('../db');
const { verifySignature, checkAccess, findOrCreateUser } = require('../utils/auth');
// Создайте лимитер для попыток аутентификации
const authLimiter = rateLimit({
@@ -13,208 +17,221 @@ const authLimiter = rateLimit({
max: 20, // Увеличьте лимит с 5 до 20
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Слишком много попыток аутентификации. Попробуйте позже.' }
message: { error: 'Слишком много попыток аутентификации. Попробуйте позже.' },
});
// Маршрут для получения nonce для подписи
// Получение nonce для аутентификации
router.get('/nonce', async (req, res) => {
try {
const { address } = req.query;
// Удалите или закомментируйте эти логи
// console.log('Nonce request:', {
// address,
// sessionID: req.sessionID,
// session: req.session
// });
if (!address) {
return res.status(400).json({ error: 'Address is required' });
}
// Генерируем случайный nonce
const nonce = crypto.randomBytes(32).toString('hex');
// Создаем сообщение для подписи
const message = `Sign this message to authenticate with DApp for Business. Nonce: ${nonce}`;
// Генерируем nonce
const nonce = crypto.randomBytes(16).toString('hex');
// Сохраняем nonce в сессии
req.session.nonce = nonce;
req.session.pendingAddress = address;
req.session.authNonce = nonce;
req.session.pendingAddress = address.toLowerCase();
// Получаем IP-адрес клиента
const clientIP = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
console.log('Сгенерирован nonce для адреса:', address);
console.log('Сессия после генерации nonce:', req.session);
// Сохраняем IP-адрес в сессии при генерации nonce
req.session.clientIP = clientIP;
// Явно сохраняем сессию
// Сохраняем сессию и ждем завершения
await new Promise((resolve, reject) => {
req.session.save((err) => {
if (err) {
// Удалите или закомментируйте эти логи
// console.error('Error saving session:', err);
return res.status(500).json({ error: 'Failed to save session' });
console.error('Ошибка при сохранении сессии:', err);
reject(err);
} else {
resolve();
}
// Удалите или закомментируйте
// console.log('Nonce saved in session:', {
// nonce,
// pendingAddress: address,
// sessionID: req.sessionID
// });
res.json({ message });
});
});
// Проверяем, что nonce сохранился
console.log('Сессия после сохранения:', req.session);
return res.json({ nonce });
} catch (error) {
// Удалите или закомментируйте эти логи
// console.error('Error generating nonce:', error);
logger.error('Error generating nonce:', error);
res.status(500).json({ error: 'Failed to generate nonce' });
console.error('Ошибка при генерации nonce:', error);
return res.status(500).json({ error: 'Internal server error' });
}
});
// Маршрут для верификации подписи
router.post('/verify', authLimiter, async (req, res) => {
// Функция для проверки роли пользователя
async function checkUserRole(address, req) {
try {
const { address, signature } = req.body;
const lowerCaseAddress = address.toLowerCase();
if (!address || !signature) {
return res.status(400).json({ error: 'Address and signature are required' });
// Проверяем наличие токена доступа в базе данных
const result = await db.query(
'SELECT role FROM access_tokens WHERE LOWER(wallet_address) = $1 AND expires_at > NOW()',
[lowerCaseAddress]
);
if (result.rows.length > 0) {
// Если есть активный токен, проверяем роль
const role = result.rows[0].role;
return role === 'ADMIN';
}
// Удалите или закомментируйте эти логи
// console.log('Verify request:', {
// address,
// signature,
// sessionID: req.sessionID,
// session: {
// nonce: req.session.nonce,
// pendingAddress: req.session.pendingAddress
// }
// });
// Если нет токена, проверяем адрес администратора из переменных окружения
const adminAddresses = (process.env.ADMIN_ADDRESSES || '')
.split(',')
.map((a) => a.toLowerCase());
return adminAddresses.includes(lowerCaseAddress);
} catch (error) {
console.error('Ошибка при проверке роли пользователя:', error);
return false;
}
}
// Получаем nonce из сессии
const nonce = req.session.nonce;
const pendingAddress = req.session.pendingAddress;
// Верификация подписи
router.post('/verify', async (req, res) => {
try {
const { address, signature, message, nonce } = req.body;
if (!nonce || !pendingAddress) {
return res.status(400).json({ error: 'No pending authentication request' });
console.log('Верификация подписи:', { address, signature, message });
console.log('Сессия при верификации:', req.session);
if (!address || !signature || !message) {
return res.status(400).json({ error: 'Address, signature and message are required' });
}
// Получаем IP-адрес клиента
const clientIP = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
// Проверяем наличие nonce в сессии
if (!req.session.authNonce || !req.session.pendingAddress) {
console.error('Сессия не содержит nonce или pendingAddress:', req.session);
// Проверяем, что IP-адрес совпадает
if (req.session.clientIP !== clientIP) {
return res.status(400).json({ error: 'IP address mismatch' });
// Проверяем наличие nonce в заголовке
const headerNonce = req.headers['x-auth-nonce'];
if (headerNonce) {
console.log('Найден nonce в заголовке:', headerNonce);
req.session.authNonce = headerNonce;
req.session.pendingAddress = address.toLowerCase();
}
// Проверяем, что адрес совпадает с тем, для которого был сгенерирован nonce
if (pendingAddress.toLowerCase() !== address.toLowerCase()) {
return res.status(400).json({ error: 'Address mismatch' });
// Если в запросе есть nonce в сообщении, извлекаем его
let extractedNonce = null;
if (message) {
const match = message.match(/nonce: ([a-f0-9]+)/);
if (match && match[1]) {
extractedNonce = match[1];
console.log('Извлечен nonce из сообщения:', extractedNonce);
// Устанавливаем nonce в сессию
req.session.authNonce = extractedNonce;
req.session.pendingAddress = address.toLowerCase();
// Сохраняем сессию
await new Promise((resolve) => {
req.session.save((err) => {
if (err) console.error('Ошибка при сохранении сессии:', err);
resolve();
});
});
}
}
}
// Создаем сообщение для проверки подписи
const message = `Sign this message to authenticate with DApp for Business. Nonce: ${nonce}`;
// Формируем ожидаемое сообщение
const expectedMessage = `Подтвердите вход в DApp for Business с nonce: ${req.session.authNonce}`;
// Восстанавливаем адрес из подписи
const recoveredAddress = ethers.verifyMessage(message, signature);
// Проверяем, что адрес совпадает с ожидаемым
if (req.session.pendingAddress && req.session.pendingAddress.toLowerCase() !== address.toLowerCase()) {
console.error('Адрес не совпадает с ожидаемым:', {
expected: req.session.pendingAddress,
received: address,
});
return res.status(400).json({ error: 'Invalid address' });
}
let verified = false;
try {
// Проверяем подпись с использованием ethers.js
const recoveredAddress = ethers.verifyMessage(expectedMessage, signature);
// Проверяем, что восстановленный адрес совпадает с предоставленным
if (recoveredAddress.toLowerCase() !== address.toLowerCase()) {
console.error('Неверная подпись:', {
expected: address.toLowerCase(),
recovered: recoveredAddress.toLowerCase(),
});
return res.status(400).json({ error: 'Invalid signature' });
}
// Проверяем, существует ли пользователь в базе данных
const user = await db.query('SELECT * FROM users WHERE address = $1', [address]);
let userId;
let isAdmin = false;
if (user.rows.length === 0) {
// Если пользователь не существует, создаем его
const newUser = await db.query(
'INSERT INTO users (address, created_at) VALUES ($1, NOW()) RETURNING id',
[address]
);
userId = newUser.rows[0].id;
} else {
userId = user.rows[0].id;
isAdmin = user.rows[0].is_admin || false;
verified = true;
console.log('Подпись успешно проверена');
} catch (error) {
console.error('Ошибка при проверке подписи:', error);
return res.status(400).json({ error: 'Invalid signature format' });
}
// Устанавливаем состояние аутентификации в сессии
// Если подпись верна, аутентифицируем пользователя
if (verified) {
// Найдем или создадим пользователя
const user = await findOrCreateUser(address, 'wallet');
// Обновляем сессию
req.session.authenticated = true;
req.session.address = address;
req.session.isAdmin = isAdmin;
req.session.userId = user.id;
req.session.authType = 'wallet';
req.session.userId = userId;
req.session.isAdmin = user.is_admin;
req.session.role = user.role;
req.session.authChannel = 'web';
req.session.language = req.body.language || 'en';
// Удаляем nonce из сессии
delete req.session.nonce;
// Удаляем временные данные
delete req.session.authNonce;
delete req.session.pendingAddress;
// Явно сохраняем сессию
req.session.save((err) => {
// Сохраняем сессию
await new Promise((resolve, reject) => {
req.session.save(err => {
if (err) {
// Удалите или закомментируйте эти логи
// console.error('Error saving session:', err);
return res.status(500).json({ error: 'Failed to save session' });
console.error('Ошибка при сохранении сессии:', err);
reject(err);
} else {
resolve();
}
});
});
// Удалите или закомментируйте
// console.log('Authentication successful:', {
// address,
// isAdmin,
// sessionID: req.sessionID
// });
console.log('Аутентификация успешна:', {
address,
isAdmin: user.is_admin,
userId: user.id,
role: user.role
});
res.json({
authenticated: true,
address,
isAdmin,
authType: 'wallet'
});
isAdmin: user.is_admin,
role: user.role
});
} else {
res.status(401).json({ error: 'Invalid signature' });
}
} catch (error) {
console.error('Error verifying signature:', error);
// Более подробная обработка ошибок
if (error.message.includes('invalid signature')) {
return res.status(400).json({
error: 'Недействительная подпись',
message: 'Подпись не соответствует адресу. Пожалуйста, попробуйте снова.'
});
}
if (error.message.includes('invalid address')) {
return res.status(400).json({
error: 'Недействительный адрес',
message: 'Указанный адрес имеет неверный формат.'
});
}
res.status(500).json({
error: 'Ошибка верификации подписи',
message: 'Не удалось проверить подпись. Пожалуйста, попробуйте снова позже.'
});
console.error('Authentication error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Маршрут для проверки состояния аутентификации
// Проверка текущей сессии
router.get('/check', (req, res) => {
// Удалите или закомментируйте эти логи
// console.log('Session check:', {
// session: req.session,
// authenticated: req.session.authenticated
// });
console.log('Сессия при проверке:', req.session);
if (req.session.authenticated) {
// Если сессия существует и пользователь аутентифицирован
if (req.session && req.session.authenticated) {
res.json({
authenticated: true,
address: req.session.address,
isAdmin: req.session.isAdmin,
authType: req.session.authType
isAdmin: req.session.isAdmin || false,
role: req.session.role || 'USER'
});
} else {
res.json({
@@ -226,17 +243,35 @@ router.get('/check', (req, res) => {
}
});
// Маршрут для выхода из системы
// Обработчик выхода из системы
router.post('/logout', (req, res) => {
req.session.destroy(err => {
try {
// Сохраняем sessionID перед удалением сессии
const sessionID = req.sessionID;
// Удаляем сессию из хранилища
req.session.destroy(async (err) => {
if (err) {
// Удалите или закомментируйте эти логи
// console.error('Error destroying session:', err);
return res.status(500).json({ error: 'Failed to logout' });
console.error('Ошибка при удалении сессии:', err);
return res.status(500).json({ error: 'Internal server error' });
}
try {
// Удаляем запись из базы данных
await db.query('DELETE FROM sessions WHERE sid = $1', [sessionID]);
console.log(`Сессия ${sessionID} удалена из базы данных`);
} catch (dbErr) {
console.error('Ошибка при удалении сессии из базы данных:', dbErr);
}
// Очищаем cookie
res.clearCookie('dapp.sid');
res.json({ success: true });
});
} catch (error) {
console.error('Logout error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Маршрут для авторизации через Telegram
@@ -344,7 +379,7 @@ router.post('/email/verify', async (req, res) => {
authenticated: true,
address: email,
isAdmin,
authType: 'email'
authType: 'email',
});
} catch (error) {
// Удалите или закомментируйте эти логи
@@ -354,4 +389,176 @@ router.post('/email/verify', async (req, res) => {
}
});
// Добавляем маршрут для проверки прав доступа
router.get('/check-access', requireAuth, (req, res) => {
try {
// Получаем информацию о пользователе
const userData = {
address: req.session.address,
isAdmin: req.session.isAdmin || false,
roles: req.session.roles || [],
authenticated: true,
};
// Проверяем доступ к различным разделам
const access = {
dashboard: true, // Все аутентифицированные пользователи имеют доступ к панели управления
admin: userData.isAdmin, // Только администраторы имеют доступ к админке
contracts: userData.roles.includes('CONTRACT_MANAGER') || userData.isAdmin,
users: userData.roles.includes('USER_MANAGER') || userData.isAdmin,
};
res.json({
user: userData,
access: access,
});
} catch (error) {
console.error('Ошибка при проверке прав доступа:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Упрощенный маршрут для обновления сессии
router.post('/refresh-session', async (req, res) => {
try {
const { address } = req.body;
if (!address) {
return res.status(400).json({ success: false, message: 'Адрес не указан' });
}
console.log(`Получен запрос на обновление сессии для адреса: ${address}`);
// Проверяем, существует ли пользователь в базе данных
const userResult = await pool.query('SELECT * FROM users WHERE address = $1', [
address.toLowerCase(),
]);
let user = null;
if (userResult.rows.length > 0) {
user = userResult.rows[0];
console.log(`Найден пользователь: ${user.id}`);
} else {
console.log(`Пользователь с адресом ${address} не найден`);
}
// Обновляем сессию
req.session.authenticated = true;
req.session.address = address.toLowerCase();
if (user) {
req.session.userId = user.id;
req.session.isAdmin = user.is_admin || false;
req.session.role = user.is_admin ? 'ADMIN' : 'USER';
} else {
// Если пользователь не найден в базе, проверяем через переменные окружения
const adminAddresses = (process.env.ADMIN_ADDRESSES || '')
.split(',')
.map((a) => a.toLowerCase());
const isAdmin = adminAddresses.includes(address.toLowerCase());
req.session.isAdmin = isAdmin;
req.session.role = isAdmin ? 'ADMIN' : 'USER';
}
// Сохраняем сессию
await new Promise((resolve, reject) => {
req.session.save((err) => {
if (err) {
console.error('Ошибка при сохранении сессии:', err);
reject(err);
} else {
resolve();
}
});
});
console.log('Сессия обновлена:', req.session);
return res.json({
success: true,
message: 'Сессия обновлена',
user: {
id: user ? user.id : null,
address: address.toLowerCase(),
isAdmin: req.session.isAdmin,
role: req.session.role,
},
});
} catch (error) {
console.error('Ошибка при обновлении сессии:', error);
return res.status(500).json({ success: false, message: 'Ошибка сервера' });
}
});
// Маршрут для обновления статуса администратора
router.post('/update-admin-status', async (req, res) => {
try {
const { address, isAdmin } = req.body;
if (!address) {
return res.status(400).json({ error: 'Address is required' });
}
console.log(`Запрос на обновление статуса администратора для адреса ${address} на ${isAdmin}`);
// Проверяем, существует ли пользователь
const userResult = await db.query('SELECT * FROM users WHERE address = $1', [
address.toLowerCase(),
]);
if (userResult.rows.length === 0) {
// Если пользователь не найден, создаем его
await db.query('INSERT INTO users (address, is_admin, created_at) VALUES ($1, $2, NOW())', [
address.toLowerCase(),
isAdmin,
]);
console.log(
`Создан новый пользователь с адресом ${address} и статусом администратора ${isAdmin}`
);
} else {
// Если пользователь найден, обновляем его статус
await db.query('UPDATE users SET is_admin = $1 WHERE address = $2', [
isAdmin,
address.toLowerCase(),
]);
console.log(
`Обновлен статус администратора для пользователя с адресом ${address} на ${isAdmin}`
);
}
res.json({ success: true });
} catch (error) {
console.error('Ошибка при обновлении статуса администратора:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Маршрут для проверки структуры таблицы users
router.get('/check-db-structure', async (req, res) => {
try {
// Получаем информацию о таблице users
const tableInfo = await pool.query(`
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'users'
`);
res.json({
tableStructure: tableInfo.rows,
});
} catch (error) {
console.error('Ошибка при получении структуры базы данных:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Добавьте обработку ошибок
router.use((err, req, res, next) => {
console.error('Auth route error:', err);
res.status(500).json({ success: false, message: 'Ошибка сервера' });
});
module.exports = { router };

View File

@@ -1,184 +1,199 @@
const express = require('express');
const router = express.Router();
const { checkAccess } = require('../utils/access-check');
const { createOllamaChain, directOllamaQuery, checkOllamaAvailability, ChatOllama } = require('../services/ollama');
const { ChatOllama } = require('@langchain/ollama');
const { getVectorStore } = require('../services/vectorStore');
const db = require('../db');
const { requireAuth, requireAdmin } = require('../middleware/auth');
const logger = require('../utils/logger');
// Хранилище истории чатов
const chatHistory = {};
// Обработка чат-сообщений с проверкой сессии
router.post('/', async (req, res) => {
// Обработчик сообщений чата
router.post('/message', requireAuth, async (req, res) => {
try {
console.log('Получен запрос в chat.js:', {
body: req.body,
session: req.session ? {
id: req.sessionID,
address: req.session.address,
isAuthenticated: req.session.isAuthenticated,
authenticated: req.session.authenticated
} : null,
cookies: req.cookies,
headers: {
cookie: req.headers.cookie,
origin: req.headers.origin,
referer: req.headers.referer,
'content-type': req.headers['content-type']
}
});
const { message, language = 'ru' } = req.body;
// Проверяем, что тело запроса правильно парсится
if (req.headers['content-type'] === 'application/json') {
console.log('JSON body:', JSON.stringify(req.body));
// Проверка аутентификации
if (!req.session || !req.session.authenticated) {
return res.status(401).json({ error: 'Требуется аутентификация' });
}
console.log(`Получено сообщение: ${message}, язык: ${language}`);
// Определяем язык сообщения, если не указан явно
let detectedLanguage = language;
if (!language || language === 'auto') {
// Простая эвристика для определения языка
const cyrillicPattern = /[а-яА-ЯёЁ]/;
detectedLanguage = cyrillicPattern.test(message) ? 'ru' : 'en';
}
// Формируем системный промпт в зависимости от языка
let systemPrompt = '';
if (detectedLanguage === 'ru') {
systemPrompt = 'Вы - полезный ассистент. Отвечайте на русском языке.';
} else {
console.log('Non-JSON body:', req.body);
systemPrompt = 'You are a helpful assistant. Respond in English.';
}
// ВАЖНО: Принимаем любой адрес из запроса без проверки сессии
const userAddress = req.body.address || '0xdefault';
// Отправляем запрос к Ollama с указанием языка
console.log(`Отправка запроса к Ollama (модель: ${process.env.OLLAMA_MODEL || 'mistral'}, язык: ${detectedLanguage}): ${message}`);
const { message } = req.body;
if (!message) {
return res.status(400).json({ error: 'Message is required' });
}
console.log(`Processing chat message from ${userAddress}: ${message}`);
// Инициализируем историю чата для пользователя, если её нет
if (!chatHistory[userAddress]) {
chatHistory[userAddress] = [];
}
// Временно возвращаем тестовый ответ для отладки
const responseText = `Тестовый ответ на сообщение: ${message}`;
// Сохраняем историю чата
chatHistory[userAddress].push({
type: 'human',
text: message
});
chatHistory[userAddress].push({
type: 'ai',
text: responseText
});
return res.json({ response: responseText });
} catch (error) {
console.error('Подробная ошибка:', error.stack);
console.error('Chat error:', error);
res.status(500).json({
error: "Извините, произошла ошибка при обработке вашего запроса. Пожалуйста, попробуйте позже."
});
}
});
// Добавьте новый эндпоинт для проверки сессии
router.get('/check-session', (req, res) => {
// Проверяем доступность Ollama
console.log('Проверка доступности Ollama...');
try {
console.log('Проверка сессии в chat.js:', {
sessionID: req.sessionID,
session: req.session ? {
isAuthenticated: req.session.isAuthenticated,
authenticated: req.session.authenticated,
address: req.session.address
} : null,
cookies: req.cookies,
const response = await fetch(`${process.env.OLLAMA_BASE_URL || 'http://localhost:11434'}/api/tags`);
const data = await response.json();
console.log('Ollama доступен. Доступные модели:');
data.models.forEach(model => {
console.log(`- ${model.name}`);
});
} catch (error) {
console.error('Ошибка при проверке доступности Ollama:', error);
return res.status(500).json({ error: 'Сервис Ollama недоступен' });
}
// Создаем экземпляр ChatOllama
const chat = new ChatOllama({
baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
model: process.env.OLLAMA_MODEL || 'mistral',
system: systemPrompt
});
console.log('Отправка запроса к Ollama...');
// Получаем ответ от модели
let aiResponse;
try {
const response = await chat.invoke(message);
aiResponse = response.content;
console.log('Ответ AI:', aiResponse);
} catch (error) {
console.error('Ошибка при вызове ChatOllama:', error);
// Альтернативный метод запроса через прямой API
try {
console.log('Пробуем альтернативный метод запроса...');
const response = await fetch(`${process.env.OLLAMA_BASE_URL || 'http://localhost:11434'}/api/generate`, {
method: 'POST',
headers: {
cookie: req.headers.cookie
}
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: process.env.OLLAMA_MODEL || 'mistral',
prompt: message,
system: systemPrompt,
stream: false
}),
});
// Если сессия отсутствует, но есть адрес в куки authToken, создаем временную сессию
if ((!req.session || (!req.session.isAuthenticated && !req.session.authenticated)) && req.cookies.authToken) {
console.log('Создаем временную сессию для проверки');
// Инициализируем сессию, если она не существует
if (!req.session) {
req.session = {};
}
req.session.isAuthenticated = true;
req.session.authenticated = true;
req.session.isAdmin = true;
return res.json({
success: true,
message: 'Temporary session created',
isAdmin: true
});
}
if (!req.session) {
return res.status(401).json({ error: 'No session' });
}
if (!req.session.isAuthenticated && !req.session.authenticated) {
return res.status(401).json({ error: 'Unauthorized' });
const data = await response.json();
aiResponse = data.response;
console.log('Ответ AI (альтернативный метод):', aiResponse);
} catch (fallbackError) {
console.error('Ошибка при использовании альтернативного метода:', fallbackError);
throw error; // Выбрасываем исходную ошибку
}
}
// Отправляем ответ клиенту
res.json({
success: true,
address: req.session.address,
isAdmin: req.session.isAdmin
reply: aiResponse,
language: detectedLanguage
});
} catch (error) {
console.error('Ошибка при проверке сессии:', error);
res.status(500).json({ error: 'Internal server error' });
logger.error('Error processing message:', error);
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
}
});
// Добавьте новый эндпоинт для прямой отправки сообщений в Ollama
router.post('/ollama', async (req, res) => {
// Добавьте этот маршрут для проверки доступных моделей
router.get('/models', async (req, res) => {
try {
const { message, model = 'mistral' } = req.body;
const ollama = new Ollama();
const models = await ollama.list();
console.log(`Отправка сообщения в Ollama (${model}):`, message);
if (!message) {
return res.status(400).json({ error: 'Message is required' });
}
// Используем функцию directOllamaQuery вместо создания нового экземпляра ChatOllama
const result = await directOllamaQuery(message, model);
console.log('Ответ от Ollama:', result);
// Возвращаем ответ клиенту
res.json({
response: result,
model: model
success: true,
models: models.models.map((model) => model.name),
});
} catch (error) {
console.error('Ошибка при отправке сообщения в Ollama:', error);
res.status(500).json({
error: "Ошибка при отправке сообщения в Ollama. Убедитесь, что сервер Ollama запущен."
});
console.error('Ошибка при получении списка моделей:', error);
res.status(500).json({ success: false, message: 'Ошибка сервера' });
}
});
// Проверьте, что маршрут правильно настроен
router.post('/message', async (req, res) => {
// Маршрут для получения истории диалогов (доступен пользователю для своих диалогов)
router.get('/history', requireAuth, async (req, res) => {
try {
const { message } = req.body;
const userId = req.session.userId;
const { limit = 50, offset = 0 } = req.query;
if (!message) {
return res.status(400).json({ error: 'Message is required' });
const result = await db.query(`
SELECT id, channel, sender_type, content, metadata, created_at
FROM chat_history
WHERE user_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3
`, [userId, limit, offset]);
res.json(result.rows);
} catch (error) {
logger.error('Error fetching chat history:', error);
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
}
});
// Маршрут для получения всех диалогов (только для админов)
router.get('/admin/history', requireAdmin, async (req, res) => {
try {
const { limit = 50, offset = 0, userId } = req.query;
let query = `
SELECT ch.id, ch.user_id, u.username, ch.channel,
ch.sender_type, ch.content, ch.metadata, ch.created_at
FROM chat_history ch
LEFT JOIN users u ON ch.user_id = u.id
`;
const params = [];
let paramIndex = 1;
if (userId) {
query += ` WHERE ch.user_id = $${paramIndex}`;
params.push(userId);
paramIndex++;
}
console.log('Получено сообщение:', message);
query += ` ORDER BY ch.created_at DESC LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
params.push(limit, offset);
// Здесь ваш код обработки сообщения
// ...
const result = await db.query(query, params);
// Временный ответ для тестирования
res.json({
response: `Это тестовый ответ на ваше сообщение: "${message}". Сервер работает.`
});
res.json(result.rows);
} catch (error) {
console.error('Error processing message:', error);
res.status(500).json({ error: 'Internal server error' });
logger.error('Error fetching admin chat history:', error);
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
}
});
// Сохранение сообщения в историю чата
router.post('/message', requireAuth, async (req, res) => {
try {
const { content, channel = 'web', metadata = {} } = req.body;
const userId = req.session.userId;
// Сохранение сообщения пользователя
const userMessageResult = await db.query(`
INSERT INTO chat_history (user_id, channel, sender_type, content, metadata)
VALUES ($1, $2, 'user', $3, $4)
RETURNING id
`, [userId, channel, content, metadata]);
const messageId = userMessageResult.rows[0].id;
res.json({ success: true, messageId });
} catch (error) {
logger.error('Error saving chat message:', error);
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
}
});

View File

@@ -9,9 +9,9 @@ router.get('/', (req, res) => {
contracts: [
{
name: 'AccessToken',
address: process.env.ACCESS_TOKEN_ADDRESS
}
]
address: process.env.ACCESS_TOKEN_ADDRESS,
},
],
});
});
@@ -23,9 +23,9 @@ router.get('/details', requireRole('ADMIN'), (req, res) => {
{
name: 'AccessToken',
address: process.env.ACCESS_TOKEN_ADDRESS,
network: process.env.ETHEREUM_NETWORK_URL.includes('sepolia') ? 'Sepolia' : 'Unknown'
}
]
network: process.env.ETHEREUM_NETWORK_URL.includes('sepolia') ? 'Sepolia' : 'Unknown',
},
],
});
});

View File

View File

@@ -1,188 +1,32 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
// Эндпоинт для отладки сессий
router.get('/session', (req, res) => {
try {
console.log('Отладка сессии:', {
sessionID: req.sessionID,
session: req.session ? {
isAuthenticated: req.session.isAuthenticated,
authenticated: req.session.authenticated,
address: req.session.address,
isAdmin: req.session.isAdmin
} : null,
cookies: req.cookies,
headers: {
cookie: req.headers.cookie
}
});
res.json({
sessionID: req.sessionID,
session: req.session ? {
isAuthenticated: req.session.isAuthenticated,
authenticated: req.session.authenticated,
address: req.session.address,
isAdmin: req.session.isAdmin
} : null,
cookies: req.cookies
});
} catch (error) {
console.error('Ошибка при отладке сессии:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Эндпоинт для создания тестовой сессии
router.post('/create-session', (req, res) => {
const { address } = req.body;
if (!address) {
return res.status(400).json({ error: 'Address is required' });
}
// Инициализируем сессию, если она не существует
if (!req.session) {
req.session = {};
}
req.session.isAuthenticated = true;
req.session.authenticated = true;
req.session.address = address.toLowerCase();
req.session.isAdmin = true;
// Сохраняем сессию
req.session.save((err) => {
if (err) {
console.error('Ошибка сохранения тестовой сессии:', err);
return res.status(500).json({ error: 'Session save error' });
}
console.log('Тестовая сессия создана:', {
sessionID: req.sessionID,
session: {
isAuthenticated: req.session.isAuthenticated,
authenticated: req.session.authenticated,
address: req.session.address,
isAdmin: req.session.isAdmin
}
});
res.cookie('authToken', 'true', {
maxAge: 86400000,
httpOnly: false,
secure: false,
sameSite: 'lax',
path: '/'
});
res.json({
success: true,
sessionID: req.sessionID,
address: req.session.address,
isAdmin: req.session.isAdmin
});
});
});
// Тестовый эндпоинт для отправки сообщений без проверки сессии
router.post('/test-chat', (req, res) => {
try {
const { message, address } = req.body;
console.log('Тестовый чат-запрос:', {
message,
address,
headers: {
cookie: req.headers.cookie,
'content-type': req.headers['content-type']
},
cookies: req.cookies,
session: req.session ? {
isAuthenticated: req.session.isAuthenticated,
authenticated: req.session.authenticated,
address: req.session.address,
isAdmin: req.session.isAdmin
} : null
});
if (!message) {
return res.status(400).json({ error: 'Message is required' });
}
// Возвращаем тестовый ответ
res.json({
response: `Тестовый ответ на сообщение: ${message}`,
receivedAddress: address,
sessionAddress: req.session?.address
});
} catch (error) {
console.error('Ошибка в тестовом чате:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Тестовый эндпоинт для проверки соединения
router.get('/ping', (req, res) => {
res.json({
message: 'pong',
timestamp: new Date().toISOString(),
server: {
port: process.env.PORT || 8080,
address: req.socket.localAddress,
hostname: require('os').hostname()
}
});
});
// Тестовый эндпоинт для проверки Ollama
router.get('/ollama-test', async (req, res) => {
try {
const { directOllamaQuery } = require('../services/ollama');
// Тестовый запрос к Ollama
const result = await directOllamaQuery('Привет, как дела?', 'mistral');
res.json({
success: true,
response: result,
model: 'mistral'
});
} catch (error) {
console.error('Ошибка при тестировании Ollama:', error);
res.status(500).json({
success: false,
error: error.message || 'Ошибка при тестировании Ollama'
});
}
});
// Тестовый эндпоинт для проверки доступности Ollama
router.get('/ollama-status', async (req, res) => {
try {
const { checkOllamaAvailability } = require('../services/ollama');
// Проверяем доступность Ollama
const isAvailable = await checkOllamaAvailability();
if (isAvailable) {
// Маршрут для проверки состояния сервера
router.get('/status', (req, res) => {
res.json({
status: 'ok',
message: 'Ollama доступен'
uptime: process.uptime(),
timestamp: Date.now(),
});
} else {
res.status(503).json({
status: 'error',
message: 'Ollama недоступен'
});
// Маршрут для проверки сессии
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('Ошибка при проверке доступности Ollama:', error);
res.status(500).json({
status: 'error',
message: error.message || 'Ошибка при проверке доступности Ollama'
});
console.error('Ошибка при получении данных из таблицы session:', error);
res.status(500).json({ error: 'Internal server error' });
}
});

View File

@@ -18,17 +18,17 @@ router.get('/', async (req, res) => {
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'
heapUsed: Math.round(memoryUsage.heapUsed / 1024 / 1024) + 'MB',
},
database: {
connected: true,
timestamp: dbResult.rows[0].now
}
timestamp: dbResult.rows[0].now,
},
});
} catch (error) {
res.status(500).json({
status: 'error',
error: error.message
error: error.message,
});
}
});

View File

@@ -6,7 +6,7 @@ const { Pool } = require('pg');
// Подключение к БД
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
});
// Middleware для проверки аутентификации
@@ -21,10 +21,9 @@ function requireAuth(req, res, next) {
router.get('/', requireAuth, async (req, res) => {
try {
// Получаем ID пользователя по Ethereum-адресу
const result = await pool.query(
'SELECT id FROM users WHERE address = $1',
[req.session.address]
);
const result = await pool.query('SELECT id FROM users WHERE address = $1', [
req.session.address,
]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'User not found' });
@@ -48,10 +47,9 @@ router.delete('/:type/:value', requireAuth, async (req, res) => {
const { type, value } = req.params;
// Получаем ID пользователя по Ethereum-адресу
const result = await pool.query(
'SELECT id FROM users WHERE address = $1',
[req.session.address]
);
const result = await pool.query('SELECT id FROM users WHERE address = $1', [
req.session.address,
]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'User not found' });

View File

@@ -1,340 +0,0 @@
const express = require('express');
const router = express.Router();
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
});
// Middleware для проверки аутентификации
function requireAuth(req, res, next) {
if (!req.session || (!req.session.isAuthenticated && !req.session.authenticated)) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
}
// Получение всех досок пользователя
router.get('/boards', async (req, res) => {
try {
// Для разработки: если сессия не содержит адрес, используем тестовый
const userAddress = (req.session.address || '0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b').toLowerCase();
console.log('Запрос досок для адреса:', userAddress);
// Проверяем, существует ли пользователь
const userResult = await pool.query(
'SELECT id FROM users WHERE address = $1',
[userAddress]
);
console.log('Результат запроса пользователя:', userResult.rows);
if (userResult.rows.length === 0) {
console.log('Пользователь не найден, создаем нового');
// Если пользователь не найден, создаем его
const newUserResult = await pool.query(
'INSERT INTO users (address, created_at) VALUES ($1, NOW()) RETURNING id',
[userAddress]
);
console.log('Создан новый пользователь:', newUserResult.rows);
}
// Получаем доски пользователя
const ownBoardsQuery = 'SELECT kb.* FROM kanban_boards kb ' +
'JOIN users u ON kb.owner_id = u.id ' +
'WHERE u.address = $1 ' +
'ORDER BY kb.updated_at DESC';
console.log('Запрос досок пользователя:', ownBoardsQuery);
const ownBoardsResult = await pool.query(ownBoardsQuery, [userAddress]);
console.log('Результат запроса досок пользователя:', ownBoardsResult.rows);
// Получаем доски, к которым у пользователя есть доступ
const sharedBoardsResult = await pool.query(
'SELECT kb.* FROM kanban_boards kb ' +
'JOIN kanban_board_access kba ON kb.id = kba.board_id ' +
'JOIN users u1 ON kba.user_id = u1.id ' +
'JOIN users u2 ON kb.owner_id = u2.id ' +
'WHERE u1.address = $1 AND u2.address != $1 ' +
'ORDER BY kb.updated_at DESC',
[userAddress]
);
// Получаем публичные доски
const publicBoardsResult = await pool.query(
'SELECT kb.* FROM kanban_boards kb ' +
'JOIN users u ON kb.owner_id = u.id ' +
'WHERE kb.is_public = true AND u.address != $1 ' +
'AND NOT EXISTS (' +
' SELECT 1 FROM kanban_board_access kba ' +
' JOIN users u2 ON kba.user_id = u2.id ' +
' WHERE kba.board_id = kb.id AND u2.address = $1' +
') ' +
'ORDER BY kb.updated_at DESC',
[userAddress]
);
res.json({
ownBoards: ownBoardsResult.rows,
sharedBoards: sharedBoardsResult.rows,
publicBoards: publicBoardsResult.rows
});
} catch (error) {
console.error('Error fetching boards:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Создание новой доски
router.post('/boards', requireAuth, async (req, res) => {
try {
const { title, description, isPublic } = req.body;
// Получаем ID пользователя
let userResult = await pool.query(
'SELECT id FROM users WHERE address = $1',
[req.session.address]
);
let userId;
if (userResult.rows.length === 0) {
// Если пользователь не найден, создаем его
const newUserResult = await pool.query(
'INSERT INTO users (address, created_at, preferred_language) VALUES ($1, NOW(), $2) RETURNING id',
[req.session.address, 'ru']
);
userId = newUserResult.rows[0].id;
} else {
userId = userResult.rows[0].id;
}
// Создаем новую доску
const result = await pool.query(
`INSERT INTO kanban_boards (title, description, owner_id, is_public, created_at, updated_at)
VALUES ($1, $2, $3, $4, NOW(), NOW())
RETURNING *`,
[title, description, userId, isPublic]
);
// Создаем стандартные колонки
const columns = ['Backlog', 'In Progress', 'Review', 'Done'];
for (let i = 0; i < columns.length; i++) {
await pool.query(
`INSERT INTO kanban_columns (board_id, title, position, created_at, updated_at)
VALUES ($1, $2, $3, NOW(), NOW())`,
[result.rows[0].id, columns[i], i]
);
}
res.status(201).json(result.rows[0]);
} catch (error) {
console.error('Error creating kanban board:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Получение конкретной доски со всеми колонками и карточками
router.get('/boards/:id', requireAuth, async (req, res) => {
try {
const boardId = req.params.id;
// Получаем ID пользователя
let userResult = await pool.query(
'SELECT id FROM users WHERE address = $1',
[req.session.address]
);
let userId;
if (userResult.rows.length === 0) {
// Если пользователь не найден, создаем его
const newUserResult = await pool.query(
'INSERT INTO users (address, created_at, preferred_language) VALUES ($1, NOW(), $2) RETURNING id',
[req.session.address, 'ru']
);
userId = newUserResult.rows[0].id;
} else {
userId = userResult.rows[0].id;
}
// Проверяем доступ к доске
const boardResult = await pool.query(
'SELECT * FROM kanban_boards WHERE id = $1',
[boardId]
);
if (boardResult.rows.length === 0) {
return res.status(404).json({ error: 'Board not found' });
}
const board = boardResult.rows[0];
// Проверяем, имеет ли пользователь доступ к доске
if (board.owner_id !== userId && !board.is_public) {
const accessResult = await pool.query(
'SELECT * FROM kanban_board_access WHERE board_id = $1 AND user_id = $2',
[boardId, userId]
);
if (accessResult.rows.length === 0) {
return res.status(403).json({ error: 'Access denied' });
}
}
// Получаем колонки доски
const columnsResult = await pool.query(
'SELECT * FROM kanban_columns WHERE board_id = $1 ORDER BY position',
[boardId]
);
// Получаем карточки для всех колонок
const cardsResult = await pool.query(
`SELECT kc.*, u.address as assigned_address
FROM kanban_cards kc
LEFT JOIN users u ON kc.assigned_to = u.id
WHERE kc.column_id IN (
SELECT id FROM kanban_columns WHERE board_id = $1
)
ORDER BY kc.position`,
[boardId]
);
// Группируем карточки по колонкам
const columns = columnsResult.rows.map(column => {
const cards = cardsResult.rows.filter(card => card.column_id === column.id);
return {
...column,
cards
};
});
res.json({
...board,
columns
});
} catch (error) {
console.error('Error getting kanban board:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Добавление колонки к доске
router.post('/boards/:boardId/columns', requireAuth, async (req, res) => {
try {
const { boardId } = req.params;
const { title, wipLimit } = req.body;
// Проверяем, существует ли доска
const boardResult = await pool.query(
'SELECT * FROM kanban_boards WHERE id = $1',
[boardId]
);
if (boardResult.rows.length === 0) {
return res.status(404).json({ error: 'Board not found' });
}
// Получаем максимальную позицию колонок
const positionResult = await pool.query(
'SELECT MAX(position) as max_position FROM kanban_columns WHERE board_id = $1',
[boardId]
);
const position = positionResult.rows[0].max_position ? positionResult.rows[0].max_position + 1 : 0;
// Создаем новую колонку
const result = await pool.query(
`INSERT INTO kanban_columns (board_id, title, position, wip_limit, created_at, updated_at)
VALUES ($1, $2, $3, $4, NOW(), NOW())
RETURNING *`,
[boardId, title, position, wipLimit]
);
res.status(201).json(result.rows[0]);
} catch (error) {
console.error('Error creating column:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Получение колонок доски
router.get('/boards/:boardId/columns', requireAuth, async (req, res) => {
try {
const { boardId } = req.params;
const result = await pool.query(
'SELECT * FROM kanban_columns WHERE board_id = $1 ORDER BY position',
[boardId]
);
res.json(result.rows);
} catch (error) {
console.error('Error getting columns:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Создание карточки
router.post('/cards', requireAuth, async (req, res) => {
try {
const { title, description, columnId, dueDate } = req.body;
// Получаем ID пользователя
let userResult = await pool.query(
'SELECT id FROM users WHERE address = $1',
[req.session.address]
);
let userId;
if (userResult.rows.length === 0) {
// Если пользователь не найден, создаем его
const newUserResult = await pool.query(
'INSERT INTO users (address, created_at, preferred_language) VALUES ($1, NOW(), $2) RETURNING id',
[req.session.address, 'ru']
);
userId = newUserResult.rows[0].id;
} else {
userId = userResult.rows[0].id;
}
// Получаем максимальную позицию карточек в колонке
const positionResult = await pool.query(
'SELECT MAX(position) as max_position FROM kanban_cards WHERE column_id = $1',
[columnId]
);
const position = positionResult.rows[0].max_position ? positionResult.rows[0].max_position + 1 : 0;
// Создаем новую карточку
const result = await pool.query(
`INSERT INTO kanban_cards (column_id, title, description, position, due_date, created_by, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
RETURNING *`,
[columnId, title, description, position, dueDate, userId]
);
// Получаем информацию о пользователе для отображения
const cardWithUser = {
...result.rows[0],
assigned_address: null
};
res.status(201).json(cardWithUser);
} catch (error) {
console.error('Error creating card:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Добавляем остальные маршруты для работы с колонками, карточками и т.д.
// ...
module.exports = router;

246
backend/routes/messages.js Normal file
View File

@@ -0,0 +1,246 @@
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;

56
backend/routes/roles.js Normal file
View File

@@ -0,0 +1,56 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
const { requireAuth, requireAdmin } = require('../middleware/auth');
const { checkTokenBalanceAndUpdateRole } = require('../utils/access-check');
const logger = require('../utils/logger');
// Маршрут для проверки и обновления роли пользователя
router.post('/check-role', requireAuth, async (req, res) => {
try {
if (!req.session.address) {
return res.status(400).json({ error: 'В сессии отсутствует адрес кошелька' });
}
const isAdmin = await checkTokenBalanceAndUpdateRole(req.session.address);
// Обновление сессии
req.session.isAdmin = isAdmin;
res.json({ isAdmin });
} catch (error) {
logger.error('Error checking role:', error);
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
}
});
// Маршрут для получения всех ролей (только для админов)
router.get('/', requireAdmin, async (req, res) => {
try {
const result = await db.query('SELECT * FROM roles ORDER BY id');
res.json(result.rows);
} catch (error) {
logger.error('Error fetching roles:', error);
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
}
});
// Маршрут для получения пользователей с их ролями (только для админов)
router.get('/users', requireAdmin, async (req, res) => {
try {
const result = await db.query(`
SELECT u.id, u.username, u.preferred_language, r.name as role,
u.created_at, u.last_token_check
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
ORDER BY u.created_at DESC
`);
res.json(result.rows);
} catch (error) {
logger.error('Error fetching users with roles:', error);
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
}
});
module.exports = router;

View File

@@ -1,5 +1,8 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
const logger = require('../utils/logger');
const { requireAuth } = require('../middleware/auth');
// Получение списка пользователей
router.get('/', (req, res) => {
@@ -11,8 +14,33 @@ router.get('/:address', (req, res) => {
const { address } = req.params;
res.json({
address,
message: 'User details endpoint'
message: 'User details endpoint',
});
});
// Маршрут для обновления языка пользователя
router.post('/update-language', requireAuth, async (req, res) => {
try {
const { language } = req.body;
const userId = req.session.userId;
// Проверка валидности языка
const validLanguages = ['ru', 'en'];
if (!validLanguages.includes(language)) {
return res.status(400).json({ error: 'Неподдерживаемый язык' });
}
// Обновление языка в базе данных
await db.query(
'UPDATE users SET preferred_language = $1 WHERE id = $2',
[language, userId]
);
res.json({ success: true });
} catch (error) {
logger.error('Error updating language:', error);
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
}
});
module.exports = router;

View File

@@ -4,14 +4,9 @@ const path = require('path');
const packageJson = require('../package.json');
const dependencies = packageJson.dependencies || {};
const requiredDependencies = [
'express-rate-limit',
'winston',
'helmet',
'csurf'
];
const requiredDependencies = ['express-rate-limit', 'winston', 'helmet', 'csurf'];
const missingDependencies = requiredDependencies.filter(dep => !dependencies[dep]);
const missingDependencies = requiredDependencies.filter((dep) => !dependencies[dep]);
if (missingDependencies.length > 0) {
console.error('Missing dependencies:', missingDependencies);

View File

@@ -0,0 +1,125 @@
const fs = require('fs');
const path = require('path');
const { promisify } = require('util');
const readFile = promisify(fs.readFile);
const readdir = promisify(fs.readdir);
const stat = promisify(fs.stat);
// Паттерны для поиска несовместимых конструкций ethers.js v5
const patterns = [
'ethers.providers.JsonRpcProvider',
'ethers.providers.Web3Provider',
'ethers.utils.parseEther',
'ethers.utils.formatEther',
'ethers.utils.formatUnits',
'ethers.utils.parseUnits',
'ethers.utils.verifyMessage',
'ethers.utils.keccak256',
'ethers.utils.toUtf8Bytes',
'ethers.utils.arrayify',
'ethers.utils.hexlify',
'ethers.BigNumber.from',
'ethers.constants.Zero',
'ethers.constants.One',
'ethers.constants.Two',
'ethers.constants.MaxUint256',
'ethers.constants.AddressZero',
'ethers.constants.HashZero',
];
// Соответствующие замены для ethers.js v6.x
const replacements = [
'ethers.JsonRpcProvider',
'ethers.BrowserProvider',
'ethers.parseEther',
'ethers.formatEther',
'ethers.formatUnits',
'ethers.parseUnits',
'ethers.verifyMessage',
'ethers.keccak256',
'ethers.toUtf8Bytes',
'ethers.getBytes',
'ethers.hexlify',
'ethers.getBigInt',
'ethers.ZeroAddress',
'ethers.ZeroAddress',
'ethers.ZeroAddress',
'ethers.MaxUint256',
'ethers.ZeroAddress',
'ethers.ZeroHash',
];
// Функция для рекурсивного обхода директории
async function walkDir(dir, fileList = []) {
const files = await readdir(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const fileStat = await stat(filePath);
if (fileStat.isDirectory()) {
// Пропускаем node_modules и .git
if (file !== 'node_modules' && file !== '.git') {
fileList = await walkDir(filePath, fileList);
}
} else if (file.endsWith('.js')) {
fileList.push(filePath);
}
}
return fileList;
}
// Функция для проверки файла
async function checkFile(filePath) {
try {
if (filePath.includes('check-ethers-v6-compatibility.js')) {
return false; // Пропускаем проверку самого скрипта
}
const content = await readFile(filePath, 'utf8');
let hasIssues = false;
for (let i = 0; i < patterns.length; i++) {
if (content.includes(patterns[i])) {
console.log(`\x1b[33mПроблема в файле ${filePath}:\x1b[0m`);
console.log(` Найдено: \x1b[31m${patterns[i]}\x1b[0m`);
console.log(` Заменить на: \x1b[32m${replacements[i]}\x1b[0m`);
hasIssues = true;
}
}
return hasIssues;
} catch (error) {
console.error(`Ошибка при проверке файла ${filePath}:`, error);
return false;
}
}
// Основная функция
async function main() {
try {
console.log('Проверка совместимости с ethers.js v6.x...');
const files = await walkDir(path.resolve(__dirname, '..'));
let issuesFound = false;
for (const file of files) {
const hasIssues = await checkFile(file);
if (hasIssues) {
issuesFound = true;
}
}
if (!issuesFound) {
console.log('\x1b[32mПроблем не найдено. Код совместим с ethers.js v6.x\x1b[0m');
} else {
console.log('\n\x1b[33mНайдены проблемы совместимости с ethers.js v6.x\x1b[0m');
console.log('Пожалуйста, обновите код в соответствии с рекомендациями выше.');
}
} catch (error) {
console.error('Ошибка при проверке совместимости:', error);
}
}
main();

View File

@@ -0,0 +1,34 @@
const axios = require('axios');
async function checkOllamaModels() {
try {
console.log('Проверка доступных моделей Ollama...');
const baseUrl = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
const response = await axios.get(`${baseUrl}/api/tags`, {
timeout: 5000, // 5 секунд таймаут
});
if (response.status === 200 && response.data && response.data.models) {
console.log('\nДоступные модели Ollama:');
console.log('------------------------');
response.data.models.forEach((model) => {
console.log(`- ${model.name}`);
});
console.log('\nДля использования конкретной модели, укажите ее в .env файле:');
console.log('OLLAMA_EMBEDDINGS_MODEL=mistral');
console.log('OLLAMA_MODEL=mistral');
} else {
console.log('Не удалось получить список моделей');
}
} catch (error) {
console.error('Ошибка при проверке моделей Ollama:', error.message);
console.log('\nУбедитесь, что Ollama запущен. Вы можете запустить его командой:');
console.log('ollama serve');
}
}
// Запускаем проверку
checkOllamaModels();

View File

@@ -1,34 +1,31 @@
const hre = require("hardhat");
const hre = require('hardhat');
async function main() {
const accessToken = await hre.ethers.getContractAt(
"AccessToken",
"0xF352c498cF0857F472dC473E4Dd39551E79B1063"
'AccessToken',
'0xF352c498cF0857F472dC473E4Dd39551E79B1063'
);
const owner = await accessToken.owner();
console.log("Contract owner:", owner);
console.log('Contract owner:', owner);
// Проверяем все токены и их владельцев
console.log("\nAll tokens:");
console.log('\nAll tokens:');
for (let i = 1; i <= 10; i++) {
try {
const tokenOwner = await accessToken.ownerOf(i);
console.log(`Token ${i} owner: ${tokenOwner}`);
} catch (error) {
if (!error.message.includes("invalid token ID")) {
if (!error.message.includes('invalid token ID')) {
console.log(`Token ${i} error:`, error.message);
}
}
}
// Проверяем активные токены для всех известных адресов
const addresses = [
owner,
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
];
const addresses = [owner, '0x70997970C51812dc3A010C7d01b50e0d17dc79C8'];
console.log("\nActive tokens:");
console.log('\nActive tokens:');
for (const address of addresses) {
const activeToken = await accessToken.activeTokens(address);
console.log(`${address}: Token ${activeToken.toString()}`);

View File

@@ -0,0 +1,21 @@
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);
});

View File

@@ -1,42 +1,41 @@
const hre = require("hardhat");
const hre = require('hardhat');
async function main() {
const accessToken = await hre.ethers.getContractAt(
"AccessToken",
"0xF352c498cF0857F472dC473E4Dd39551E79B1063"
'AccessToken',
'0xF352c498cF0857F472dC473E4Dd39551E79B1063'
);
const moderatorAddress = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8";
const moderatorAddress = '0x70997970C51812dc3A010C7d01b50e0d17dc79C8';
try {
console.log("\nMinting moderator token...");
console.log('\nMinting moderator token...');
const mintTx = await accessToken.mintAccessToken(moderatorAddress, 1); // MODERATOR
console.log("Waiting for transaction:", mintTx.hash);
console.log('Waiting for transaction:', mintTx.hash);
await mintTx.wait();
console.log("Moderator token minted");
console.log('Moderator token minted');
// Проверяем результат
const activeToken = await accessToken.activeTokens(moderatorAddress);
console.log(`Moderator's active token: ${activeToken}`);
const role = await accessToken.checkRole(moderatorAddress);
console.log(`Moderator role: ${["ADMIN", "MODERATOR", "SUPPORT"][role]}`);
console.log(`Moderator role: ${['ADMIN', 'MODERATOR', 'SUPPORT'][role]}`);
} catch (error) {
console.log("Moderator token minting error:", error.message);
console.log('Moderator token minting error:', error.message);
}
// Проверяем все активные токены
console.log("\nAll active tokens:");
const addresses = [
await accessToken.owner(),
moderatorAddress
];
console.log('\nAll active tokens:');
const addresses = [await accessToken.owner(), moderatorAddress];
for (const address of addresses) {
try {
const activeToken = await accessToken.activeTokens(address);
const role = await accessToken.checkRole(address);
console.log(`${address}: Token ${activeToken}, Role: ${["ADMIN", "MODERATOR", "SUPPORT"][role]}`);
console.log(
`${address}: Token ${activeToken}, Role: ${['ADMIN', 'MODERATOR', 'SUPPORT'][role]}`
);
} catch (error) {
console.log(`${address}: ${error.message}`);
}
@@ -46,6 +45,6 @@ async function main() {
main()
.then(() => process.exit(0))
.catch((error) => {
console.error("Script error:", error);
console.error('Script error:', error);
process.exit(1);
});

View File

@@ -1,18 +1,18 @@
const hre = require("hardhat");
const hre = require('hardhat');
async function main() {
const AccessToken = await hre.ethers.getContractFactory("AccessToken");
const AccessToken = await hre.ethers.getContractFactory('AccessToken');
const accessToken = await AccessToken.deploy();
await accessToken.waitForDeployment();
const address = await accessToken.getAddress();
console.log("AccessToken deployed to:", address);
console.log('AccessToken deployed to:', address);
// Создаем первый админский токен для владельца контракта
const [owner] = await hre.ethers.getSigners();
const tx = await accessToken.mintAccessToken(owner.address, 0); // 0 = ADMIN
await tx.wait();
console.log("Admin token minted for:", owner.address);
console.log('Admin token minted for:', owner.address);
}
main()

View File

@@ -1,17 +1,17 @@
const hre = require("hardhat");
const hre = require('hardhat');
async function main() {
console.log("Начинаем деплой контракта...");
console.log('Начинаем деплой контракта...');
// Получаем контракт
const MyContract = await hre.ethers.getContractFactory("MyContract");
const MyContract = await hre.ethers.getContractFactory('MyContract');
// Деплоим контракт
const myContract = await MyContract.deploy();
await myContract.waitForDeployment();
const address = await myContract.getAddress();
console.log("Контракт развернут по адресу:", address);
console.log('Контракт развернут по адресу:', address);
}
main()

View File

@@ -5,7 +5,7 @@ dotenv.config();
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
});
async function initDb() {
@@ -40,7 +40,8 @@ async function initDb() {
const boardId = boardResult.rows[0].id;
// Добавляем тестовые колонки
await pool.query(`
await pool.query(
`
INSERT INTO kanban_columns (board_id, title, position)
VALUES
($1, 'Backlog', 0),
@@ -48,7 +49,9 @@ async function initDb() {
($1, 'Review', 2),
($1, 'Done', 3)
ON CONFLICT DO NOTHING
`, [boardId]);
`,
[boardId]
);
}
console.log('База данных инициализирована успешно');

View File

@@ -1,13 +1,13 @@
const hre = require("hardhat");
const hre = require('hardhat');
async function main() {
const accessToken = await hre.ethers.getContractAt(
"AccessToken",
"0xF352c498cF0857F472dC473E4Dd39551E79B1063"
'AccessToken',
'0xF352c498cF0857F472dC473E4Dd39551E79B1063'
);
const owner = await accessToken.owner();
console.log("Contract owner:", owner);
console.log('Contract owner:', owner);
// Создаем админский токен для владельца
try {
@@ -16,34 +16,34 @@ async function main() {
console.log(`Admin token minted for ${owner}`);
const role = await accessToken.checkRole(owner);
console.log("Owner role:", ["ADMIN", "MODERATOR", "SUPPORT"][role]);
console.log('Owner role:', ['ADMIN', 'MODERATOR', 'SUPPORT'][role]);
} catch (error) {
console.log("Admin token minting error:", error.message);
console.log('Admin token minting error:', error.message);
}
// Создаем тестовый токен модератора
const moderatorAddress = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"; // Тестовый адрес модератора
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]);
console.log('Moderator role:', ['ADMIN', 'MODERATOR', 'SUPPORT'][role]);
} catch (error) {
console.log("Moderator token minting error:", error.message);
console.log('Moderator token minting error:', error.message);
}
// Проверяем все токены
console.log("\nChecking all tokens:");
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]}`);
console.log(`Token ${i}: Owner ${owner}, Role: ${['ADMIN', 'MODERATOR', 'SUPPORT'][role]}`);
} catch (error) {
// Пропускаем несуществующие токены
if (!error.message.includes("nonexistent token")) {
if (!error.message.includes('nonexistent token')) {
console.log(`Token ${i} error:`, error.message);
}
}

View File

@@ -1,72 +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 ownerRole = await accessToken.checkRole(owner);
console.log("Owner role:", ["ADMIN", "MODERATOR", "SUPPORT"][ownerRole]);
} catch (error) {
console.log("Owner role check error:", error.message);
}
// Создадим токен модератора для тестового адреса
const moderatorAddress = "0xF45aa4917b3775bA37f48Aeb3dc1a943561e9e0B";
try {
const tx = await accessToken.mintAccessToken(moderatorAddress, 1); // 1 = MODERATOR
await tx.wait();
console.log(`Moderator token minted for ${moderatorAddress}`);
// Проверим роль модератора
const modRole = await accessToken.checkRole(moderatorAddress);
console.log("Moderator role:", ["ADMIN", "MODERATOR", "SUPPORT"][modRole]);
} catch (error) {
console.log("Moderator token minting error:", error.message);
}
// Получим все активные токены (с ограничением по блокам)
const currentBlock = await hre.ethers.provider.getBlockNumber();
const fromBlock = currentBlock - 1000; // Последние 1000 блоков
const filter = accessToken.filters.Transfer(null, null, null);
const events = await accessToken.queryFilter(filter, fromBlock);
console.log("\nActive tokens (last 1000 blocks):");
for (let event of events) {
if (event.args.from === "0x0000000000000000000000000000000000000000") {
console.log(`Token ID: ${event.args.tokenId}, Owner: ${event.args.to}`);
try {
const role = await accessToken.checkRole(event.args.to);
console.log(`Role: ${["ADMIN", "MODERATOR", "SUPPORT"][role]}`);
} catch (error) {
console.log("Role check error:", error.message);
}
}
}
// Альтернативный способ - проверить конкретный токен
console.log("\nChecking specific tokens:");
for (let i = 1; i <= 2; i++) {
try {
const owner = await accessToken.ownerOf(i);
const role = await accessToken.checkRole(owner);
console.log(`Token ${i}: Owner ${owner}, Role: ${["ADMIN", "MODERATOR", "SUPPORT"][role]}`);
} catch (error) {
console.log(`Token ${i} not found or error:`, error.message);
}
}
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

View File

@@ -1,9 +1,9 @@
const hre = require("hardhat");
const hre = require('hardhat');
async function main() {
const accessToken = await hre.ethers.getContractAt(
"AccessToken",
"0xF352c498cF0857F472dC473E4Dd39551E79B1063"
'AccessToken',
'0xF352c498cF0857F472dC473E4Dd39551E79B1063'
);
// Отзываем все токены от 1 до 3

View File

@@ -6,7 +6,7 @@ require('dotenv').config();
// Подключение к БД
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
});
async function runMigrations() {
@@ -24,12 +24,13 @@ async function runMigrations() {
// Получаем список уже примененных миграций
const { rows } = await pool.query('SELECT name FROM migrations');
const appliedMigrations = rows.map(row => row.name);
const appliedMigrations = rows.map((row) => row.name);
// Получаем список файлов миграций
const migrationsDir = path.join(__dirname, '../migrations');
const migrationFiles = fs.readdirSync(migrationsDir)
.filter(file => file.endsWith('.sql'))
const migrationFiles = fs
.readdirSync(migrationsDir)
.filter((file) => file.endsWith('.sql'))
.sort(); // Сортируем файлы по имени
// Применяем миграции, которые еще не были применены
@@ -45,10 +46,7 @@ async function runMigrations() {
await pool.query(sql);
// Записываем информацию о примененной миграции
await pool.query(
'INSERT INTO migrations (name) VALUES ($1)',
[file]
);
await pool.query('INSERT INTO migrations (name) VALUES ($1)', [file]);
console.log(`Миграция ${file} успешно применена`);
} else {

View File

@@ -0,0 +1,53 @@
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);
});

View File

@@ -3,7 +3,7 @@ const express = require('express');
const cors = require('cors');
const { SiweMessage, generateNonce } = require('siwe');
const { ethers } = require('ethers');
const TelegramBotService = require('./services/telegramBot');
// const TelegramBotService = require('./services/telegramBot');
const EmailBotService = require('./services/emailBot');
const { initializeVectorStore } = require('./services/vectorStore');
const session = require('express-session');
@@ -12,24 +12,27 @@ const usersRouter = require('./routes/users');
const { router: authRouter } = require('./routes/auth');
const contractsRouter = require('./routes/contracts');
const accessRouter = require('./routes/access');
const chatRouter = require('./routes/chat');
const path = require('path');
const axios = require('axios');
const { ChatOllama } = require('@langchain/ollama');
const { getVectorStore } = require('./services/vectorStore');
const debugRouter = require('./routes/debug');
// const debugRoutes = require('./routes/debug');
const identitiesRouter = require('./routes/identities');
const kanbanRouter = require('./routes/kanban');
const { pool } = require('./db');
const fs = require('fs');
const pgSession = require('connect-pg-simple')(session);
const sessionStore = new pgSession({
pool: pool,
tableName: 'session',
createTableIfMissing: true
createTableIfMissing: true,
});
const helmet = require('helmet');
const csrf = require('csurf');
// const csrf = require('csurf');
// const cookieParser = require('cookie-parser');
const messagesRouter = require('./routes/messages');
// Импорт сервисов
const { initTelegramBot } = require('./services/telegram-service');
const PORT = process.env.PORT || 8000;
@@ -50,24 +53,21 @@ const provider = new ethers.JsonRpcProvider(process.env.ETHEREUM_NETWORK_URL);
console.log('Provider URL:', process.env.ETHEREUM_NETWORK_URL);
console.log('Contract address:', process.env.CONTRACT_ADDRESS);
const contract = new ethers.Contract(
process.env.CONTRACT_ADDRESS,
contractABI,
provider
);
const contract = new ethers.Contract(process.env.CONTRACT_ADDRESS, contractABI, provider);
// Проверяем, что библиотека ethers.js правильно импортирована
console.log('Ethers.js version:', ethers.version);
// Порядок middleware важен!
// 1. CORS должен быть первым
app.use(cors({
origin: ['http://127.0.0.1:5173', 'http://localhost:5173'],
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', 'X-Requested-With'],
exposedHeaders: ['Set-Cookie']
}));
allowedHeaders: ['Content-Type', 'Authorization', 'X-Auth-Nonce'],
})
);
// Добавьте после настройки CORS
app.use(helmet());
@@ -77,18 +77,23 @@ app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 3. Затем сессии
app.use(session({
app.use(
session({
secret: process.env.SESSION_SECRET || 'your-secret-key',
resave: true,
saveUninitialized: true,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: false,
sameSite: 'lax',
maxAge: 24 * 60 * 60 * 1000
secure: process.env.NODE_ENV === 'production', // В разработке можно установить false
sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax',
maxAge: 24 * 60 * 60 * 1000, // 1 день
},
store: sessionStore
}));
store: new pgSession({
pool: pool,
tableName: 'session',
}),
})
);
// Добавьте после настройки сессий
app.use((req, res, next) => {
@@ -164,60 +169,51 @@ app.use((req, res, next) => {
// Добавляем middleware для отладки сессий
app.use((req, res, next) => {
// console.log('Session debug:', {
// url: req.url,
// method: req.method,
// sessionID: req.sessionID,
// cookies: req.headers.cookie,
// session: req.session ? {
// isAuthenticated: req.session.isAuthenticated,
// authenticated: req.session.authenticated,
// address: req.session.address,
// isAdmin: req.session.isAdmin,
// nonce: req.session.nonce ? '[REDACTED]' : undefined,
// pendingAddress: req.session.pendingAddress
// } : null
// });
console.log('Сессия:', req.session);
console.log('Куки:', req.headers.cookie);
next();
});
// Настройка CSRF-защиты
const csrfProtection = csrf({
cookie: {
key: '_csrf',
path: '/',
httpOnly: true,
secure: process.env.NODE_ENV === 'production', // true в production, false в development
sameSite: process.env.NODE_ENV === 'production' ? 'strict' : 'lax'
}
});
// Добавьте cookie-парсер перед CSRF-защитой
// app.use(cookieParser());
// Затем настройте CSRF-защиту
// const csrfProtection = csrf({
// cookie: {
// key: '_csrf',
// path: '/',
// httpOnly: true,
// secure: process.env.NODE_ENV === 'production',
// sameSite: 'lax'
// }
// });
// Добавьте маршрут для получения CSRF-токена
// app.get('/api/csrf-token', csrfProtection, (req, res) => {
// res.json({ csrfToken: req.csrfToken() });
// });
// Применяем CSRF-защиту только к определенным маршрутам
app.use('/api/protected', csrfProtection);
app.use('/api/admin', csrfProtection);
app.use('/api/kanban', csrfProtection);
// Маршрут для получения CSRF-токена
app.get('/api/csrf-token', csrfProtection, (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// app.use('/api/protected', csrfProtection);
// app.use('/api/admin', csrfProtection);
// app.use('/api/kanban', csrfProtection);
// Обработчик ошибок CSRF
app.use((err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN') {
console.error('CSRF error:', {
url: req.url,
method: req.method,
headers: req.headers,
body: req.body
});
return res.status(403).json({
error: 'CSRF token validation failed',
message: 'Your session may have expired. Please refresh the page and try again.'
});
}
next(err);
});
// app.use((err, req, res, next) => {
// if (err.code === 'EBADCSRFTOKEN') {
// console.error('CSRF error:', {
// url: req.url,
// method: req.method,
// headers: req.headers,
// body: req.body
// });
// return res.status(403).json({
// error: 'CSRF token validation failed',
// message: 'Your session may have expired. Please refresh the page and try again.'
// });
// }
// next(err);
// });
async function initServices() {
try {
@@ -244,10 +240,10 @@ app.use('/api/users', usersRouter);
app.use('/api/auth', authRouter);
app.use('/api/contracts', contractsRouter);
app.use('/api/access', accessRouter);
app.use('/api/chat', chatRouter);
app.use('/api/debug', debugRouter);
// app.use('/api/chat', chatRouter);
// app.use('/api/debug', debugRoutes);
app.use('/api/identities', identitiesRouter);
app.use('/api/kanban', kanbanRouter);
app.use('/api/messages', messagesRouter);
// Добавьте простой эндпоинт для проверки состояния сервера
app.get('/api/health', (req, res) => {
@@ -259,18 +255,18 @@ app.post('/api/verify', async (req, res) => {
try {
// Перенаправляем запрос на /api/auth/verify
const { message, signature } = req.body;
console.log("Перенаправление запроса на /api/auth/verify:", { message, signature });
console.log('Перенаправление запроса на /api/auth/verify:', { message, signature });
// Проверяем наличие необходимых данных
if (!message || !message.address || !signature) {
return res.status(400).json({
success: false,
error: 'Отсутствуют необходимые данные для верификации'
error: 'Отсутствуют необходимые данные для верификации',
});
}
const address = message.address.toLowerCase();
console.log("Адрес из сообщения:", address);
console.log('Адрес из сообщения:', address);
// Проверяем, является ли пользователь администратором
const isAdmin = true; // Для примера всегда true
@@ -311,8 +307,8 @@ app.post('/api/verify', async (req, res) => {
isAuthenticated: req.session.isAuthenticated,
authenticated: req.session.authenticated,
address: req.session.address,
isAdmin: req.session.isAdmin
}
isAdmin: req.session.isAdmin,
},
});
res.cookie('authToken', 'true', {
@@ -320,20 +316,20 @@ app.post('/api/verify', async (req, res) => {
httpOnly: false,
secure: false,
sameSite: 'lax',
path: '/'
path: '/',
});
res.json({
success: true,
address: address,
isAdmin: isAdmin
isAdmin: isAdmin,
});
});
} catch (error) {
console.error("Ошибка верификации:", error);
console.error('Ошибка верификации:', error);
res.status(500).json({
success: false,
error: error.message || 'Внутренняя ошибка сервера'
error: error.message || 'Внутренняя ошибка сервера',
});
}
});
@@ -345,7 +341,7 @@ app.get('/api/session', (req, res) => {
sessionID: req.sessionID,
isAuthenticated: req.session?.isAuthenticated,
authenticated: req.session?.authenticated,
address: req.session?.address
address: req.session?.address,
});
if (req.session && (req.session.isAuthenticated || req.session.authenticated)) {
@@ -353,14 +349,14 @@ app.get('/api/session', (req, res) => {
isAuthenticated: true,
authenticated: true,
address: req.session.address,
isAdmin: req.session.isAdmin
isAdmin: req.session.isAdmin,
});
} else {
res.json({
isAuthenticated: false,
authenticated: false,
address: null,
isAdmin: false
isAdmin: false,
});
}
});
@@ -384,8 +380,8 @@ app.get('/api/protected', (req, res) => {
message: 'This is a protected API endpoint',
user: {
address: req.session.address,
isAdmin: req.session.isAdmin
}
isAdmin: req.session.isAdmin,
},
});
});
@@ -394,75 +390,31 @@ app.get('/api/admin', (req, res) => {
message: 'This is an admin API endpoint',
user: {
address: req.session.address,
isAdmin: req.session.isAdmin
}
isAdmin: req.session.isAdmin,
},
});
});
// Добавьте обработчик ошибок
app.use((err, req, res, next) => {
console.error('Глобальный обработчик ошибок:', err);
// Обработка ошибок CSRF
if (err.code === 'EBADCSRFTOKEN') {
return res.status(403).json({
error: 'Недействительный CSRF-токен',
message: 'Возможно, ваша сессия истекла. Пожалуйста, обновите страницу и попробуйте снова.'
});
console.error('Глобальная ошибка:', err.stack);
if (!res.headersSent) {
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
}
// Обработка ошибок валидации
if (err.name === 'ValidationError') {
return res.status(400).json({
error: 'Ошибка валидации',
details: err.details || err.message
});
}
// Обработка ошибок базы данных
if (err.code === '23505') { // Postgres unique violation
return res.status(409).json({
error: 'Конфликт данных',
message: 'Запись с такими данными уже существует.'
});
}
// Общая обработка ошибок
res.status(err.status || 500).json({
error: 'Внутренняя ошибка сервера',
message: process.env.NODE_ENV === 'production' ? 'Что-то пошло не так' : err.message
});
});
// Перед запуском сервера
console.log('Перед запуском сервера на порту:', PORT);
// Запуск сервера и инициализация сервисов
const server = app.listen(PORT, '0.0.0.0', async () => {
let server;
checkDatabaseStructure().then(() => {
// Запускаем сервер
server = app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
console.log('Server address:', server.address());
// Инициализируем сервисы без блокировки запуска сервера
initServices().catch(err => {
console.error('Ошибка при инициализации сервисов:', err);
});
// Проверяем доступность Ollama в фоновом режиме
try {
const { checkOllamaAvailability } = require('./services/ollama');
checkOllamaAvailability().catch(err => {
console.error('Ошибка при проверке Ollama:', err);
});
} catch (error) {
console.error('Ошибка при импорте модуля Ollama:', error);
}
}).on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`Port ${PORT} is already in use. Please try another port.`);
process.exit(1);
} else {
console.error('Server error:', err);
}
});
// Добавляем graceful shutdown
@@ -515,15 +467,16 @@ async function checkOllamaServer() {
}
// Настройка периодической очистки устаревших сессий
const pgSessionCleanup = setInterval(function() {
const pgSessionCleanup = setInterval(function () {
console.log('Cleaning up expired sessions...');
pool.query('DELETE FROM session WHERE expire < NOW()')
.then(result => {
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));
.catch((err) => console.error('Error cleaning up sessions:', err));
}, 3600000); // Очистка каждый час
// Очистка интервала при завершении работы
@@ -674,3 +627,110 @@ app.use('/api/admin', (req, res, next) => {
next();
});
// Проверка структуры базы данных
async function checkDatabaseStructure() {
try {
const db = require('./db');
// Проверяем наличие таблицы roles
const rolesTable = await db.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = 'roles'
);
`);
if (!rolesTable.rows[0].exists) {
console.error('Таблица roles не существует. Выполните миграцию.');
process.exit(1);
}
// Проверяем наличие колонки role_id в таблице users
const roleIdColumn = await db.query(`
SELECT EXISTS (
SELECT FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'role_id'
);
`);
if (!roleIdColumn.rows[0].exists) {
console.error('Колонка role_id не существует в таблице users. Выполните миграцию.');
process.exit(1);
}
console.log('Структура базы данных проверена успешно.');
} catch (error) {
console.error('Ошибка при проверке структуры базы данных:', error);
process.exit(1);
}
}
// Обработка сигналов завершения
process.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('uncaughtException', (error) => {
console.error('Необработанное исключение:', error);
// Не завершаем процесс, чтобы nodemon мог перезапустить сервер
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Необработанное отклонение промиса:', reason);
// Не завершаем процесс, чтобы nodemon мог перезапустить сервер
});
// Инициализация Telegram бота
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 sessions 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 sessions WHERE expire < NOW()');
console.log(`Первоначальная очистка: удалено ${result.rowCount} устаревших сессий`);
} catch (err) {
console.error('Ошибка при первоначальной очистке сессий:', err);
}
}, 5 * 60 * 1000);

View File

@@ -0,0 +1,158 @@
const { ChatOllama } = require('@langchain/ollama');
const { pool } = require('../db');
// Инициализация модели 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 'Извините, произошла ошибка при обработке вашего сообщения. Пожалуйста, попробуйте еще раз позже.';
}
}
/**
* Получение информации о пользователе
* @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]
);
if (userResult.rows.length === 0) {
return { id: userId };
}
// Получение идентификаторов пользователя
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]
);
if (conversationResult.rows.length === 0) {
return '';
}
const conversationId = conversationResult.rows[0].id;
// Получение последних сообщений из диалога
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]
);
// Формирование истории в текстовом формате
const history = messagesResult.rows
.reverse()
.map((msg) => {
const sender = msg.sender_type === 'user' ? 'Пользователь' : 'ИИ';
return `${sender}: ${msg.content}`;
})
.join('\n\n');
return history;
} catch (error) {
console.error('Error getting conversation history:', error);
return '';
}
}
module.exports = {
processMessage,
getUserInfo,
getConversationHistory,
};

View File

@@ -1,47 +0,0 @@
const fs = require('fs');
const path = require('path');
const { Document } = require('langchain/document');
const { RecursiveCharacterTextSplitter } = require('langchain/text_splitter');
// Функция для загрузки документов из файлов
async function loadDocumentsFromFiles(directory) {
const documents = [];
try {
const files = fs.readdirSync(directory);
for (const file of files) {
const filePath = path.join(directory, file);
const stat = fs.statSync(filePath);
if (stat.isFile() && (file.endsWith('.txt') || file.endsWith('.md'))) {
const content = fs.readFileSync(filePath, 'utf-8');
documents.push(
new Document({
pageContent: content,
metadata: {
source: filePath,
filename: file,
},
})
);
}
}
// Разделяем документы на чанки
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000,
chunkOverlap: 200,
});
const splitDocs = await textSplitter.splitDocuments(documents);
return splitDocs;
} catch (error) {
console.error('Error loading documents:', error);
throw error;
}
}
module.exports = { loadDocumentsFromFiles };

View File

@@ -1,68 +1,246 @@
const { pool } = require('../db');
const nodemailer = require('nodemailer');
const { ChatOllama } = require('@langchain/ollama');
const { PGVectorStore } = require('@langchain/community/vectorstores/pgvector');
const { Pool } = require('pg');
const Imap = require('imap');
const { simpleParser } = require('mailparser');
const { checkMailServer } = require('../utils/checkMail');
const { sleep, isValidEmail } = require('../utils/helpers');
const { linkIdentity, getUserIdByIdentity } = require('../utils/identity-linker');
require('dotenv').config();
const simpleParser = require('mailparser').simpleParser;
const { processMessage } = require('./ai-assistant');
class EmailBotService {
constructor() {
this.enabled = false;
console.log('EmailBotService: Сервис отключен (заглушка)');
// Конфигурация для отправки писем
const transporter = nodemailer.createTransport({
host: process.env.EMAIL_SMTP_HOST,
port: process.env.EMAIL_SMTP_PORT,
secure: process.env.EMAIL_SMTP_PORT === '465', // true для 465, false для других портов
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASSWORD,
},
});
// Конфигурация для получения писем
const imapConfig = {
user: process.env.EMAIL_USER,
password: process.env.EMAIL_PASSWORD,
host: process.env.EMAIL_IMAP_HOST,
port: process.env.EMAIL_IMAP_PORT,
tls: true,
tlsOptions: { rejectUnauthorized: false },
};
/**
* Инициализация сервиса электронной почты
*/
function initEmailBot() {
if (!process.env.EMAIL_USER || !process.env.EMAIL_PASSWORD) {
console.warn('EMAIL_USER or EMAIL_PASSWORD not set, Email integration disabled');
return null;
}
async start() {
console.log('EmailBotService: Запуск сервиса отключен (заглушка)');
return false;
}
console.log('Email bot initialized');
async stop() {
console.log('EmailBotService: Остановка сервиса отключена (заглушка)');
return true;
}
// Запуск проверки почты каждые 5 минут
const checkInterval = 5 * 60 * 1000; // 5 минут
setInterval(checkEmails, checkInterval);
isEnabled() {
return this.enabled;
}
// Первая проверка при запуске
checkEmails();
return {
sendEmail,
checkEmails,
};
}
// В обработчике команд добавьте код для связывания аккаунтов
async function processCommand(email, command, args) {
if (command === 'link' && args.length > 0) {
const ethAddress = args[0];
/**
* Проверка новых писем
*/
function checkEmails() {
const imap = new Imap(imapConfig);
// Проверяем формат Ethereum-адреса
if (!/^0x[a-fA-F0-9]{40}$/.test(ethAddress)) {
return 'Неверный формат Ethereum-адреса. Используйте формат 0x...';
imap.once('ready', () => {
imap.openBox('INBOX', false, (err, box) => {
if (err) {
console.error('Error opening inbox:', err);
return;
}
// Поиск непрочитанных писем
imap.search(['UNSEEN'], (err, results) => {
if (err) {
console.error('Error searching emails:', err);
return;
}
if (results.length === 0) {
console.log('No new emails');
imap.end();
return;
}
console.log(`Found ${results.length} new emails`);
const f = imap.fetch(results, { bodies: '' });
f.on('message', (msg, seqno) => {
msg.on('body', (stream, info) => {
simpleParser(stream, async (err, parsed) => {
if (err) {
console.error('Error parsing email:', err);
return;
}
try {
// Получаем ID пользователя по Ethereum-адресу
const userId = await getUserIdByIdentity('ethereum', ethAddress);
// Обработка письма
await processEmail(parsed);
if (!userId) {
return 'Пользователь с таким Ethereum-адресом не найден. Сначала войдите через веб-интерфейс.';
}
// Связываем Email-аккаунт с пользователем
const success = await linkIdentity(userId, 'email', email);
if (success) {
return `Ваш Email-аккаунт успешно связан с Ethereum-адресом ${ethAddress}`;
} else {
return 'Не удалось связать аккаунты. Возможно, этот Email-аккаунт уже связан с другим пользователем.';
// Пометить как прочитанное
imap.setFlags(results, ['\\Seen'], (err) => {
if (err) {
console.error('Error marking email as read:', err);
}
});
} catch (error) {
console.error('Ошибка при связывании аккаунтов:', error);
return 'Произошла ошибка при связывании аккаунтов. Попробуйте позже.';
}
console.error('Error processing email:', error);
}
});
});
});
// Обработка других команд...
f.once('error', (err) => {
console.error('Fetch error:', err);
});
f.once('end', () => {
imap.end();
});
});
});
});
imap.once('error', (err) => {
console.error('IMAP error:', err);
});
imap.connect();
}
module.exports = EmailBotService;
/**
* Обработка полученного письма
* @param {Object} email - Распарсенное письмо
*/
async function processEmail(email) {
try {
const from = email.from.value[0].address;
const subject = email.subject;
const text = email.text || '';
console.log(`Processing email from ${from}, subject: ${subject}`);
// Поиск пользователя по email
const userResult = await pool.query(
`SELECT u.* FROM users u
JOIN user_identities ui ON u.id = ui.user_id
WHERE ui.identity_type = 'email' AND ui.identity_value = $1 AND ui.verified = TRUE`,
[from]
);
if (userResult.rows.length === 0) {
console.log(`No verified user found for email ${from}`);
// Отправка ответа о необходимости регистрации
await sendEmail(
from,
'Регистрация в системе',
'Для использования ИИ-ассистента через email, пожалуйста, зарегистрируйтесь на нашем сайте и подтвердите свой email.'
);
return;
}
const user = userResult.rows[0];
// Получение или создание диалога
const conversationResult = await pool.query(
`SELECT * FROM conversations
WHERE user_id = $1
ORDER BY updated_at DESC
LIMIT 1`,
[user.id]
);
let conversationId;
if (conversationResult.rows.length === 0) {
// Создание нового диалога
const newConversationResult = await pool.query(
`INSERT INTO conversations (user_id, title)
VALUES ($1, $2)
RETURNING id`,
[user.id, subject || 'Email диалог']
);
conversationId = newConversationResult.rows[0].id;
} else {
conversationId = conversationResult.rows[0].id;
}
// Сохранение сообщения пользователя
await pool.query(
`INSERT INTO messages (conversation_id, sender_type, sender_id, content, channel)
VALUES ($1, $2, $3, $4, $5)`,
[conversationId, 'user', user.id, text, 'email']
);
// Обработка сообщения ИИ-ассистентом
const aiResponse = await processMessage(user.id, text, user.language || 'ru');
// Сохранение ответа ИИ
await pool.query(
`INSERT INTO messages (conversation_id, sender_type, sender_id, content, channel)
VALUES ($1, $2, $3, $4, $5)`,
[conversationId, 'ai', null, aiResponse, 'email']
);
// Обновление времени последнего обновления диалога
await pool.query(
`UPDATE conversations
SET updated_at = NOW()
WHERE id = $1`,
[conversationId]
);
// Отправка ответа пользователю
await sendEmail(from, `Re: ${subject}`, aiResponse);
console.log(`Sent response to ${from}`);
} catch (error) {
console.error('Error processing email:', error);
throw error;
}
}
/**
* Отправка email
* @param {string} to - Адрес получателя
* @param {string} subject - Тема письма
* @param {string} text - Текст письма
* @returns {Promise<Object>} - Результат отправки
*/
async function sendEmail(to, subject, text) {
try {
const info = await transporter.sendMail({
from: process.env.EMAIL_USER,
to,
subject,
text,
});
console.log('Email sent:', info.messageId);
return info;
} catch (error) {
console.error('Error sending email:', error);
throw error;
}
}
module.exports = {
initEmailBot,
sendEmail,
checkEmails,
};

32
backend/services/index.js Normal file
View File

@@ -0,0 +1,32 @@
const { initTelegramBot } = require('./telegram-service');
const { initEmailBot, sendEmail, checkEmails } = require('./emailBot');
const {
initializeVectorStore,
getVectorStore,
similaritySearch,
addDocument,
} = require('./vectorStore');
const { processMessage, getUserInfo, getConversationHistory } = require('./ai-assistant');
// ... другие импорты
module.exports = {
// Telegram
initTelegramBot,
// Email
initEmailBot,
sendEmail,
checkEmails,
// Vector Store
initializeVectorStore,
getVectorStore,
similaritySearch,
addDocument,
// AI Assistant
processMessage,
getUserInfo,
getConversationHistory,
// ... другие экспорты
};

View File

@@ -1,7 +1,9 @@
const { ChatOllama } = require('@langchain/ollama');
const { RetrievalQAChain } = require("langchain/chains");
const { PromptTemplate } = require("@langchain/core/prompts");
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 = `
@@ -23,13 +25,13 @@ async function checkOllamaAvailability() {
try {
// Добавляем таймаут для запроса
const response = await axios.get('http://localhost:11434/api/tags', {
timeout: 5000 // 5 секунд таймаут
timeout: 5000, // 5 секунд таймаут
});
if (response.status === 200) {
console.log('Ollama доступен. Доступные модели:');
if (response.data && response.data.models) {
response.data.models.forEach(model => {
response.data.models.forEach((model) => {
console.log(`- ${model.name}`);
});
}
@@ -42,32 +44,45 @@ async function checkOllamaAvailability() {
}
}
// Функция для прямого запроса к Ollama API
async function directOllamaQuery(message, model = 'mistral') {
// Функция для прямого запроса к Ollama
async function directOllamaQuery(message, language = 'en') {
try {
console.log(`Отправка запроса к Ollama (модель: ${model}):`, message);
// Всегда используем модель mistral, независимо от языка
const modelName = 'mistral';
// Проверяем доступность Ollama перед отправкой запроса
const isAvailable = await checkOllamaAvailability();
if (!isAvailable) {
throw new Error('Сервер Ollama недоступен');
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 недоступен');
}
// Создаем экземпляр ChatOllama
const ollama = new ChatOllama({
console.log('Отправка запроса к Ollama...');
const chatModel = new ChatOllama({
baseUrl: 'http://localhost:11434',
model: model,
model: modelName,
temperature: 0.7,
});
console.log('Отправка запроса к Ollama...');
const result = await ollama.invoke(message);
console.log('Получен ответ от Ollama');
const response = await chatModel.invoke([new HumanMessage(message)]);
return result.content;
return response.content;
} catch (error) {
console.error('Ошибка при запросе к Ollama:', error);
throw error;
// Возвращаем сообщение об ошибке
return 'Извините, произошла ошибка при обработке вашего запроса. Пожалуйста, попробуйте позже.';
}
}
@@ -98,7 +113,7 @@ async function createOllamaChain(vectorStore) {
// Создаем шаблон запроса
const prompt = new PromptTemplate({
template: PROMPT_TEMPLATE,
inputVariables: ["context", "query"],
inputVariables: ['context', 'query'],
});
console.log('Шаблон запроса создан');
@@ -108,17 +123,13 @@ async function createOllamaChain(vectorStore) {
console.log('Создаем цепочку для поиска и ответа...');
// Создаем цепочку для поиска и ответа
const chain = RetrievalQAChain.fromLLM(
model,
retriever,
{
const chain = RetrievalQAChain.fromLLM(model, retriever, {
returnSourceDocuments: true,
prompt: prompt,
inputKey: "query",
outputKey: "text",
verbose: true
}
);
inputKey: 'query',
outputKey: 'text',
verbose: true,
});
console.log('Цепочка для поиска и ответа создана');
return chain;

View File

@@ -0,0 +1,262 @@
const TelegramBot = require('node-telegram-bot-api');
const { pool } = require('../db');
const { processMessage } = require('./ai-assistant');
// Инициализация бота
const token = process.env.TELEGRAM_BOT_TOKEN;
let bot = null;
if (token) {
bot = new TelegramBot(token, { polling: true });
console.log('Telegram bot initialized');
} else {
console.warn('TELEGRAM_BOT_TOKEN not set, Telegram integration disabled');
}
/**
* Инициализация Telegram бота
*/
function initTelegramBot() {
if (!bot) return;
// Обработка команды /start
bot.onText(/\/start/, async (msg) => {
const chatId = msg.chat.id;
const userId = msg.from.id;
const username =
msg.from.username || `${msg.from.first_name} ${msg.from.last_name || ''}`.trim();
try {
// Проверка существования пользователя
const user = await findOrCreateUser(userId, username, chatId);
// Приветственное сообщение
bot.sendMessage(chatId, `Привет, ${username}! Я ИИ-ассистент. Чем могу помочь?`);
} catch (error) {
console.error('Error handling /start command:', error);
bot.sendMessage(
chatId,
'Произошла ошибка при обработке команды. Пожалуйста, попробуйте позже.'
);
}
});
// Обработка текстовых сообщений
bot.on('message', async (msg) => {
if (!msg.text || msg.text.startsWith('/')) return;
const chatId = msg.chat.id;
const userId = msg.from.id;
const username =
msg.from.username || `${msg.from.first_name} ${msg.from.last_name || ''}`.trim();
try {
// Проверка существования пользователя
const user = await findOrCreateUser(userId, username, chatId);
// Получение или создание диалога
const conversation = await getOrCreateConversation(user.id);
// Сохранение сообщения пользователя
await saveMessage(conversation.id, 'user', user.id, msg.text, 'telegram');
// Обработка сообщения ИИ-ассистентом
const aiResponse = await processMessage(user.id, msg.text, user.language || 'ru');
// Сохранение ответа ИИ
await saveMessage(conversation.id, 'ai', null, aiResponse, 'telegram');
// Отправка ответа
bot.sendMessage(chatId, aiResponse);
} catch (error) {
console.error('Error processing message:', error);
bot.sendMessage(
chatId,
'Произошла ошибка при обработке сообщения. Пожалуйста, попробуйте позже.'
);
}
});
console.log('Telegram bot handlers registered');
}
/**
* Поиск или создание пользователя по Telegram ID
* @param {number} telegramId - Telegram ID пользователя
* @param {string} username - Имя пользователя
* @param {number} chatId - ID чата
* @returns {Promise<Object>} - Информация о пользователе
*/
async function findOrCreateUser(telegramId, username, chatId) {
try {
// Поиск пользователя по Telegram ID
const userIdResult = await pool.query(
`SELECT user_id FROM user_identities
WHERE identity_type = 'telegram' AND identity_value = $1`,
[telegramId.toString()]
);
if (userIdResult.rows.length > 0) {
// Пользователь найден
const userId = userIdResult.rows[0].user_id;
// Получение информации о пользователе
const userResult = await pool.query('SELECT * FROM users WHERE id = $1', [userId]);
return userResult.rows[0];
} else {
// Создание нового пользователя
const userResult = await pool.query(
`INSERT INTO users (
username,
role_id,
is_admin,
language,
address
) VALUES (
$1,
(SELECT id FROM roles WHERE name = 'user'),
FALSE,
'ru',
'0x' || encode(gen_random_bytes(20), 'hex')
) RETURNING *`,
[username]
);
const newUser = userResult.rows[0];
// Добавление идентификатора Telegram
await pool.query(
`INSERT INTO user_identities (
user_id,
identity_type,
identity_value,
verified
) VALUES ($1, 'telegram', $2, TRUE)`,
[newUser.id, telegramId.toString()]
);
// Сохранение метаданных Telegram
await pool.query(
`INSERT INTO user_preferences (
user_id,
preference_key,
preference_value
) VALUES ($1, 'telegram_chat_id', $2)`,
[newUser.id, chatId.toString()]
);
return newUser;
}
} catch (error) {
console.error('Error finding or creating user:', error);
throw error;
}
}
/**
* Получение или создание диалога для пользователя
* @param {number} userId - ID пользователя
* @returns {Promise<Object>} - Информация о диалоге
*/
async function getOrCreateConversation(userId) {
try {
// Поиск активного диалога
const conversationResult = await pool.query(
`SELECT * FROM conversations
WHERE user_id = $1
ORDER BY updated_at DESC
LIMIT 1`,
[userId]
);
if (conversationResult.rows.length > 0) {
// Обновление времени последней активности
await pool.query('UPDATE conversations SET updated_at = NOW() WHERE id = $1', [
conversationResult.rows[0].id,
]);
return conversationResult.rows[0];
} else {
// Создание нового диалога
const newConversationResult = await pool.query(
`INSERT INTO conversations (user_id, title)
VALUES ($1, $2)
RETURNING *`,
[userId, 'Диалог в Telegram']
);
return newConversationResult.rows[0];
}
} catch (error) {
console.error('Error getting or creating conversation:', error);
throw error;
}
}
/**
* Сохранение сообщения
* @param {number} conversationId - ID диалога
* @param {string} senderType - Тип отправителя ('user', 'ai')
* @param {number|null} senderId - ID отправителя
* @param {string} content - Текст сообщения
* @param {string} channel - Канал ('telegram')
* @returns {Promise<Object>} - Информация о сообщении
*/
async function saveMessage(conversationId, senderType, senderId, content, channel) {
try {
const messageResult = await pool.query(
`INSERT INTO messages (
conversation_id,
sender_type,
sender_id,
content,
channel
) VALUES ($1, $2, $3, $4, $5)
RETURNING *`,
[conversationId, senderType, senderId, content, channel]
);
return messageResult.rows[0];
} catch (error) {
console.error('Error saving message:', error);
throw error;
}
}
/**
* Отправка сообщения пользователю через Telegram
* @param {number} userId - ID пользователя
* @param {string} message - Текст сообщения
* @returns {Promise<boolean>} - Успешность отправки
*/
async function sendMessageToUser(userId, message) {
if (!bot) return false;
try {
// Получение Telegram chat ID пользователя
const chatIdResult = await pool.query(
`SELECT preference_value FROM user_preferences
WHERE user_id = $1 AND preference_key = 'telegram_chat_id'`,
[userId]
);
if (chatIdResult.rows.length === 0) {
return false;
}
const chatId = chatIdResult.rows[0].preference_value;
// Отправка сообщения
await bot.sendMessage(chatId, message);
return true;
} catch (error) {
console.error('Error sending message to user:', error);
return false;
}
}
module.exports = {
initTelegramBot,
sendMessageToUser,
};

View File

@@ -1,402 +0,0 @@
const TelegramBot = require('node-telegram-bot-api');
const { ChatOllama } = require('@langchain/ollama');
const axios = require('axios');
const dns = require('dns').promises;
require('dotenv').config();
const { sleep } = require('../utils/helpers');
const util = require('util');
const exec = util.promisify(require('child_process').exec);
const { linkIdentity, getUserIdByIdentity } = require('../utils/identity-linker');
class TelegramBotService {
constructor() {
// Проверяем наличие токена
if (!process.env.TELEGRAM_BOT_TOKEN) {
throw new Error('Token is required');
}
this.isRunning = false;
this.maxRetries = 3;
this.retryDelay = 5000; // 5 секунд между попытками
// Создаем бота без polling
this.bot = new TelegramBot(process.env.TELEGRAM_BOT_TOKEN, {
polling: false,
request: {
proxy: null,
agentOptions: {
rejectUnauthorized: true,
minVersion: 'TLSv1.2'
},
timeout: 30000
}
});
this.token = process.env.TELEGRAM_BOT_TOKEN;
this.chat = new ChatOllama({
model: 'mistral',
baseUrl: 'http://localhost:11434'
});
// Добавляем настройки прокси для axios
this.axiosConfig = {
timeout: 5000,
proxy: false,
httpsAgent: new (require('https').Agent)({
rejectUnauthorized: true,
minVersion: 'TLSv1.2'
})
};
this.initialize();
}
setupHandlers() {
this.bot.onText(/.*/, async (msg) => {
try {
const chatId = msg.chat.id;
const userQuestion = msg.text;
// Пропускаем команды
if (userQuestion.startsWith('/')) {
return;
}
console.log('Получен вопрос:', userQuestion);
// Используем локальную модель
const result = await this.chat.invoke(userQuestion);
const assistantResponse = result.content;
await this.bot.sendMessage(chatId, assistantResponse);
} catch (error) {
console.error('Telegram bot error:', error);
await this.bot.sendMessage(msg.chat.id,
'Извините, произошла ошибка при обработке вашего запроса. ' +
'Попробуйте повторить позже или обратитесь к администратору.'
);
}
});
this.bot.onText(/\/link (.+)/, async (msg, match) => {
const chatId = msg.chat.id;
const ethAddress = match[1];
// Проверяем формат Ethereum-адреса
if (!/^0x[a-fA-F0-9]{40}$/.test(ethAddress)) {
this.bot.sendMessage(chatId, 'Неверный формат Ethereum-адреса. Используйте формат 0x...');
return;
}
try {
// Получаем ID пользователя по Ethereum-адресу
const userId = await getUserIdByIdentity('ethereum', ethAddress);
if (!userId) {
this.bot.sendMessage(chatId, 'Пользователь с таким Ethereum-адресом не найден. Сначала войдите через веб-интерфейс.');
return;
}
// Связываем Telegram-аккаунт с пользователем
const success = await linkIdentity(userId, 'telegram', chatId.toString());
if (success) {
this.bot.sendMessage(chatId, `Ваш Telegram-аккаунт успешно связан с Ethereum-адресом ${ethAddress}`);
} else {
this.bot.sendMessage(chatId, 'Не удалось связать аккаунты. Возможно, этот Telegram-аккаунт уже связан с другим пользователем.');
}
} catch (error) {
console.error('Ошибка при связывании аккаунтов:', error);
this.bot.sendMessage(chatId, 'Произошла ошибка при связывании аккаунтов. Попробуйте позже.');
}
});
}
setupCommands() {
this.bot.onText(/\/start/, async (msg) => {
const welcomeMessage = `
👋 Здравствуйте! Я - ассистент DApp for Business.
Я готов помочь вам с вопросами о:
• Разработке dApps
• Блокчейн-технологиях
• Web3 и криптовалютах
Просто задавайте вопросы, а если нужна помощь -
используйте команду /help
`;
await this.bot.sendMessage(msg.chat.id, welcomeMessage);
});
this.bot.onText(/\/help/, async (msg) => {
const helpMessage = `
🤖 Я - ассистент DApp for Business
Я могу помочь вам с:
• Разработкой децентрализованных приложений
• Интеграцией блокчейн-технологий в бизнес
• Консультациями по Web3 и криптовалютам
Команды:
/start - начать работу с ботом
/help - показать это сообщение
/status - проверить состояние бота
Просто задавайте вопросы на русском или английском языке!
`;
await this.bot.sendMessage(msg.chat.id, helpMessage);
});
this.bot.onText(/\/status/, async (msg) => {
try {
const status = {
isRunning: this.isRunning,
uptime: process.uptime(),
memoryUsage: process.memoryUsage(),
connections: {
telegram: Boolean(this.bot),
ollama: Boolean(this.chat)
}
};
const statusMessage = `
📊 Статус бота:
🟢 Статус: ${status.isRunning ? 'Работает' : 'Остановлен'}
⏱ Время работы: ${Math.floor(status.uptime / 60)} мин
🔗 Подключения:
• Telegram API: ${status.connections.telegram ? '✅' : '❌'}
• Ollama: ${status.connections.ollama ? '✅' : '❌'}
💾 Использование памяти:
• Heap: ${Math.round(status.memoryUsage.heapUsed / 1024 / 1024)}MB
• RSS: ${Math.round(status.memoryUsage.rss / 1024 / 1024)}MB
`;
await this.bot.sendMessage(msg.chat.id, statusMessage);
} catch (error) {
console.error('Ошибка при получении статуса:', error);
await this.bot.sendMessage(msg.chat.id, 'Ошибка при получении статуса бота');
}
});
}
async initialize() {
let retries = 0;
while (retries < this.maxRetries) {
try {
console.log(`Попытка инициализации Telegram бота (${retries + 1}/${this.maxRetries})...`);
// Сначала проверяем DNS и доступность
try {
console.log('Проверка DNS для api.telegram.org...');
const addresses = await dns.resolve4('api.telegram.org');
console.log('IP адреса api.telegram.org:', addresses);
// Пинг для проверки доступности (теперь ждем результат)
try {
const { stdout } = await exec('ping -c 1 api.telegram.org');
console.log('Результат ping:', stdout);
} catch (pingError) {
console.error('Ошибка при выполнении ping:', pingError);
throw new Error('Сервер Telegram недоступен');
}
} catch (error) {
console.error('Ошибка сетевой проверки:', error);
throw error;
}
// Затем проверяем API
try {
const response = await axios.get(
`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/getMe`,
this.axiosConfig
);
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
console.log('Успешное подключение к API Telegram:', {
botInfo: response.data.result
});
} catch (error) {
console.error('Ошибка при проверке API Telegram:', {
message: error.message,
code: error.code,
response: error.response?.data,
config: {
url: error.config?.url,
method: error.config?.method,
timeout: error.config?.timeout
}
});
throw error;
}
// Основная инициализация бота
await this.initBot();
console.log('Telegram bot service initialized');
return;
} catch (error) {
retries++;
console.error('Ошибка при инициализации Telegram бота:', {
name: error.name,
message: error.message,
code: error.code,
response: error.response?.data,
stack: error.stack
});
if (retries < this.maxRetries) {
console.log(`Повторная попытка через ${this.retryDelay/1000} секунд...`);
await sleep(this.retryDelay);
} else {
console.error('Превышено максимальное количество попыток подключения к Telegram');
throw error;
}
}
}
}
async initBot() {
try {
// Проверяем, не запущен ли уже бот
const webhookInfo = await this.bot.getWebHookInfo();
// Если есть webhook или активный polling, пробуем остановить
if (webhookInfo.url || webhookInfo.has_custom_certificate) {
console.log('Удаляем существующий webhook...');
await this.bot.deleteWebHook();
await new Promise(resolve => setTimeout(resolve, 2000));
}
// Пробуем получить обновления с большим таймаутом
try {
console.log('Проверяем наличие других экземпляров бота...');
const updates = await this.bot.getUpdates({
offset: -1,
limit: 1,
timeout: 0
});
console.log('Проверка существующих подключений:', updates);
} catch (error) {
if (error.code === 409) {
console.log('Обнаружен активный бот, пробуем остановить...');
await this.stop();
await new Promise(resolve => setTimeout(resolve, 5000));
// Повторная попытка получить обновления
await this.bot.getUpdates({ offset: -1, limit: 1, timeout: 0 });
}
}
// Небольшая пауза перед запуском поллинга
await new Promise(resolve => setTimeout(resolve, 1000));
// Запускаем polling
console.log('Запускаем polling...');
await this.bot.startPolling({
interval: 2000,
params: {
timeout: 10
}
});
this.isRunning = true;
this.setupHandlers();
this.setupErrorHandlers();
this.setupCommands();
} catch (error) {
if (error.code === 409) {
console.log('Бот уже запущен в другом процессе');
this.isRunning = false;
} else {
console.error('Ошибка при инициализации Telegram бота:', error);
throw error;
}
}
}
setupErrorHandlers() {
this.bot.on('polling_error', (error) => {
console.error('Telegram polling error:', {
code: error.code,
message: error.message,
stack: error.stack
});
// Обработка различных ошибок
if (this.isRunning && (error.code === 'EFATAL' || error.code === 'ETELEGRAM')) {
console.log('Переподключение к Telegram через 5 секунд...');
setTimeout(async () => {
try {
await this.stop();
await this.initBot();
} catch (err) {
console.error('Ошибка при перезапуске бота:', err);
}
}, 5000);
} else if (error.code === 'ECONNRESET' || error.code === 'ECONNREFUSED') {
// Для ошибок соединения пробуем сразу переподключиться
this.bot.startPolling();
}
});
// Обработка других ошибок
this.bot.on('error', (error) => {
console.error('Telegram bot error:', error);
// Пробуем переподключиться при любой ошибке
setTimeout(() => this.bot.startPolling(), 5000);
});
// Обработка webhook ошибок
this.bot.on('webhook_error', (error) => {
console.error('Telegram webhook error:', error);
});
}
async stop() {
if (this.isRunning) {
console.log('Останавливаем Telegram бота...');
try {
// Сначала отключаем обработчики
this.bot.removeAllListeners();
// Останавливаем поллинг
await this.bot.stopPolling();
// Очищаем очередь обновлений
await this.bot.getUpdates({
offset: -1,
limit: 1,
timeout: 1
});
this.isRunning = false;
console.log('Telegram бот остановлен');
} catch (error) {
console.error('Ошибка при остановке бота:', error);
// Принудительно отмечаем как остановленный
this.isRunning = false;
}
}
}
async checkTelegramAvailability() {
const { stdout } = await exec('ping -c 1 api.telegram.org');
const match = stdout.match(/time=(\d+(\.\d+)?)/);
if (match) {
const pingTime = parseFloat(match[1]);
console.log(`Время отклика Telegram API: ${pingTime}ms`);
if (pingTime > 1000) { // Если пинг больше секунды
console.warn('Внимание: высокая задержка при подключении к Telegram API');
}
}
return stdout;
}
}
module.exports = TelegramBotService;

View File

@@ -1,134 +1,212 @@
const { HNSWLib } = require("@langchain/community/vectorstores/hnswlib");
const { OllamaEmbeddings } = require("@langchain/ollama");
const { DirectoryLoader } = require("langchain/document_loaders/fs/directory");
const { TextLoader } = require("langchain/document_loaders/fs/text");
const { RecursiveCharacterTextSplitter } = require("langchain/text_splitter");
const { 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 DOCS_DIR = path.join(__dirname, '../data/documents');
// Путь к директории для хранения векторного индекса
const VECTOR_STORE_DIR = path.join(__dirname, '../data/vector_store');
// Путь к директории для хранения векторной базы данных
const VECTOR_STORE_PATH = path.join(__dirname, '../data/vector_store');
// Создаем директории, если они не существуют
if (!fs.existsSync(DOCS_DIR)) {
fs.mkdirSync(DOCS_DIR, { recursive: true });
console.log(`Создана директория для документов: ${DOCS_DIR}`);
}
// Инициализация embeddings с использованием локальной модели Ollama
const embeddings = new OllamaEmbeddings({
model: process.env.OLLAMA_EMBEDDINGS_MODEL || 'mistral',
baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
});
if (!fs.existsSync(VECTOR_STORE_DIR)) {
fs.mkdirSync(VECTOR_STORE_DIR, { recursive: true });
console.log(`Создана директория для векторного хранилища: ${VECTOR_STORE_DIR}`);
}
// Глобальная переменная для хранения экземпляра векторного хранилища
let vectorStore = null;
// Функция для инициализации векторного хранилища
/**
* Инициализация векторного хранилища
*/
async function initializeVectorStore() {
try {
console.log('Инициализация векторного хранилища...');
// Проверяем, существует ли директория с документами
if (!fs.existsSync(DOCS_DIR)) {
console.warn(`Директория с документами не найдена: ${DOCS_DIR}`);
return null;
// Создание директории, если она не существует
if (!fs.existsSync(VECTOR_STORE_PATH)) {
fs.mkdirSync(VECTOR_STORE_PATH, { recursive: true });
console.log(`Created vector store directory at ${VECTOR_STORE_PATH}`);
}
// Проверяем, есть ли документы в директории
const files = fs.readdirSync(DOCS_DIR);
if (files.length === 0) {
console.warn(`В директории с документами нет файлов: ${DOCS_DIR}`);
return null;
// Проверка наличия файлов индекса
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();
}
console.log(`Найдено ${files.length} файлов в директории с документами`);
// Загружаем документы из директории
const loader = new DirectoryLoader(
DOCS_DIR,
{
".txt": (path) => new TextLoader(path),
".md": (path) => new TextLoader(path),
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),
});
console.log('Загрузка документов...');
const docs = await loader.load();
console.log(`Загружено ${docs.length} документов`);
console.log(`Loaded ${docs.length} documents`);
if (docs.length === 0) {
console.warn('Не удалось загрузить документы');
return null;
}
// Разбиваем документы на чанки
// Создаем пустой векторный индекс, если нет документов
vectorStore = new HNSWLib(embeddings, {
space: 'cosine',
numDimensions: 4096, // Размерность для Ollama embeddings (зависит от модели)
});
} else {
// Разделение документов на чанки
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000,
chunkOverlap: 200,
});
console.log('Разбиение документов на чанки...');
const splitDocs = await textSplitter.splitDocuments(docs);
console.log(`Документы разбиты на ${splitDocs.length} чанков`);
console.log(`Split into ${splitDocs.length} chunks`);
// Создаем эмбеддинги с помощью Ollama
console.log('Создание эмбеддингов...');
const embeddings = new OllamaEmbeddings({
model: "mistral",
baseUrl: "http://localhost:11434",
});
// Создание векторного хранилища
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();
}
// Проверяем, существует ли уже векторное хранилище
if (fs.existsSync(path.join(VECTOR_STORE_DIR, 'hnswlib.index'))) {
console.log('Загрузка существующего векторного хранилища...');
try {
vectorStore = await HNSWLib.load(
VECTOR_STORE_DIR,
embeddings
);
console.log('Векторное хранилище успешно загружено');
return vectorStore;
const results = await vectorStore.similaritySearch(query, k);
return results;
} catch (error) {
console.error('Ошибка при загрузке векторного хранилища:', error);
console.log('Создание нового векторного хранилища...');
}
}
// Создаем новое векторное хранилище
console.log('Создание нового векторного хранилища...');
vectorStore = await HNSWLib.fromDocuments(
splitDocs,
embeddings
);
// Сохраняем векторное хранилище
console.log('Сохранение векторного хранилища...');
await vectorStore.save(VECTOR_STORE_DIR);
console.log('Векторное хранилище успешно сохранено');
return vectorStore;
} catch (error) {
console.error('Ошибка при инициализации векторного хранилища:', error);
console.log('Приложение продолжит работу без векторного хранилища');
// Возвращаем заглушку вместо реального хранилища
return {
addDocuments: async () => console.log('Векторное хранилище недоступно: addDocuments'),
similaritySearch: async () => {
console.log('Векторное хранилище недоступно: similaritySearch');
console.error('Error performing similarity search:', error);
return [];
}
};
}
}
// Функция для получения экземпляра векторного хранилища
async function getVectorStore() {
/**
* Добавление нового документа в векторное хранилище
* @param {string} text - Текст документа
* @param {Object} metadata - Метаданные документа
* @returns {Promise<boolean>} - Успешность добавления
*/
async function addDocument(text, metadata = {}) {
if (!vectorStore) {
vectorStore = await initializeVectorStore();
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;
}
return vectorStore;
}
module.exports = { initializeVectorStore, getVectorStore };
module.exports = {
initializeVectorStore,
getVectorStore,
similaritySearch,
addDocument,
};

View File

@@ -1,7 +1,7 @@
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { expect } = require('chai');
const { ethers } = require('hardhat');
describe("AccessToken", function () {
describe('AccessToken', function () {
let AccessToken;
let accessToken;
let owner;
@@ -10,36 +10,32 @@ describe("AccessToken", function () {
beforeEach(async function () {
[owner, addr1, addr2] = await ethers.getSigners();
AccessToken = await ethers.getContractFactory("AccessToken");
AccessToken = await ethers.getContractFactory('AccessToken');
accessToken = await AccessToken.deploy();
});
describe("Minting", function () {
it("Should mint admin token", async function () {
describe('Minting', function () {
it('Should mint admin token', async function () {
await accessToken.mintAccessToken(addr1.address, 0);
expect(await accessToken.checkRole(addr1.address)).to.equal(0);
});
it("Should mint moderator token", async function () {
it('Should mint moderator token', async function () {
await accessToken.mintAccessToken(addr1.address, 1);
expect(await accessToken.checkRole(addr1.address)).to.equal(1);
});
});
describe("Access Control", function () {
it("Should fail for non-token holders", async function () {
await expect(
accessToken.checkRole(addr1.address)
).to.be.revertedWith("No active token");
describe('Access Control', function () {
it('Should fail for non-token holders', async function () {
await expect(accessToken.checkRole(addr1.address)).to.be.revertedWith('No active token');
});
it("Should revoke access", async function () {
it('Should revoke access', async function () {
await accessToken.mintAccessToken(addr1.address, 0);
const tokenId = await accessToken.activeTokens(addr1.address);
await accessToken.revokeToken(tokenId);
await expect(
accessToken.checkRole(addr1.address)
).to.be.revertedWith("No active token");
await expect(accessToken.checkRole(addr1.address)).to.be.revertedWith('No active token');
});
});
});

View File

@@ -1,7 +1,10 @@
const { ethers } = require('ethers');
require('dotenv').config();
const db = require('../db');
const contractArtifact = require('../artifacts/contracts/MyContract.sol/MyContract.json');
const contractABI = contractArtifact.abi;
const logger = require('./logger');
const { getContract } = require('./contracts');
// Проверяем наличие необходимых переменных окружения
if (!process.env.ACCESS_TOKEN_ADDRESS) {
@@ -18,11 +21,7 @@ let accessToken;
try {
const AccessTokenABI = require('../artifacts/contracts/AccessToken.sol/AccessToken.json').abi;
accessToken = new ethers.Contract(
process.env.ACCESS_TOKEN_ADDRESS,
AccessTokenABI,
provider
);
accessToken = new ethers.Contract(process.env.ACCESS_TOKEN_ADDRESS, AccessTokenABI, provider);
} catch (error) {
console.error('Ошибка инициализации контракта AccessToken:', error);
}
@@ -52,7 +51,7 @@ async function checkAccess(address) {
return {
hasAccess: true,
role,
tokenId: activeTokenId.toString()
tokenId: activeTokenId.toString(),
};
} catch (error) {
console.error('Access check error:', error);
@@ -60,30 +59,138 @@ async function checkAccess(address) {
}
}
async function checkAdmin(address) {
// Функция для проверки, является ли пользователь администратором
async function checkIfAdmin(address) {
try {
console.log('Проверка прав администратора для адреса:', address);
// Проверяем, является ли пользователь администратором через смарт-контракт
const contract = new ethers.Contract(
process.env.CONTRACT_ADDRESS,
contractABI,
provider
);
console.log('Контракт инициализирован:', {
address: process.env.CONTRACT_ADDRESS,
provider: provider.connection.url
});
// Проверяем в базе данных
const result = await db.query('SELECT is_admin FROM users WHERE address = $1', [address]);
const isAdmin = await contract.isAdmin(address);
console.log('Результат проверки из контракта:', isAdmin);
if (result.rows.length === 0) {
console.log(`Пользователь с адресом ${address} не найден в базе данных`);
return false;
}
const isAdmin = result.rows[0].is_admin;
console.log(`Пользователь с адресом ${address} имеет статус администратора:`, isAdmin);
return isAdmin;
} catch (error) {
console.error('Ошибка при проверке прав администратора:', error);
// В случае ошибки возвращаем false вместо выброса исключения
return false;
}
}
module.exports = { checkAccess, checkAdmin };
/**
* Проверяет баланс токенов пользователя и обновляет его роль
* @param {string} address - Адрес кошелька пользователя
* @returns {Promise<boolean>} - Имеет ли пользователь права администратора
*/
async function checkTokenBalanceAndUpdateRole(address) {
try {
// Получение контракта токенов
const accessTokenContract = await getContract('AccessToken');
// Проверка баланса
const balance = await accessTokenContract.balanceOf(address);
// Минимальное количество токенов для прав администратора
const minTokens = ethers.utils.parseUnits(process.env.MIN_ADMIN_TOKENS || "1", 18);
const isAdmin = balance.gte(minTokens);
// Получение 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
`, [address.toLowerCase()]);
if (userResult.rows.length > 0) {
const userId = userResult.rows[0].id;
// Получение ID роли
const roleResult = await db.query(
'SELECT id FROM roles WHERE name = $1',
[isAdmin ? 'admin' : 'user']
);
if (roleResult.rows.length > 0) {
const roleId = roleResult.rows[0].id;
// Обновление роли пользователя
await db.query(
'UPDATE users SET role_id = $1, last_token_check = NOW() WHERE id = $2',
[roleId, userId]
);
logger.info(`Updated user ${userId} role to ${isAdmin ? 'admin' : 'user'} based on token balance`);
}
}
return isAdmin;
} catch (error) {
logger.error(`Error checking token balance for ${address}: ${error.message}`);
return false;
}
}
/**
* Получает информацию о пользователе, включая его роль
* @param {number} userId - ID пользователя
* @returns {Promise<Object>} - Информация о пользователе
*/
async function getUserInfo(userId) {
try {
const result = await db.query(`
SELECT u.id, u.username, u.preferred_language, r.name as role
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
WHERE u.id = $1
`, [userId]);
if (result.rows.length === 0) {
return null;
}
return result.rows[0];
} catch (error) {
logger.error(`Error getting user info for ${userId}: ${error.message}`);
return null;
}
}
/**
* Запускает проверку токенов для всех пользователей
*/
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 token balances for ${walletUsers.rows.length} users`);
for (const user of walletUsers.rows) {
// Проверка баланса токенов
const hasTokens = await checkTokenBalanceAndUpdateRole(user.address);
logger.info(`User ${user.id} with address ${user.address}: admin=${hasTokens}`);
}
} catch (error) {
logger.error(`Error checking token balances: ${error.message}`);
}
}
module.exports = {
checkAccess,
checkIfAdmin,
checkTokenBalanceAndUpdateRole,
getUserInfo,
checkAllUsersTokens
};

View File

@@ -2,13 +2,11 @@ const { SiweMessage } = require('siwe');
const { ethers } = require('ethers');
const AccessTokenABI = require('../artifacts/contracts/AccessToken.sol/AccessToken.json').abi;
require('dotenv').config();
const { pool } = require('../db');
const provider = new ethers.JsonRpcProvider(process.env.ETHEREUM_NETWORK_URL);
const accessToken = new ethers.Contract(
process.env.ACCESS_TOKEN_ADDRESS,
AccessTokenABI,
provider
);
// В ethers.js v6.x используется JsonRpcProvider напрямую
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
const accessToken = new ethers.Contract(process.env.ACCESS_TOKEN_ADDRESS, AccessTokenABI, provider);
// Проверяем наличие адреса контракта
if (!process.env.ACCESS_TOKEN_ADDRESS) {
@@ -25,9 +23,9 @@ if (!process.env.ACCESS_TOKEN_ADDRESS) {
async function verifySignature(message, signature, address) {
try {
// Формируем сообщение для проверки
const domain = message.domain || window.location.host;
const domain = message.domain || 'localhost';
const statement = message.statement || 'Sign in with Ethereum to the app.';
const uri = message.uri || window.location.origin;
const uri = message.uri || 'http://localhost:8000';
const version = message.version || '1';
const chainId = message.chainId || '1';
const nonce = message.nonce;
@@ -43,7 +41,7 @@ Chain ID: ${chainId}
Nonce: ${nonce}
`;
// Восстанавливаем адрес из подписи
// В ethers.js v6.x используется verifyMessage напрямую
const recoveredAddress = ethers.verifyMessage(messageToVerify, signature);
return recoveredAddress.toLowerCase() === address.toLowerCase();
@@ -64,7 +62,7 @@ async function verifyAndCheckAccess(message, signature, address) {
if (!verified) {
return {
verified: false,
access: { hasAccess: false }
access: { hasAccess: false },
};
}
@@ -73,12 +71,114 @@ async function verifyAndCheckAccess(message, signature, address) {
return {
verified: true,
access
access,
};
}
// Функция для поиска или создания пользователя
async function findOrCreateUser(identifier, identityType = 'wallet') {
try {
// Проверяем, является ли адрес адресом администратора
const isAdmin = identityType === 'wallet' &&
identifier.toLowerCase() === process.env.ADMIN_WALLET_ADDRESS.toLowerCase();
console.log(`Проверка на администратора: ${identifier.toLowerCase()} === ${process.env.ADMIN_WALLET_ADDRESS.toLowerCase()} = ${isAdmin}`);
// Проверяем, существует ли пользователь с таким идентификатором
const identityResult = await pool.query(
'SELECT user_id FROM user_identities WHERE identity_type = $1 AND identity_value = $2',
[identityType, identifier.toLowerCase()]
);
let userId;
let isNewUser = false;
if (identityResult.rows.length > 0) {
// Пользователь найден
userId = identityResult.rows[0].user_id;
console.log(`Найден существующий пользователь с ID: ${userId}`);
// Обновляем статус администратора, если это необходимо
if (isAdmin) {
await pool.query(
'UPDATE users SET is_admin = true WHERE id = $1',
[userId]
);
console.log(`Обновлен статус администратора для пользователя ${userId}`);
}
} else {
// Создаем нового пользователя с явным указанием всех необходимых полей
const username = `user_${Date.now()}`;
// Проверяем существование роли USER или ADMIN
const roleName = isAdmin ? 'ADMIN' : 'USER';
const roleCheck = await pool.query('SELECT id FROM roles WHERE name = $1', [roleName]);
let roleId;
if (roleCheck.rows.length > 0) {
roleId = roleCheck.rows[0].id;
} else {
// Если роли нет, создаем её
const newRole = await pool.query(
'INSERT INTO roles (name, description) VALUES ($1, $2) RETURNING id',
[roleName, isAdmin ? 'Administrator role' : 'Regular user role']
);
roleId = newRole.rows[0].id;
}
// Создаем пользователя с обязательными полями
const userResult = await pool.query(
`INSERT INTO users (username, role_id, address, created_at, is_admin)
VALUES ($1, $2, $3, NOW(), $4)
RETURNING id`,
[username, roleId, identifier.toLowerCase(), isAdmin]
);
userId = userResult.rows[0].id;
isNewUser = true;
// Создаем запись в таблице идентификаторов
await pool.query(
'INSERT INTO user_identities (user_id, identity_type, identity_value, verified, created_at) VALUES ($1, $2, $3, true, NOW())',
[userId, identityType, identifier.toLowerCase()]
);
console.log(`Создан новый пользователь с ID: ${userId}, isAdmin: ${isAdmin}`);
}
// Получаем информацию о пользователе
try {
const userInfo = await pool.query(
`SELECT u.id, u.username, u.is_admin, r.name as role
FROM users u
JOIN roles r ON u.role_id = r.id
WHERE u.id = $1`,
[userId]
);
if (userInfo.rows.length > 0) {
return userInfo.rows[0];
}
} catch (err) {
console.error('Ошибка при получении информации о пользователе:', err);
}
// Если не удалось получить полную информацию, возвращаем базовую
return {
id: userId,
username: isNewUser ? `user_${Date.now()}` : 'unknown',
is_admin: isAdmin,
role: isAdmin ? 'ADMIN' : 'USER'
};
} catch (error) {
console.error('Ошибка при поиске/создании пользователя:', error);
throw error;
}
}
module.exports = {
verifyAndCheckAccess,
verifySignature,
checkAccess
checkAccess,
findOrCreateUser
};

View File

@@ -0,0 +1,46 @@
const { ethers } = require('ethers');
const fs = require('fs');
const path = require('path');
const logger = require('./logger');
/**
* Получает экземпляр контракта по его имени
* @param {string} contractName - Имя контракта (например, 'AccessToken')
* @returns {Promise<ethers.Contract>} - Экземпляр контракта
*/
async function getContract(contractName) {
try {
// Путь к артефакту контракта
const artifactPath = path.join(__dirname, '..', 'artifacts', 'contracts', `${contractName}.sol`, `${contractName}.json`);
// Проверка существования файла
if (!fs.existsSync(artifactPath)) {
throw new Error(`Артефакт контракта ${contractName} не найден по пути ${artifactPath}`);
}
// Загрузка ABI из артефакта
const contractArtifact = require(artifactPath);
const contractABI = contractArtifact.abi;
// Получение адреса контракта из переменных окружения
const contractAddress = process.env[`${contractName.toUpperCase()}_ADDRESS`];
if (!contractAddress) {
throw new Error(`Адрес контракта ${contractName} не найден в переменных окружения`);
}
// Подключение к провайдеру
const provider = new ethers.JsonRpcProvider(process.env.ETHEREUM_NETWORK_URL);
// Создание экземпляра контракта
const contract = new ethers.Contract(contractAddress, contractABI, provider);
return contract;
} catch (error) {
logger.error(`Ошибка при получении контракта ${contractName}: ${error.message}`);
throw error;
}
}
module.exports = {
getContract
};

2
backend/utils/db.js Normal file
View File

@@ -0,0 +1,2 @@
// Реэкспорт основного модуля db
module.exports = require('../db');

View File

@@ -1,6 +1,6 @@
// Функция для создания задержки
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Функция для валидации email адреса
@@ -11,5 +11,5 @@ function isValidEmail(email) {
module.exports = {
sleep,
isValidEmail
isValidEmail,
};

View File

@@ -3,7 +3,7 @@ const { Pool } = require('pg');
// Подключение к БД
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
});
/**
@@ -91,5 +91,5 @@ async function getUserIdentities(userId) {
module.exports = {
linkIdentity,
getUserIdByIdentity,
getUserIdentities
getUserIdentities,
};

View File

@@ -3,25 +3,19 @@ const path = require('path');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
format: winston.format.combine(winston.format.colorize(), winston.format.simple()),
}),
new winston.transports.File({
filename: path.join(__dirname, '../logs/error.log'),
level: 'error'
level: 'error',
}),
new winston.transports.File({
filename: path.join(__dirname, '../logs/combined.log')
})
]
filename: path.join(__dirname, '../logs/combined.log'),
}),
],
});
module.exports = logger;

4
backend/utils/wallet.js Normal file
View File

@@ -0,0 +1,4 @@
import { ethers } from 'ethers';
const provider = new ethers.JsonRpcProvider(process.env.ETHEREUM_NETWORK_URL);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);

File diff suppressed because it is too large Load Diff

10
frontend/.prettierrc Normal file
View File

@@ -0,0 +1,10 @@
{
"endOfLine": "lf",
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"vueIndentScriptAndStyle": true,
"singleAttributePerLine": false
}

64
frontend/eslint.config.js Normal file
View File

@@ -0,0 +1,64 @@
import globals from 'globals';
import vuePlugin from 'eslint-plugin-vue';
import prettierPlugin from 'eslint-plugin-prettier';
import prettierConfig from '@vue/eslint-config-prettier';
export default [
{
ignores: ['node_modules/**', 'dist/**', 'public/**'],
},
{
files: ['**/*.js'],
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
globals: {
...globals.browser,
...globals.es2021,
process: 'readonly',
__dirname: 'readonly',
},
},
rules: {
'no-unused-vars': 'off',
'no-console': 'off',
'no-undef': 'error',
'no-duplicate-imports': 'error',
},
},
{
files: ['**/*.vue'],
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
globals: {
...globals.browser,
...globals.es2021,
},
parser: vuePlugin.parser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
plugins: {
vue: vuePlugin,
prettier: prettierPlugin,
},
processor: vuePlugin.processors['.vue'],
rules: {
...prettierConfig.rules,
'vue/multi-word-component-names': 'off',
'vue/no-unused-vars': 'warn',
'vue/html-self-closing': ['warn', {
html: {
void: 'always',
normal: 'always',
component: 'always'
}
}],
'vue/component-name-in-template-casing': ['warn', 'PascalCase'],
},
},
];

File diff suppressed because it is too large Load Diff

View File

@@ -6,23 +6,36 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --ignore-pattern 'node_modules/'",
"lint:fix": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-pattern 'node_modules/'",
"format": "prettier --write \"**/*.{js,vue,json,md}\"",
"format:check": "prettier --check \"**/*.{js,vue,json,md}\""
},
"dependencies": {
"axios": "^1.3.4",
"axios": "^1.8.1",
"buffer": "^6.0.3",
"connect-pg-simple": "^10.0.0",
"ethers": "6.13.5",
"pinia": "^2.0.33",
"siwe": "^2.1.4",
"sortablejs": "^1.15.6",
"vue": "^3.2.47",
"vue-i18n": "9",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.1.0",
"@vitejs/plugin-vue": "^5.2.1",
"@vue/eslint-config-prettier": "^10.2.0",
"axios-mock-adapter": "^2.1.0",
"eslint": "^9.21.0",
"eslint-config-prettier": "^10.0.2",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-vue": "^9.32.0",
"globals": "^16.0.0",
"prettier": "^3.5.3",
"rollup": "^3.29.4",
"rollup-plugin-polyfill-node": "^0.12.0",
"vite": "^4.2.1"
"vite": "^6.2.0"
}
}

View File

@@ -1,286 +1,215 @@
<template>
<div id="app">
<header class="app-header">
<div class="header-brand">
<h1>DApp for Business</h1>
</div>
<div class="header-auth">
<template v-if="auth.isAuthenticated">
<span class="user-address">{{ shortAddress }}</span>
<button class="btn btn-outline" @click="handleDisconnect">Отключить кошелек</button>
</template>
<template v-else>
<button class="btn btn-primary" @click="navigateToHome">Подключиться</button>
</template>
</div>
</header>
<div class="app-layout">
<!-- Сайдбар для авторизованных пользователей -->
<aside v-if="auth.isAuthenticated" class="sidebar">
<nav class="sidebar-nav">
<router-link to="/" class="nav-item">
<span class="nav-icon">🏠</span>
<span class="nav-text">Главная</span>
</router-link>
<router-link v-if="auth.isAdmin" to="/dashboard" class="nav-item">
<span class="nav-icon">📊</span>
<span class="nav-text">Дашборд</span>
</router-link>
<router-link to="/kanban" class="nav-item">
<span class="nav-icon">📋</span>
<span class="nav-text">Канбан</span>
</router-link>
<router-link v-if="auth.isAdmin" to="/access-test" class="nav-item">
<span class="nav-icon">🔐</span>
<span class="nav-text">Смарт-контракты</span>
</router-link>
</nav>
</aside>
<navigation />
<main class="main-content">
<div v-if="isLoading" class="loading">
Загрузка...
</div>
<router-view v-else />
<router-view />
</main>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed, provide } from 'vue';
import { useRouter } from 'vue-router';
import { onMounted, watch } from 'vue';
import { useAuthStore } from './stores/auth';
import Navigation from './components/Navigation.vue';
import axios from 'axios';
import { connectWallet } from './services/wallet';
const router = useRouter();
const auth = useAuthStore();
const isLoading = ref(true);
const authStore = useAuthStore();
// Вычисляемое свойство для отображения сокращенного адреса
const shortAddress = computed(() => {
if (!auth.address) return '';
return `${auth.address.substring(0, 6)}...${auth.address.substring(auth.address.length - 4)}`;
});
// Проверка сессии при загрузке приложения
async function checkSession() {
try {
// Проверяем, установлены ли куки
const cookies = document.cookie;
console.log('Текущие куки:', cookies);
await authStore.checkAuth();
console.log('Проверка сессии:', {
authenticated: authStore.isAuthenticated,
address: authStore.address,
isAdmin: authStore.isAdmin,
authType: authStore.authType,
});
console.log('Проверка аутентификации при загрузке:', authStore.isAuthenticated);
console.log('Статус администратора при загрузке:', authStore.isAdmin);
// Если пользователь авторизован, но куки не установлены, пробуем обновить сессию
if (authStore.isAuthenticated && !cookies.includes('connect.sid')) {
console.log('Куки не установлены, пробуем обновить сессию');
await refreshSession();
}
} catch (error) {
console.error('Ошибка при проверке сессии:', error);
}
}
// Функция для обновления сессии
async function refreshSession() {
try {
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000';
// Проверяем, есть ли адрес пользователя
if (!authStore.address) {
console.log('Нет адреса пользователя для обновления сессии');
return;
}
console.log('Попытка обновления сессии для адреса:', authStore.address);
// Сначала проверяем, доступен ли маршрут
try {
const response = await axios.post(
`${apiUrl}/api/auth/refresh-session`,
{
address: authStore.address,
},
{
withCredentials: true,
}
);
console.log('Сессия обновлена:', response.data);
} catch (error) {
if (error.response && error.response.status === 404) {
console.log('Маршрут refresh-session не найден, пробуем альтернативный метод');
// Альтернативный метод: используем маршрут проверки аутентификации
const checkResponse = await axios.get(`${apiUrl}/api/auth/check`, {
withCredentials: true,
headers: {
Authorization: `Bearer ${authStore.address}`,
},
});
console.log('Проверка аутентификации:', checkResponse.data);
} else {
throw error;
}
}
} catch (error) {
console.error('Ошибка при обновлении сессии:', error);
// Добавляем более подробную информацию об ошибке
if (error.response) {
console.error('Статус ответа:', error.response.status);
console.error('Данные ответа:', error.response.data);
}
}
}
// Проверяем сессию при загрузке приложения
onMounted(async () => {
console.log('App mounted');
// Проверяем куки
const cookies = document.cookie;
console.log('Куки при загрузке:', cookies);
try {
// Восстанавливаем состояние аутентификации из localStorage
auth.restoreAuth();
// Проверяем текущую сессию
const response = await axios.get('/api/auth/check', { withCredentials: true });
console.log('Ответ проверки сессии:', response.data);
// Проверяем сессию на сервере
const response = await axios.get('/api/auth/check');
console.log('Проверка сессии:', response.data);
// Если сессия активна, но состояние аутентификации не установлено
if (response.data.authenticated && !auth.isAuthenticated) {
auth.setAuth({
if (response.data.authenticated) {
// Если сессия активна, обновляем состояние аутентификации
authStore.updateAuthState({
authenticated: response.data.authenticated,
address: response.data.address,
isAdmin: response.data.isAdmin,
authType: response.data.authType || 'wallet'
authType: 'wallet'
});
}
// Если сессия не активна, но состояние аутентификации установлено
if (!response.data.authenticated && auth.isAuthenticated) {
auth.disconnect();
console.log('Сессия восстановлена:', response.data);
} else {
console.log('Нет активной сессии');
// Если в localStorage есть адрес, пробуем восстановить сессию
const savedAddress = localStorage.getItem('walletAddress');
if (savedAddress) {
console.log('Найден сохраненный адрес:', savedAddress);
try {
const refreshResponse = await axios.post('/api/auth/refresh-session',
{ address: savedAddress },
{ withCredentials: true }
);
if (refreshResponse.data.success) {
authStore.updateAuthState({
authenticated: true,
address: savedAddress,
isAdmin: refreshResponse.data.user.isAdmin,
authType: 'wallet'
});
console.log('Сессия восстановлена через refresh-session');
}
} catch (refreshError) {
console.error('Ошибка при восстановлении сессии:', refreshError);
}
}
}
} catch (error) {
console.error('Error checking session:', error);
// Не отключаем пользователя при ошибке проверки сессии
} finally {
isLoading.value = false;
console.error('Ошибка при проверке сессии:', error);
}
});
// Функция для отключения кошелька
async function handleDisconnect() {
await auth.disconnect();
router.push('/');
}
// Функция для подключения кошелька
async function navigateToHome() {
console.log('Connecting wallet...');
try {
await connectWallet((errorMessage) => {
console.error('Ошибка при подключении кошелька:', errorMessage);
// Можно добавить отображение ошибки пользователю
});
} catch (error) {
console.error('Ошибка при подключении кошелька:', error);
// Если не удалось подключить кошелек, перенаправляем на главную страницу
console.log('Navigating to home page');
router.push('/');
// Добавляем небольшую задержку, чтобы убедиться, что компонент HomeView загрузился
setTimeout(() => {
// Прокручиваем страницу вниз, чтобы показать опции подключения
const chatMessages = document.querySelector('.chat-messages');
if (chatMessages) {
chatMessages.scrollTop = chatMessages.scrollHeight;
}
// Если опции подключения еще не отображаются, имитируем отправку сообщения
const authOptions = document.querySelector('.auth-options');
if (!authOptions) {
const sendButton = document.querySelector('.send-btn');
if (sendButton) {
// Заполняем поле ввода
const textarea = document.querySelector('textarea');
if (textarea) {
textarea.value = 'Привет';
}
// Нажимаем кнопку отправки
sendButton.click();
// Следим за изменением статуса аутентификации
watch(
() => authStore.isAuthenticated,
(isAuthenticated) => {
if (isAuthenticated) {
console.log('Пользователь авторизован, проверяем куки');
const cookies = document.cookie;
if (!cookies.includes('connect.sid')) {
console.log('Куки не установлены после авторизации, пробуем обновить сессию');
refreshSession();
}
}
}, 500);
}
}
// Предоставляем состояние аутентификации всем компонентам
provide('auth', auth);
);
</script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Roboto', 'Helvetica Neue', sans-serif;
line-height: 1.6;
margin: 0;
font-family: 'Roboto', 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #333;
background-color: #f5f5f5;
}
#app {
height: 100vh;
display: flex;
flex-direction: column;
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #1976d2;
color: white;
padding: 0.75rem 1.5rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
z-index: 100;
}
.header-brand h1 {
font-size: 1.5rem;
margin: 0;
}
.header-auth {
display: flex;
align-items: center;
gap: 1rem;
}
.user-address {
font-family: monospace;
background-color: rgba(255, 255, 255, 0.2);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.9rem;
}
.btn {
background: none;
border: 1px solid white;
color: white;
padding: 0.25rem 0.75rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
}
.btn:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.btn-outline {
border: 1px solid white;
}
.btn-primary {
background-color: white;
color: #1976d2;
border: none;
}
.app-layout {
display: flex;
flex: 1;
overflow: hidden;
}
.sidebar {
width: 250px;
background-color: #fff;
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.05);
overflow-y: auto;
z-index: 50;
}
.sidebar-nav {
padding: 1rem 0;
}
.nav-item {
display: flex;
align-items: center;
padding: 0.75rem 1.5rem;
color: #333;
text-decoration: none;
transition: background-color 0.2s;
}
.nav-item:hover {
background-color: #f5f5f5;
}
.nav-item.router-link-active {
background-color: #e3f2fd;
color: #1976d2;
border-left: 3px solid #1976d2;
}
.nav-icon {
margin-right: 0.75rem;
font-size: 1.2rem;
min-height: 100vh;
}
.main-content {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
background-color: #f5f5f5;
padding: 1rem;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
font-size: 1.2rem;
color: #666;
button {
cursor: pointer;
}
.btn {
padding: 0.5rem 1rem;
border-radius: 4px;
font-weight: 500;
border: none;
}
.btn-primary {
background-color: #3498db;
color: white;
}
.btn-secondary {
background-color: #95a5a6;
color: white;
}
.btn-danger {
background-color: #e74c3c;
color: white;
}
</style>

4
frontend/src/api/auth.js Normal file
View File

@@ -0,0 +1,4 @@
import axios from 'axios';
// Настройка axios для работы с куками
axios.defaults.withCredentials = true;

View File

@@ -1,14 +1,13 @@
<template>
<div class="access-control">
<div v-if="error" class="error-message">
{{ error }}
</div>
<div v-else-if="loading" class="loading-message">Загрузка...</div>
<div v-else>
<div v-if="!isConnected" class="alert alert-warning">
Подключите ваш кошелек для проверки доступа
</div>
<div v-else-if="loading" class="alert alert-info">
Проверка доступа...
</div>
<div v-else-if="error" class="alert alert-danger">
{{ error }}
</div>
<div v-else-if="accessInfo.hasAccess" class="alert alert-success">
<strong>Доступ разрешен!</strong>
<div>Токен: {{ accessInfo.token }}</div>
@@ -20,21 +19,24 @@
<p>У вас нет активного токена доступа.</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import { useEthereum } from '../composables/useEthereum';
import axios from 'axios';
import { useAuthStore } from '../stores/auth';
const { address, isConnected } = useEthereum();
const authStore = useAuthStore();
const address = ref('');
const isConnected = ref(true);
const loading = ref(false);
const error = ref(null);
const accessInfo = ref({
hasAccess: false,
token: '',
role: '',
expiresAt: null
expiresAt: null,
});
// Форматирование даты
@@ -53,8 +55,8 @@ async function checkAccess() {
try {
const response = await axios.get('/access/check', {
headers: {
'x-wallet-address': address.value
}
'x-wallet-address': address.value,
},
});
accessInfo.value = response.data;
@@ -68,9 +70,12 @@ async function checkAccess() {
}
// Проверяем доступ при изменении адреса
watch(() => address.value, () => {
watch(
() => address.value,
() => {
checkAccess();
});
}
);
// Проверяем доступ при монтировании компонента
onMounted(() => {
@@ -78,4 +83,106 @@ onMounted(() => {
checkAccess();
}
});
async function loadTokens() {
try {
console.log('Загрузка токенов...');
loading.value = true;
// Добавляем withCredentials для передачи куки с сессией
const response = await axios.get('/api/access/tokens', {
withCredentials: true,
});
console.log('Ответ API:', response.data);
if (response.data && response.data.length > 0) {
// Если есть токены, берем первый активный
const activeToken = response.data.find((token) => {
const expiresAt = new Date(token.expires_at);
return expiresAt > new Date();
});
if (activeToken) {
accessInfo.value = {
hasAccess: true,
token: activeToken.id,
role: activeToken.role,
expiresAt: activeToken.expires_at,
};
} else {
accessInfo.value = { hasAccess: false };
}
} else {
accessInfo.value = { hasAccess: false };
}
} catch (error) {
console.error('Ошибка при загрузке токенов:', error);
error.value = 'Ошибка при проверке доступа: ' + (error.response?.data?.error || error.message);
accessInfo.value = { hasAccess: false };
} finally {
loading.value = false;
}
}
onMounted(async () => {
console.log('Компонент AccessControl загружен');
console.log('isAdmin:', authStore.isAdmin);
await loadTokens();
});
</script>
<style scoped>
.access-control {
margin: 20px 0;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
}
.alert {
padding: 10px 15px;
margin-bottom: 10px;
border-radius: 4px;
}
.alert-warning {
background-color: #fff3cd;
border: 1px solid #ffeeba;
color: #856404;
}
.alert-info {
background-color: #d1ecf1;
border: 1px solid #bee5eb;
color: #0c5460;
}
.alert-danger {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.alert-success {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.error-message {
color: #721c24;
background-color: #f8d7da;
padding: 10px;
border-radius: 4px;
margin-bottom: 15px;
}
.loading-message {
color: #0c5460;
background-color: #d1ecf1;
padding: 10px;
border-radius: 4px;
margin-bottom: 15px;
}
</style>

View File

@@ -1,194 +0,0 @@
<template>
<div class="card">
<div class="card-header">
<h5>Проверка доступа</h5>
</div>
<div class="card-body">
<div v-if="!isConnected" class="alert alert-warning">
Подключите ваш кошелек для проверки доступа
</div>
<div v-else>
<div class="mb-3">
<h6>Статус доступа:</h6>
<div v-if="loading" class="alert alert-info">
Проверка доступа...
</div>
<div v-else-if="error" class="alert alert-danger">
{{ error }}
</div>
<div v-else-if="accessInfo.hasAccess" class="alert alert-success">
<strong>Доступ разрешен!</strong>
<div>Токен: {{ accessInfo.token }}</div>
<div>Роль: {{ accessInfo.role }}</div>
<div>Истекает: {{ formatDate(accessInfo.expiresAt) }}</div>
</div>
<div v-else class="alert alert-danger">
<strong>Доступ запрещен!</strong>
<p>У вас нет активного токена доступа.</p>
</div>
</div>
<div class="mb-3">
<h6>Тестирование API:</h6>
<div class="d-grid gap-2">
<button
@click="testPublicAPI"
class="btn btn-primary mb-2"
>
Тест публичного API
</button>
<button
@click="testProtectedAPI"
class="btn btn-warning mb-2"
>
Тест защищенного API
</button>
<button
@click="testAdminAPI"
class="btn btn-danger"
>
Тест админского API
</button>
</div>
</div>
<div v-if="apiResult" class="mt-3">
<h6>Результат запроса:</h6>
<div :class="['alert', apiResult.success ? 'alert-success' : 'alert-danger']">
<strong>{{ apiResult.message }}</strong>
<div v-if="apiResult.data">
<pre>{{ JSON.stringify(apiResult.data, null, 2) }}</pre>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import { useEthereum } from './useEthereum';
import axios from 'axios';
const { address, isConnected } = useEthereum();
const loading = ref(false);
const error = ref(null);
const accessInfo = ref({
hasAccess: false,
token: '',
role: '',
expiresAt: null
});
const apiResult = ref(null);
// Форматирование даты
function formatDate(timestamp) {
if (!timestamp) return 'Н/Д';
return new Date(timestamp).toLocaleString();
}
// Проверка доступа
async function checkAccess() {
if (!isConnected.value || !address.value) return;
loading.value = true;
error.value = null;
try {
const response = await axios.get('/api/access/check', {
headers: {
'x-wallet-address': address.value
}
});
accessInfo.value = response.data;
} catch (err) {
console.error('Ошибка проверки доступа:', err);
error.value = err.response?.data?.error || 'Ошибка проверки доступа';
accessInfo.value = { hasAccess: false };
} finally {
loading.value = false;
}
}
// Тест публичного API
async function testPublicAPI() {
apiResult.value = null;
try {
const response = await axios.get('/api/public');
apiResult.value = {
success: true,
message: 'Публичный API доступен',
data: response.data
};
} catch (err) {
apiResult.value = {
success: false,
message: 'Ошибка доступа к публичному API',
data: err.response?.data
};
}
}
// Тест защищенного API
async function testProtectedAPI() {
apiResult.value = null;
try {
const response = await axios.get('/api/protected', {
headers: {
'x-wallet-address': address.value
}
});
apiResult.value = {
success: true,
message: 'Защищенный API доступен',
data: response.data
};
} catch (err) {
apiResult.value = {
success: false,
message: 'Ошибка доступа к защищенному API',
data: err.response?.data
};
}
}
// Тест админского API
async function testAdminAPI() {
apiResult.value = null;
try {
const response = await axios.get('/api/admin', {
headers: {
'x-wallet-address': address.value
}
});
apiResult.value = {
success: true,
message: 'Админский API доступен',
data: response.data
};
} catch (err) {
apiResult.value = {
success: false,
message: 'Ошибка доступа к админскому API',
data: err.response?.data
};
}
}
// Проверяем доступ при изменении адреса
watch(() => address.value, () => {
checkAccess();
});
// Проверяем доступ при монтировании компонента
onMounted(() => {
if (isConnected.value && address.value) {
checkAccess();
}
});
</script>

View File

@@ -1,209 +1,161 @@
<template>
<div class="card">
<div class="card-header">
<h5>Управление токенами доступа</h5>
<div class="access-token-manager">
<h3>Управление токенами доступа</h3>
<div class="token-actions">
<button @click="mintNewToken">Выпустить новый токен</button>
<button @click="loadTokens">Обновить список</button>
</div>
<div class="card-body">
<div v-if="!isConnected" class="alert alert-warning">
Подключите ваш кошелек для управления токенами
</div>
<div v-else-if="loading" class="alert alert-info">
Загрузка...
</div>
<div v-else>
<h6>Создать новый токен</h6>
<form @submit.prevent="createToken" class="mb-4">
<div class="mb-3">
<label for="walletAddress" class="form-label">Адрес кошелька</label>
<input
type="text"
class="form-control"
id="walletAddress"
v-model="newToken.walletAddress"
placeholder="0x..."
required
/>
</div>
<div class="mb-3">
<label for="role" class="form-label">Роль</label>
<select class="form-select" id="role" v-model="newToken.role" required>
<option value="USER">Пользователь</option>
<option value="ADMIN">Администратор</option>
</select>
</div>
<div class="mb-3">
<label for="expiresAt" class="form-label">Срок действия (дни)</label>
<input
type="number"
class="form-control"
id="expiresAt"
v-model="newToken.expiresInDays"
min="1"
max="365"
required
/>
</div>
<button type="submit" class="btn btn-primary">Создать токен</button>
</form>
<h6>Активные токены</h6>
<div v-if="tokens.length === 0" class="alert alert-info">
Нет активных токенов
</div>
<div v-else class="table-responsive">
<table class="table table-striped">
<div v-if="loading">Загрузка...</div>
<table v-else-if="tokens.length > 0" class="tokens-table">
<thead>
<tr>
<th>ID</th>
<th>Адрес</th>
<th>Владелец</th>
<th>Роль</th>
<th>Истекает</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<tr v-for="token in tokens" :key="token.id">
<td>{{ token.id }}</td>
<td>{{ shortenAddress(token.walletAddress) }}</td>
<td>{{ token.role }}</td>
<td>{{ formatDate(token.expiresAt) }}</td>
<td>{{ token.owner }}</td>
<td>{{ getRoleName(token.role) }}</td>
<td>
<button
@click="revokeToken(token.id)"
class="btn btn-sm btn-danger"
>
Отозвать
</button>
<button @click="revokeToken(token.id)">Отозвать</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div v-else>Нет доступных токенов</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useEthereum } from './useEthereum';
import axios from 'axios';
const { address, isConnected } = useEthereum();
const loading = ref(false);
const tokens = ref([]);
const newToken = ref({
walletAddress: '',
role: 'USER',
expiresInDays: 30
});
const loading = ref(false);
// Сокращение адреса кошелька
function shortenAddress(addr) {
if (!addr) return '';
return `${addr.substring(0, 6)}...${addr.substring(addr.length - 4)}`;
const roles = {
0: 'Администратор',
1: 'Модератор',
2: 'Пользователь',
};
function getRoleName(roleId) {
return roles[roleId] || 'Неизвестная роль';
}
// Форматирование даты
function formatDate(timestamp) {
if (!timestamp) return 'Н/Д';
return new Date(timestamp).toLocaleString();
}
// Загрузка токенов
async function loadTokens() {
if (!isConnected.value || !address.value) return;
try {
console.log('Загрузка токенов...');
loading.value = true;
try {
// Добавляем withCredentials для передачи куки с сессией
const response = await axios.get('/api/access/tokens', {
headers: {
'x-wallet-address': address.value
}
withCredentials: true,
});
console.log('Ответ API:', response.data);
tokens.value = response.data;
} catch (err) {
console.error('Ошибка загрузки токенов:', err);
alert('Ошибка загрузки токенов: ' + (err.response?.data?.error || err.message));
} catch (error) {
console.error('Ошибка при загрузке токенов:', error);
if (error.response) {
console.error('Статус ошибки:', error.response.status);
console.error('Данные ошибки:', error.response.data);
} else if (error.request) {
console.error('Запрос без ответа:', error.request);
} else {
console.error('Ошибка настройки запроса:', error.message);
}
} finally {
loading.value = false;
}
}
// Создание токена
async function createToken() {
if (!isConnected.value || !address.value) return;
loading.value = true;
async function mintNewToken() {
try {
await axios.post('/api/access/tokens',
const walletAddress = prompt('Введите адрес получателя:');
if (!walletAddress) return;
const role = prompt('Введите роль (ADMIN, MODERATOR, USER):');
if (!role) return;
const expiresInDays = prompt('Введите срок действия в днях:');
if (!expiresInDays) return;
// Используем правильные имена параметров
await axios.post(
'/api/access/mint',
{
walletAddress: newToken.value.walletAddress,
role: newToken.value.role,
expiresInDays: parseInt(newToken.value.expiresInDays)
walletAddress,
role,
expiresInDays,
},
{
headers: {
'x-wallet-address': address.value
}
withCredentials: true,
}
);
// Сбрасываем форму
newToken.value = {
walletAddress: '',
role: 'USER',
expiresInDays: 30
};
// Перезагружаем список токенов
await loadTokens();
alert('Токен успешно создан');
} catch (err) {
console.error('Ошибка создания токена:', err);
alert('Ошибка создания токена: ' + (err.response?.data?.error || err.message));
} finally {
loading.value = false;
} catch (error) {
console.error('Ошибка при выпуске токена:', error);
if (error.response) {
console.error('Статус ошибки:', error.response.status);
console.error('Данные ошибки:', error.response.data);
}
}
}
// Отзыв токена
async function revokeToken(tokenId) {
if (!isConnected.value || !address.value) return;
if (!confirm('Вы уверены, что хотите отозвать этот токен?')) {
return;
}
loading.value = true;
try {
await axios.delete(`/api/access/tokens/${tokenId}`, {
headers: {
'x-wallet-address': address.value
}
});
if (!confirm(`Вы уверены, что хотите отозвать токен #${tokenId}?`)) return;
// Перезагружаем список токенов
await axios.post('/api/access/revoke', { tokenId });
await loadTokens();
alert('Токен успешно отозван');
} catch (err) {
console.error('Ошибка отзыва токена:', err);
alert('Ошибка отзыва токена: ' + (err.response?.data?.error || err.message));
} finally {
loading.value = false;
} catch (error) {
console.error('Ошибка при отзыве токена:', error);
}
}
// Загружаем токены при монтировании компонента
onMounted(() => {
if (isConnected.value && address.value) {
loadTokens();
}
onMounted(async () => {
await loadTokens();
});
</script>
<style scoped>
.access-token-manager {
margin: 20px 0;
}
.token-actions {
margin: 15px 0;
}
.tokens-table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
.tokens-table th,
.tokens-table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.tokens-table th {
background-color: #f2f2f2;
}
button {
margin-right: 5px;
padding: 5px 10px;
cursor: pointer;
}
</style>

View File

@@ -1,298 +0,0 @@
<template>
<div class="ai-chat">
<div v-if="!isAuthenticated" class="connect-wallet-message">
Для отправки сообщений необходимо подключить кошелек
</div>
<div class="chat-messages" ref="messagesContainer">
<div v-for="(message, index) in messages" :key="index"
:class="['message', message.role]">
{{ message.content }}
</div>
</div>
<div class="chat-input">
<textarea
v-model="userInput"
@keydown.enter.prevent="sendMessage"
placeholder="Введите ваше сообщение..."
:disabled="!isAuthenticated"
:class="{ 'disabled': !isAuthenticated }"
></textarea>
<button
@click="sendMessage"
:disabled="!isAuthenticated || !userInput.trim()"
:class="{ 'disabled': !isAuthenticated }"
>
{{ isAuthenticated ? 'Отправить' : 'Подключите кошелек' }}
</button>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useAuthStore } from '../stores/auth';
import { useRouter } from 'vue-router';
import axios from 'axios';
const auth = useAuthStore();
const router = useRouter();
const messages = ref([]);
const userInput = ref('');
const messagesContainer = ref(null);
const isAuthenticated = computed(() => auth.isAuthenticated);
const currentUserAddress = computed(() => auth.address);
async function checkAndRefreshSession() {
try {
// Проверяем, есть ли активная сессия
const sessionResponse = await fetch('/api/session', {
credentials: 'include'
});
if (!sessionResponse.ok) {
console.error('Ошибка при проверке сессии:', sessionResponse.status, sessionResponse.statusText);
// Проверяем, доступен ли сервер
try {
const pingResponse = await fetch('/api/debug/ping');
if (!pingResponse.ok) {
throw new Error(`Сервер недоступен: ${pingResponse.status} ${pingResponse.statusText}`);
}
const pingData = await pingResponse.json();
console.log('Ping response:', pingData);
} catch (pingError) {
console.error('Ошибка при проверке доступности сервера:', pingError);
throw new Error('Сервер недоступен. Пожалуйста, убедитесь, что сервер запущен и доступен.');
}
// Пробуем восстановить из localStorage
if (auth.restoreAuth()) {
console.log('Сессия восстановлена из localStorage в Chats');
return true;
}
throw new Error(`Ошибка сервера: ${sessionResponse.status} ${sessionResponse.statusText}`);
}
const sessionData = await sessionResponse.json();
console.log('Проверка сессии в Chats:', sessionData);
// Проверяем аутентификацию
if (sessionData.isAuthenticated || sessionData.authenticated) {
// Сессия активна, обновляем состояние auth store
auth.setAuth(sessionData.address, sessionData.isAdmin);
return true;
} else {
// Сессия не активна, пробуем восстановить из localStorage
if (auth.restoreAuth()) {
console.log('Сессия восстановлена из localStorage в Chats');
return true;
}
// Если не удалось восстановить, выбрасываем ошибку
throw new Error('Необходимо переподключить кошелек');
}
} catch (error) {
console.log('Session check error:', error);
throw error;
}
}
async function sendMessage() {
if (!userInput.value.trim() || !isAuthenticated.value) return;
const currentMessage = userInput.value.trim();
userInput.value = '';
// Добавляем сообщение пользователя в чат
messages.value.push({
role: 'user',
content: currentMessage
});
// Прокручиваем чат вниз
scrollToBottom();
try {
console.log('Отправка сообщения в Ollama:', currentMessage);
// Добавляем индикатор загрузки
messages.value.push({
role: 'system',
content: 'Загрузка ответа...'
});
// Прокручиваем чат вниз
scrollToBottom();
// Отправляем запрос к серверу
const response = await fetch('/api/chat/ollama', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: currentMessage,
model: 'mistral' // Указываем модель Mistral
}),
credentials: 'include'
});
// Удаляем индикатор загрузки
messages.value.pop();
// Проверяем статус ответа
if (!response.ok) {
let errorMessage = 'Ошибка при отправке сообщения';
try {
const errorData = await response.json();
errorMessage = errorData.error || errorMessage;
} catch (jsonError) {
console.error('Ошибка при парсинге JSON ответа об ошибке:', jsonError);
}
throw new Error(errorMessage);
}
const data = await response.json();
console.log('Ответ от сервера:', data);
// Добавляем ответ от сервера в чат
messages.value.push({
role: 'assistant',
content: data.response
});
// Прокручиваем чат вниз
scrollToBottom();
} catch (error) {
console.error('Error details:', error);
// Добавляем сообщение об ошибке в чат
messages.value.push({
role: 'system',
content: `Ошибка: ${error.message}`
});
// Прокручиваем чат вниз
scrollToBottom();
}
}
function scrollToBottom() {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
}
onMounted(async () => {
// Проверяем сессию
await checkAndRefreshSession();
// Загружаем историю сообщений
// ...
});
</script>
<style scoped>
.ai-chat {
display: flex;
flex-direction: column;
height: 100%;
max-width: 800px;
margin: 0 auto;
padding: 1rem;
}
.connect-wallet-message {
background-color: #fff3e0;
color: #e65100;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
text-align: center;
font-weight: 500;
border: 1px solid #ffe0b2;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 1rem;
margin-bottom: 1rem;
border: 1px solid #ddd;
border-radius: 8px;
background-color: white;
}
.message {
margin-bottom: 1rem;
padding: 0.5rem 1rem;
border-radius: 8px;
}
.message.user {
background-color: #e3f2fd;
margin-left: 2rem;
}
.message.assistant {
background-color: #f5f5f5;
margin-right: 2rem;
}
.message.system {
background-color: #ffebee;
text-align: center;
}
.chat-input {
display: flex;
gap: 1rem;
margin-top: 1rem;
}
textarea.disabled {
background-color: #f5f5f5;
border-color: #ddd;
color: #999;
cursor: not-allowed;
}
textarea.disabled::placeholder {
color: #999;
}
button.disabled {
background-color: #e0e0e0;
color: #999;
cursor: not-allowed;
}
textarea {
flex: 1;
min-height: 60px;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
resize: vertical;
transition: all 0.3s ease;
}
button {
padding: 0.5rem 1rem;
background-color: #1976d2;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
min-width: 120px;
}
button:not(.disabled):hover {
background-color: #1565c0;
}
</style>

View File

@@ -2,30 +2,28 @@
<div class="linked-accounts">
<h2>Связанные аккаунты</h2>
<div v-if="loading" class="loading">
Загрузка...
</div>
<div v-if="loading" class="loading">Загрузка...</div>
<div v-else-if="error" class="error">
{{ error }}
</div>
<div v-else>
<div v-if="identities.length === 0" class="no-accounts">
У вас нет связанных аккаунтов.
</div>
<div v-if="identities.length === 0" class="no-accounts">У вас нет связанных аккаунтов.</div>
<div v-else class="accounts-list">
<div v-for="identity in identities" :key="`${identity.identity_type}-${identity.identity_value}`" class="account-item">
<div
v-for="identity in identities"
:key="`${identity.identity_type}-${identity.identity_value}`"
class="account-item"
>
<div class="account-type">
{{ getIdentityTypeLabel(identity.identity_type) }}
</div>
<div class="account-value">
{{ formatIdentityValue(identity) }}
</div>
<button @click="unlinkAccount(identity)" class="unlink-button">
Отвязать
</button>
<button @click="unlinkAccount(identity)" class="unlink-button">Отвязать</button>
</div>
</div>
@@ -48,70 +46,71 @@
</div>
</template>
<script>
<script setup>
import { ref, onMounted, computed } from 'vue';
import { useAuthStore } from '../stores/auth';
import axios from 'axios';
export default {
name: 'LinkedAccounts',
const authStore = useAuthStore();
const identities = ref([]);
const loading = ref(true);
const error = ref(null);
data() {
return {
loading: true,
error: null,
identities: [],
userAddress: ''
};
},
const userAddress = computed(() => authStore.address);
async mounted() {
this.userAddress = this.$store.state.auth.address;
await this.loadIdentities();
},
methods: {
async loadIdentities() {
// Получение связанных аккаунтов
async function fetchLinkedAccounts() {
try {
this.loading = true;
this.error = null;
loading.value = true;
error.value = null;
const response = await axios.get('/api/identities', {
withCredentials: true
const response = await axios.get(`${import.meta.env.VITE_API_URL}/api/identities/linked`, {
withCredentials: true,
});
this.identities = response.data.identities;
} catch (error) {
console.error('Error loading identities:', error);
this.error = 'Не удалось загрузить связанные аккаунты. Попробуйте позже.';
identities.value = response.data;
} catch (err) {
console.error('Ошибка при получении связанных аккаунтов:', err);
error.value = 'Не удалось загрузить связанные аккаунты. Попробуйте позже.';
} finally {
this.loading = false;
loading.value = false;
}
},
}
async unlinkAccount(identity) {
// Отвязывание аккаунта
async function unlinkAccount(identity) {
try {
await axios.delete(`/api/identities/${identity.identity_type}/${identity.identity_value}`, {
withCredentials: true
});
// Обновляем список идентификаторов
await this.loadIdentities();
} catch (error) {
console.error('Error unlinking account:', error);
alert('Не удалось отвязать аккаунт. Попробуйте позже.');
}
await axios.post(
`${import.meta.env.VITE_API_URL}/api/identities/unlink`,
{
type: identity.identity_type,
value: identity.identity_value,
},
{
withCredentials: true,
}
);
getIdentityTypeLabel(type) {
// Обновляем список после отвязки
await fetchLinkedAccounts();
} catch (err) {
console.error('Ошибка при отвязке аккаунта:', err);
error.value = 'Не удалось отвязать аккаунт. Попробуйте позже.';
}
}
// Форматирование типа идентификатора
function getIdentityTypeLabel(type) {
const labels = {
ethereum: 'Ethereum',
telegram: 'Telegram',
email: 'Email'
email: 'Email',
};
return labels[type] || type;
},
}
formatIdentityValue(identity) {
// Форматирование значения идентификатора
function formatIdentityValue(identity) {
if (identity.identity_type === 'ethereum') {
// Сокращаем Ethereum-адрес
const value = identity.identity_value;
@@ -119,9 +118,13 @@ export default {
}
return identity.identity_value;
}
onMounted(() => {
if (authStore.isAuthenticated) {
fetchLinkedAccounts();
}
}
};
});
</script>
<style scoped>
@@ -131,7 +134,9 @@ export default {
padding: 20px;
}
.loading, .error, .no-accounts {
.loading,
.error,
.no-accounts {
margin: 20px 0;
padding: 10px;
text-align: center;

View File

@@ -15,11 +15,27 @@
</div>
</template>
<script>
export default {
name: 'Modal',
emits: ['close']
<script setup>
import { onMounted, onBeforeUnmount } from 'vue';
// Закрытие модального окна по нажатию Escape
function handleKeyDown(e) {
if (e.key === 'Escape') {
emit('close');
}
}
const emit = defineEmits(['close']);
onMounted(() => {
document.addEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'hidden'; // Блокируем прокрутку страницы
});
onBeforeUnmount(() => {
document.removeEventListener('keydown', handleKeyDown);
document.body.style.overflow = ''; // Восстанавливаем прокрутку страницы
});
</script>
<style scoped>
@@ -43,17 +59,21 @@ export default {
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.modal-header {
padding: 1rem;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
font-size: 1.1rem;
}
.modal-body {
padding: 1rem;
border-bottom: 1px solid #eee;
font-weight: bold;
font-size: 1.2rem;
}
.close-button {
@@ -65,11 +85,7 @@ export default {
}
.close-button:hover {
color: #000;
}
.modal-body {
padding: 1rem;
color: #333;
}
.modal-footer {

View File

@@ -0,0 +1,133 @@
<template>
<nav class="main-nav">
<div class="nav-brand">
<router-link to="/">DApp for Business</router-link>
</div>
<div class="nav-links">
<router-link to="/" class="nav-link">Главная</router-link>
<router-link to="/chat" class="nav-link">Чат</router-link>
<router-link v-if="authStore.isAdmin" to="/admin" class="nav-link admin-link">
Админ-панель
</router-link>
</div>
<div class="nav-auth">
<template v-if="authStore.isAuthenticated">
<div class="user-info">
<span class="user-address">{{ formatAddress(authStore.address) }}</span>
<span v-if="authStore.isAdmin" class="admin-badge">Админ</span>
</div>
<button @click="logout" class="btn-logout">Выйти</button>
</template>
<template v-else>
<wallet-connection />
</template>
</div>
</nav>
</template>
<script setup>
import { useRouter } from 'vue-router';
import { useAuthStore } from '../stores/auth';
import WalletConnection from './WalletConnection.vue';
const router = useRouter();
const authStore = useAuthStore();
// Форматирование адреса кошелька
function formatAddress(address) {
if (!address) return '';
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
}
// Выход из системы
async function logout() {
await authStore.logout();
router.push('/');
}
</script>
<style scoped>
.main-nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background-color: #fff;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.nav-brand a {
font-size: 1.25rem;
font-weight: 700;
color: #3498db;
text-decoration: none;
}
.nav-links {
display: flex;
gap: 1rem;
}
.nav-link {
color: #333;
text-decoration: none;
padding: 0.5rem;
border-radius: 4px;
}
.nav-link:hover {
background-color: #f0f0f0;
}
.nav-link.router-link-active {
color: #3498db;
font-weight: 500;
}
.admin-link {
color: #e74c3c;
}
.nav-auth {
display: flex;
align-items: center;
gap: 1rem;
}
.user-info {
display: flex;
align-items: center;
gap: 0.5rem;
}
.user-address {
font-family: monospace;
background-color: #f0f0f0;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.admin-badge {
background-color: #e74c3c;
color: white;
padding: 0.1rem 0.3rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
}
.btn-logout {
padding: 0.5rem 1rem;
border-radius: 4px;
background-color: #f0f0f0;
color: #333;
border: none;
cursor: pointer;
}
.btn-logout:hover {
background-color: #e0e0e0;
}
</style>

View File

@@ -0,0 +1,161 @@
<template>
<div class="role-manager">
<h2>Управление ролями пользователей</h2>
<div v-if="loading" class="loading">Загрузка...</div>
<div v-else-if="error" class="error">
{{ error }}
</div>
<div v-else>
<div class="current-role">
<h3>Ваша роль: {{ currentRole }}</h3>
<button @click="checkRole" :disabled="checkingRole">
{{ checkingRole ? 'Проверка...' : 'Обновить роль' }}
</button>
</div>
<div v-if="isAdmin" class="admin-section">
<h3>Пользователи системы</h3>
<table class="users-table">
<thead>
<tr>
<th>ID</th>
<th>Имя пользователя</th>
<th>Роль</th>
<th>Язык</th>
<th>Дата регистрации</th>
<th>Последняя проверка токенов</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.username || 'Не указано' }}</td>
<td>{{ user.role || 'user' }}</td>
<td>{{ user.preferred_language || 'ru' }}</td>
<td>{{ formatDate(user.created_at) }}</td>
<td>{{ user.last_token_check ? formatDate(user.last_token_check) : 'Не проверялся' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
import { ref, onMounted, computed } from 'vue';
import axios from 'axios';
export default {
setup() {
const loading = ref(false);
const error = ref(null);
const users = ref([]);
const currentRole = ref('user');
const isAdmin = ref(false);
const checkingRole = ref(false);
// Загрузка пользователей с ролями
const loadUsers = async () => {
try {
loading.value = true;
const response = await axios.get('/api/roles/users');
users.value = response.data;
} catch (err) {
console.error('Error loading users:', err);
error.value = 'Ошибка при загрузке пользователей';
} finally {
loading.value = false;
}
};
// Проверка роли текущего пользователя
const checkRole = async () => {
try {
checkingRole.value = true;
const response = await axios.post('/api/roles/check-role');
isAdmin.value = response.data.isAdmin;
currentRole.value = isAdmin.value ? 'admin' : 'user';
// Если пользователь стал администратором, загрузим список пользователей
if (isAdmin.value) {
await loadUsers();
}
} catch (err) {
console.error('Error checking role:', err);
error.value = 'Ошибка при проверке роли';
} finally {
checkingRole.value = false;
}
};
// Форматирование даты
const formatDate = (dateString) => {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleString('ru-RU');
};
onMounted(async () => {
// Проверяем роль при загрузке компонента
await checkRole();
});
return {
loading,
error,
users,
currentRole,
isAdmin,
checkingRole,
checkRole,
formatDate
};
}
};
</script>
<style scoped>
.role-manager {
padding: 20px;
}
.loading, .error {
padding: 20px;
text-align: center;
}
.error {
color: red;
}
.current-role {
margin-bottom: 20px;
padding: 15px;
background-color: #f5f5f5;
border-radius: 5px;
}
.users-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.users-table th, .users-table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.users-table th {
background-color: #f2f2f2;
}
.admin-section {
margin-top: 30px;
}
</style>

View File

@@ -1,51 +1,98 @@
<template>
<button @click.stop.prevent="connect" class="auth-btn wallet-btn">
<span class="auth-icon">💼</span> Подключить кошелек
<div class="wallet-connection">
<div v-if="error" class="error-message">
{{ error }}
</div>
<button @click="connect" class="connect-button" :disabled="loading">
<div v-if="loading" class="spinner"></div>
{{ loading ? 'Подключение...' : 'Подключить кошелек' }}
</button>
</div>
</template>
<script setup>
import { connectWallet } from '../services/wallet';
import { ref } from 'vue';
import { connectWallet } 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('');
function connect() {
async function connect() {
console.log('Нажата кнопка "Подключить кошелек"');
connectWallet((errorMessage) => {
error.value = errorMessage;
console.error('Ошибка при подключении кошелька:', errorMessage);
});
if (loading.value) return;
loading.value = true;
error.value = '';
try {
const authResult = await connectWallet();
console.log('Результат подключения:', authResult);
if (authResult && authResult.authenticated) {
authStore.updateAuthState(authResult);
router.push({ name: 'home' });
} else {
error.value = 'Не удалось подключить кошелек';
}
} catch (error) {
console.error('Ошибка при подключении кошелька:', error);
error.value = error.message || 'Ошибка при подключении кошелька';
} finally {
loading.value = false;
}
}
</script>
<style scoped>
/* Стили для кнопки подключения кошелька */
.auth-btn {
display: flex;
align-items: center;
justify-content: flex-start;
padding: 0.75rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
border: none;
width: 100%;
font-weight: 500;
transition: opacity 0.2s;
.wallet-connection {
margin: 20px 0;
}
.auth-btn:hover {
opacity: 0.9;
}
.auth-icon {
margin-right: 0.75rem;
font-size: 1.2rem;
}
.wallet-btn {
.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;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>

View File

@@ -0,0 +1,225 @@
<template>
<div class="conversation-list">
<div class="list-header">
<h3>Диалоги</h3>
<button @click="createNewConversation" class="new-conversation-btn">
<span>+</span> Новый диалог
</button>
</div>
<div v-if="loading" class="loading">Загрузка диалогов...</div>
<div v-else-if="conversations.length === 0" class="empty-list">
<p>У вас пока нет диалогов.</p>
<p>Создайте новый диалог, чтобы начать общение с ИИ-ассистентом.</p>
</div>
<div v-else class="conversations">
<div
v-for="conversation in conversations"
:key="conversation.conversation_id"
:class="[
'conversation-item',
{ active: selectedConversationId === conversation.conversation_id },
]"
@click="selectConversation(conversation.conversation_id)"
>
<div class="conversation-title">{{ conversation.title }}</div>
<div class="conversation-meta">
<span class="message-count">{{ conversation.message_count }} сообщений</span>
<span class="time">{{ formatTime(conversation.last_activity) }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed, defineEmits } from 'vue';
import { useAuthStore } from '../../stores/auth';
import axios from 'axios';
const emit = defineEmits(['select-conversation']);
const authStore = useAuthStore();
const conversations = ref([]);
const loading = ref(true);
const selectedConversationId = ref(null);
// Загрузка списка диалогов
const fetchConversations = async () => {
try {
loading.value = true;
const response = await axios.get('/api/messages/conversations');
conversations.value = response.data;
// Если есть диалоги и не выбран ни один, выбираем первый
if (conversations.value.length > 0 && !selectedConversationId.value) {
selectConversation(conversations.value[0].conversation_id);
}
} catch (error) {
console.error('Error fetching conversations:', error);
} finally {
loading.value = false;
}
};
// Выбор диалога
const selectConversation = (conversationId) => {
selectedConversationId.value = conversationId;
emit('select-conversation', conversationId);
};
// Создание нового диалога
const createNewConversation = async () => {
try {
const response = await axios.post('/api/messages/conversations', {
title: 'Новый диалог',
});
// Добавляем новый диалог в список
const newConversation = {
conversation_id: response.data.id,
title: response.data.title,
username: authStore.username,
address: authStore.address,
message_count: 0,
last_activity: response.data.created_at,
created_at: response.data.created_at,
};
conversations.value.unshift(newConversation);
// Выбираем новый диалог
selectConversation(newConversation.conversation_id);
} catch (error) {
console.error('Error creating conversation:', error);
}
};
// Форматирование времени
const formatTime = (timestamp) => {
if (!timestamp) return '';
const date = new Date(timestamp);
const now = new Date();
const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
// Сегодня - показываем только время
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} else if (diffDays === 1) {
// Вчера
return 'Вчера';
} else if (diffDays < 7) {
// В течение недели - показываем день недели
const days = ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'];
return days[date.getDay()];
} else {
// Более недели назад - показываем дату
return date.toLocaleDateString();
}
};
// Загрузка диалогов при монтировании компонента
onMounted(() => {
fetchConversations();
});
// Экспорт методов для использования в родительском компоненте
defineExpose({
fetchConversations,
});
</script>
<style scoped>
.conversation-list {
display: flex;
flex-direction: column;
width: 300px;
border-right: 1px solid #e0e0e0;
background-color: #f9f9f9;
height: 100%;
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #e0e0e0;
}
.list-header h3 {
margin: 0;
font-size: 1.2rem;
}
.new-conversation-btn {
display: flex;
align-items: center;
background-color: #4caf50;
color: white;
border: none;
border-radius: 4px;
padding: 0.5rem 0.75rem;
cursor: pointer;
font-size: 0.9rem;
}
.new-conversation-btn span {
font-size: 1.2rem;
margin-right: 0.25rem;
}
.loading,
.empty-list {
padding: 2rem;
text-align: center;
color: #666;
}
.empty-list p {
margin: 0.5rem 0;
}
.conversations {
flex: 1;
overflow-y: auto;
}
.conversation-item {
padding: 1rem;
border-bottom: 1px solid #e0e0e0;
cursor: pointer;
transition: background-color 0.2s;
}
.conversation-item:hover {
background-color: #f0f0f0;
}
.conversation-item.active {
background-color: #e8f5e9;
color: #4caf50;
}
.conversation-title {
font-weight: 500;
margin-bottom: 0.5rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.conversation-meta {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: #888;
}
.time {
font-size: 0.8rem;
}
</style>

View File

@@ -0,0 +1,134 @@
<template>
<div class="message-input">
<textarea
v-model="message"
placeholder="Введите сообщение..."
@keydown.enter.prevent="handleEnter"
ref="textareaRef"
:disabled="sending"
></textarea>
<button @click="sendMessage" class="send-button" :disabled="!message.trim() || sending">
<span v-if="sending">Отправка...</span>
<span v-else>Отправить</span>
</button>
</div>
</template>
<script setup>
import { ref, defineEmits, nextTick } from 'vue';
import axios from 'axios';
const props = defineProps({
conversationId: {
type: [Number, String],
required: true,
},
});
const emit = defineEmits(['message-sent']);
const message = ref('');
const sending = ref(false);
const textareaRef = ref(null);
// Обработка нажатия Enter
const handleEnter = (event) => {
// Если нажат Shift+Enter, добавляем перенос строки
if (event.shiftKey) {
return;
}
// Иначе отправляем сообщение
sendMessage();
};
// Отправка сообщения
const sendMessage = async () => {
if (!message.value.trim() || sending.value) return;
try {
sending.value = true;
const response = await axios.post(
`/api/messages/conversations/${props.conversationId}/messages`,
{ content: message.value }
);
// Очищаем поле ввода
message.value = '';
// Фокусируемся на поле ввода
nextTick(() => {
textareaRef.value.focus();
});
// Уведомляем родительский компонент о новых сообщениях
emit('message-sent', [response.data.userMessage, response.data.aiMessage]);
} catch (error) {
console.error('Error sending message:', error);
} finally {
sending.value = false;
}
};
// Сброс поля ввода
const resetInput = () => {
message.value = '';
};
// Экспорт методов для использования в родительском компоненте
defineExpose({
resetInput,
focus: () => textareaRef.value?.focus(),
});
</script>
<style scoped>
.message-input {
display: flex;
padding: 1rem;
border-top: 1px solid #e0e0e0;
background-color: #fff;
}
textarea {
flex: 1;
min-height: 40px;
max-height: 120px;
padding: 0.75rem;
border: 1px solid #e0e0e0;
border-radius: 4px;
resize: none;
font-family: inherit;
font-size: 0.9rem;
line-height: 1.4;
}
textarea:focus {
outline: none;
border-color: #4caf50;
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
}
.send-button {
margin-left: 0.5rem;
padding: 0 1rem;
height: 40px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s;
}
.send-button:hover:not(:disabled) {
background-color: #43a047;
}
.send-button:disabled {
background-color: #9e9e9e;
cursor: not-allowed;
}
</style>

View File

@@ -0,0 +1,194 @@
<template>
<div class="message-thread" ref="threadContainer">
<div v-if="loading" class="loading">Загрузка сообщений...</div>
<div v-else-if="messages.length === 0" class="empty-thread">
<p>Нет сообщений. Начните диалог, отправив сообщение.</p>
</div>
<div v-else class="messages">
<div v-for="message in messages" :key="message.id" :class="['message', message.sender_type]">
<div class="message-content">{{ message.content }}</div>
<div class="message-meta">
<span class="time">{{ formatTime(message.created_at) }}</span>
<span v-if="message.channel" class="channel">
{{ channelName(message.channel) }}
</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch, nextTick, defineExpose } from 'vue';
import axios from 'axios';
const props = defineProps({
conversationId: {
type: [Number, String],
required: true,
},
});
const messages = ref([]);
const loading = ref(true);
const threadContainer = ref(null);
// Загрузка сообщений диалога
const fetchMessages = async () => {
try {
loading.value = true;
const response = await axios.get(
`/api/messages/conversations/${props.conversationId}/messages`
);
messages.value = response.data;
// Прокрутка к последнему сообщению
await nextTick();
scrollToBottom();
} catch (error) {
console.error('Error fetching messages:', error);
} finally {
loading.value = false;
}
};
// Добавление новых сообщений
const addMessages = (newMessages) => {
if (Array.isArray(newMessages)) {
messages.value = [...messages.value, ...newMessages];
} else {
messages.value.push(newMessages);
}
// Прокрутка к последнему сообщению
nextTick(() => {
scrollToBottom();
});
};
// Прокрутка к последнему сообщению
const scrollToBottom = () => {
if (threadContainer.value) {
threadContainer.value.scrollTop = threadContainer.value.scrollHeight;
}
};
// Форматирование времени
const formatTime = (timestamp) => {
if (!timestamp) return '';
const date = new Date(timestamp);
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
};
// Получение названия канала
const channelName = (channel) => {
const channels = {
web: 'Веб',
telegram: 'Telegram',
email: 'Email',
};
return channels[channel] || channel;
};
// Наблюдение за изменением ID диалога
watch(
() => props.conversationId,
(newId, oldId) => {
if (newId && newId !== oldId) {
fetchMessages();
}
}
);
// Загрузка сообщений при монтировании компонента
onMounted(() => {
if (props.conversationId) {
fetchMessages();
}
});
// Экспорт методов для использования в родительском компоненте
defineExpose({
fetchMessages,
addMessages,
});
</script>
<style scoped>
.message-thread {
flex: 1;
overflow-y: auto;
padding: 1rem;
display: flex;
flex-direction: column;
}
.loading,
.empty-thread {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: #666;
}
.messages {
display: flex;
flex-direction: column;
gap: 1rem;
}
.message {
max-width: 80%;
padding: 0.75rem 1rem;
border-radius: 8px;
position: relative;
}
.message.user {
align-self: flex-end;
background-color: #e3f2fd;
border: 1px solid #bbdefb;
}
.message.ai {
align-self: flex-start;
background-color: #f5f5f5;
border: 1px solid #e0e0e0;
}
.message.admin {
align-self: flex-start;
background-color: #fff3e0;
border: 1px dashed #ffb74d;
}
.message-content {
white-space: pre-wrap;
word-break: break-word;
}
.message-meta {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 0.25rem;
font-size: 0.7rem;
color: #999;
}
.time {
font-size: 0.7rem;
}
.channel {
font-size: 0.7rem;
padding: 0.1rem 0.3rem;
border-radius: 3px;
background-color: #f0f0f0;
}
</style>

View File

@@ -1,128 +0,0 @@
<template>
<form @submit.prevent="submitForm" class="add-board-form">
<div class="form-group">
<label for="title">Название доски</label>
<input
type="text"
id="title"
v-model="form.title"
class="form-control"
required
placeholder="Введите название доски"
>
</div>
<div class="form-group">
<label for="description">Описание</label>
<textarea
id="description"
v-model="form.description"
class="form-control"
rows="3"
placeholder="Введите описание доски"
></textarea>
</div>
<div class="form-group form-check">
<input
type="checkbox"
id="isPublic"
v-model="form.isPublic"
class="form-check-input"
>
<label for="isPublic" class="form-check-label">Публичная доска</label>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" @click="cancel">Отмена</button>
<button type="submit" class="btn btn-primary" :disabled="!form.title">Создать</button>
</div>
</form>
</template>
<script>
import { ref } from 'vue';
import axios from 'axios';
export default {
name: 'AddBoardForm',
emits: ['add-board', 'cancel'],
setup(props, { emit }) {
const form = ref({
title: '',
description: '',
isPublic: false
});
const submitForm = async () => {
try {
const response = await axios.post('/api/kanban/boards', {
title: form.value.title,
description: form.value.description,
isPublic: form.value.isPublic
});
emit('add-board', response.data);
form.value = { title: '', description: '', isPublic: false };
} catch (error) {
console.error('Error creating board:', error);
alert('Не удалось создать доску');
}
};
const cancel = () => {
emit('cancel');
};
return {
form,
submitForm,
cancel
};
}
}
</script>
<style scoped>
.add-board-form {
padding: 1rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-check {
display: flex;
align-items: center;
gap: 0.5rem;
}
.form-check-label {
font-weight: normal;
}
.form-check-input {
margin-top: 0;
}
.form-control {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 1rem;
}
</style>

View File

@@ -1,121 +0,0 @@
<template>
<form @submit.prevent="submitForm" class="add-card-form">
<div class="form-group">
<label for="title">Заголовок</label>
<input
type="text"
id="title"
v-model="form.title"
class="form-control"
required
placeholder="Введите заголовок карточки"
>
</div>
<div class="form-group">
<label for="description">Описание</label>
<textarea
id="description"
v-model="form.description"
class="form-control"
rows="3"
placeholder="Введите описание карточки"
></textarea>
</div>
<div class="form-group">
<label for="dueDate">Срок выполнения</label>
<input
type="date"
id="dueDate"
v-model="form.dueDate"
class="form-control"
>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" @click="cancel">Отмена</button>
<button type="submit" class="btn btn-primary" :disabled="!form.title">Создать</button>
</div>
</form>
</template>
<script>
import { ref } from 'vue';
import axios from 'axios';
export default {
name: 'AddCardForm',
props: {
columnId: {
type: Number,
required: true
}
},
emits: ['add-card', 'cancel'],
setup(props, { emit }) {
const form = ref({
title: '',
description: '',
dueDate: ''
});
const submitForm = async () => {
try {
const response = await axios.post('/api/kanban/cards', {
title: form.value.title,
description: form.value.description,
columnId: props.columnId,
dueDate: form.value.dueDate || null
});
emit('add-card', response.data);
form.value = { title: '', description: '', dueDate: '' };
} catch (error) {
console.error('Error creating card:', error);
alert('Не удалось создать карточку');
}
};
const cancel = () => {
emit('cancel');
};
return {
form,
submitForm,
cancel
};
}
}
</script>
<style scoped>
.add-card-form {
padding: 1rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-control {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 1rem;
}
</style>

View File

@@ -1,115 +0,0 @@
<template>
<form @submit.prevent="submitForm" class="add-column-form">
<div class="form-group">
<label for="title">Название колонки</label>
<input
type="text"
id="title"
v-model="form.title"
class="form-control"
required
placeholder="Введите название колонки"
>
</div>
<div class="form-group">
<label for="wipLimit">Лимит WIP (опционально)</label>
<input
type="number"
id="wipLimit"
v-model="form.wipLimit"
class="form-control"
min="1"
placeholder="Введите лимит задач в работе"
>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" @click="cancel">Отмена</button>
<button type="submit" class="btn btn-primary" :disabled="!form.title">Создать</button>
</div>
</form>
</template>
<script>
import { ref } from 'vue';
import axios from 'axios';
export default {
name: 'AddColumnForm',
props: {
boardId: {
type: [Number, String],
required: true
}
},
emits: ['add-column', 'cancel'],
setup(props, { emit }) {
const form = ref({
title: '',
wipLimit: null
});
const submitForm = async () => {
try {
const response = await axios.post(`/api/kanban/boards/${props.boardId}/columns`, {
title: form.value.title,
wipLimit: form.value.wipLimit || null
});
// Добавляем пустой массив карточек для отображения в UI
const columnWithCards = {
...response.data,
cards: []
};
emit('add-column', columnWithCards);
form.value = { title: '', wipLimit: null };
} catch (error) {
console.error('Error creating column:', error);
alert('Не удалось создать колонку');
}
};
const cancel = () => {
emit('cancel');
};
return {
form,
submitForm,
cancel
};
}
}
</script>
<style scoped>
.add-column-form {
padding: 1rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-control {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 1rem;
}
</style>

View File

@@ -1,529 +0,0 @@
<template>
<div class="kanban-board">
<div class="board-header">
<h1>{{ board.title }}</h1>
<p v-if="board.description">{{ board.description }}</p>
<div class="board-actions">
<button @click="showAddCardModal = true" class="btn btn-primary">
Добавить карточку
</button>
<button @click="showBoardSettings = true" class="btn btn-secondary">
Настройки доски
</button>
</div>
</div>
<div class="columns-container">
<div
v-for="column in board.columns"
:key="column.id"
class="kanban-column"
:class="{ 'wip-limit-reached': column.wip_limit && column.cards.length >= column.wip_limit }"
>
<div class="column-header">
<h3>{{ column.title }}</h3>
<span v-if="column.wip_limit" class="wip-limit">
{{ column.cards.length }}/{{ column.wip_limit }}
</span>
<div class="column-actions">
<button @click="showAddCardModal = true; selectedColumn = column" class="btn-icon">
<i class="fas fa-plus"></i>
</button>
<button @click="showColumnSettings = true; selectedColumn = column" class="btn-icon">
<i class="fas fa-cog"></i>
</button>
</div>
</div>
<div class="cards-container">
<draggable
v-model="column.cards"
group="cards"
@change="onCardMove"
:animation="150"
ghost-class="ghost-card"
class="column-cards"
>
<kanban-card
v-for="card in column.cards"
:key="card.id"
:card="card"
@click="openCard(card)"
/>
</draggable>
</div>
</div>
<div class="add-column">
<button @click="showAddColumnModal = true" class="btn btn-outline">
<i class="fas fa-plus"></i> Добавить колонку
</button>
</div>
</div>
<!-- Модальные окна -->
<modal v-if="showAddCardModal" @close="showAddCardModal = false">
<template #header>Добавить карточку</template>
<template #body>
<add-card-form
:column-id="selectedColumn ? selectedColumn.id : null"
@add-card="addCard"
@cancel="showAddCardModal = false"
/>
</template>
</modal>
<modal v-if="showAddColumnModal" @close="showAddColumnModal = false">
<template #header>Добавить колонку</template>
<template #body>
<add-column-form
:board-id="board.id"
@add-column="addColumn"
@cancel="showAddColumnModal = false"
/>
</template>
</modal>
<modal v-if="showBoardSettings" @close="showBoardSettings = false">
<template #header>Настройки доски</template>
<template #body>
<board-settings-form
:board="board"
@update-board="updateBoard"
@cancel="showBoardSettings = false"
/>
</template>
</modal>
<modal v-if="showColumnSettings" @close="showColumnSettings = false">
<template #header>Настройки колонки</template>
<template #body>
<column-settings-form
:column="selectedColumn"
@update-column="updateColumn"
@delete-column="deleteColumn"
@cancel="showColumnSettings = false"
/>
</template>
</modal>
<card-detail-modal
v-if="selectedCard"
:card="selectedCard"
:column="getColumnForCard(selectedCard)"
@close="selectedCard = null"
@update-card="updateCard"
@delete-card="deleteCard"
/>
</div>
</template>
<script>
import { ref, reactive, onMounted } from 'vue';
import axios from 'axios';
import draggable from 'vuedraggable';
import KanbanCard from './KanbanCard.vue';
import Modal from '../ui/Modal.vue';
import AddCardForm from './AddCardForm.vue';
import AddColumnForm from './AddColumnForm.vue';
import BoardSettingsForm from './BoardSettingsForm.vue';
import ColumnSettingsForm from './ColumnSettingsForm.vue';
import CardDetailModal from './CardDetailModal.vue';
export default {
name: 'KanbanBoard',
components: {
draggable,
KanbanCard,
Modal,
AddCardForm,
AddColumnForm,
BoardSettingsForm,
ColumnSettingsForm,
CardDetailModal
},
props: {
boardId: {
type: String,
required: true
}
},
setup(props) {
const board = reactive({
id: null,
title: '',
description: '',
columns: []
});
const loading = ref(false);
const error = ref(null);
const showAddCardModal = ref(false);
const showAddColumnModal = ref(false);
const showBoardSettings = ref(false);
const showColumnSettings = ref(false);
const selectedColumn = ref(null);
const selectedCard = ref(null);
// Загрузка данных доски
const loadBoard = async () => {
try {
loading.value = true;
error.value = null;
const response = await axios.get(`/api/kanban/boards/${props.boardId}`, {
withCredentials: true
});
// Обновляем реактивный объект board
Object.assign(board, response.data);
} catch (err) {
console.error('Error loading board:', err);
error.value = 'Не удалось загрузить доску. Попробуйте позже.';
} finally {
loading.value = false;
}
};
// Обработка перемещения карточки
const onCardMove = async (event) => {
try {
if (event.added) {
const { element: card, newIndex } = event.added;
// Обновляем позицию и колонку карточки в БД
await axios.put(`/api/kanban/cards/${card.id}/move`, {
columnId: selectedColumn.value.id,
position: newIndex
}, {
withCredentials: true
});
}
} catch (err) {
console.error('Error moving card:', err);
// В случае ошибки перезагружаем доску
await loadBoard();
}
};
// Добавление новой карточки
const addCard = async (cardData) => {
try {
const response = await axios.post('/api/kanban/cards', cardData, {
withCredentials: true
});
// Находим колонку и добавляем в нее новую карточку
const column = board.columns.find(col => col.id === cardData.columnId);
if (column) {
column.cards.push(response.data);
}
showAddCardModal.value = false;
} catch (err) {
console.error('Error adding card:', err);
error.value = 'Не удалось добавить карточку. Попробуйте позже.';
}
};
// Добавление новой колонки
const addColumn = async (columnData) => {
try {
const response = await axios.post('/api/kanban/columns', {
...columnData,
boardId: board.id
}, {
withCredentials: true
});
// Добавляем новую колонку в список
board.columns.push({
...response.data,
cards: []
});
showAddColumnModal.value = false;
} catch (err) {
console.error('Error adding column:', err);
error.value = 'Не удалось добавить колонку. Попробуйте позже.';
}
};
// Обновление настроек доски
const updateBoard = async (boardData) => {
try {
const response = await axios.put(`/api/kanban/boards/${board.id}`, boardData, {
withCredentials: true
});
// Обновляем данные доски
Object.assign(board, response.data);
showBoardSettings.value = false;
} catch (err) {
console.error('Error updating board:', err);
error.value = 'Не удалось обновить настройки доски. Попробуйте позже.';
}
};
// Обновление настроек колонки
const updateColumn = async (columnData) => {
try {
const response = await axios.put(`/api/kanban/columns/${columnData.id}`, columnData, {
withCredentials: true
});
// Находим и обновляем колонку
const columnIndex = board.columns.findIndex(col => col.id === columnData.id);
if (columnIndex !== -1) {
board.columns[columnIndex] = {
...board.columns[columnIndex],
...response.data
};
}
showColumnSettings.value = false;
} catch (err) {
console.error('Error updating column:', err);
error.value = 'Не удалось обновить настройки колонки. Попробуйте позже.';
}
};
// Удаление колонки
const deleteColumn = async (columnId) => {
try {
await axios.delete(`/api/kanban/columns/${columnId}`, {
withCredentials: true
});
// Удаляем колонку из списка
const columnIndex = board.columns.findIndex(col => col.id === columnId);
if (columnIndex !== -1) {
board.columns.splice(columnIndex, 1);
}
showColumnSettings.value = false;
} catch (err) {
console.error('Error deleting column:', err);
error.value = 'Не удалось удалить колонку. Попробуйте позже.';
}
};
// Открытие карточки для просмотра/редактирования
const openCard = (card) => {
selectedCard.value = card;
};
// Получение колонки для карточки
const getColumnForCard = (card) => {
return board.columns.find(col => col.id === card.column_id);
};
// Обновление карточки
const updateCard = async (cardData) => {
try {
const response = await axios.put(`/api/kanban/cards/${cardData.id}`, cardData, {
withCredentials: true
});
// Находим и обновляем карточку
for (const column of board.columns) {
const cardIndex = column.cards.findIndex(c => c.id === cardData.id);
if (cardIndex !== -1) {
column.cards[cardIndex] = {
...column.cards[cardIndex],
...response.data
};
break;
}
}
// Если открыта эта карточка, обновляем ее
if (selectedCard.value && selectedCard.value.id === cardData.id) {
selectedCard.value = {
...selectedCard.value,
...response.data
};
}
} catch (err) {
console.error('Error updating card:', err);
error.value = 'Не удалось обновить карточку. Попробуйте позже.';
}
};
// Удаление карточки
const deleteCard = async (cardId) => {
try {
await axios.delete(`/api/kanban/cards/${cardId}`, {
withCredentials: true
});
// Удаляем карточку из списка
for (const column of board.columns) {
const cardIndex = column.cards.findIndex(c => c.id === cardId);
if (cardIndex !== -1) {
column.cards.splice(cardIndex, 1);
break;
}
}
selectedCard.value = null;
} catch (err) {
console.error('Error deleting card:', err);
error.value = 'Не удалось удалить карточку. Попробуйте позже.';
}
};
onMounted(() => {
loadBoard();
});
return {
board,
loading,
error,
showAddCardModal,
showAddColumnModal,
showBoardSettings,
showColumnSettings,
selectedColumn,
selectedCard,
onCardMove,
addCard,
addColumn,
updateBoard,
updateColumn,
deleteColumn,
openCard,
getColumnForCard,
updateCard,
deleteCard
};
}
};
</script>
<style scoped>
.kanban-board {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.board-header {
padding: 1rem;
background-color: #f5f5f5;
border-bottom: 1px solid #ddd;
}
.board-actions {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
}
.columns-container {
display: flex;
overflow-x: auto;
padding: 1rem;
height: calc(100vh - 150px);
}
.kanban-column {
min-width: 280px;
max-width: 280px;
margin-right: 1rem;
background-color: #f0f0f0;
border-radius: 4px;
display: flex;
flex-direction: column;
max-height: 100%;
}
.column-header {
padding: 0.75rem;
background-color: #e0e0e0;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
.column-actions {
display: flex;
gap: 0.25rem;
}
.cards-container {
padding: 0.5rem;
overflow-y: auto;
flex-grow: 1;
}
.column-cards {
min-height: 10px;
}
.wip-limit {
font-size: 0.8rem;
color: #666;
margin-left: 0.5rem;
}
.wip-limit-reached .wip-limit {
color: #e74c3c;
font-weight: bold;
}
.add-column {
min-width: 280px;
display: flex;
align-items: flex-start;
padding: 0.5rem;
}
.ghost-card {
opacity: 0.5;
background: #c8ebfb;
}
.btn {
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
border: none;
}
.btn-primary {
background-color: #3498db;
color: white;
}
.btn-secondary {
background-color: #95a5a6;
color: white;
}
.btn-outline {
background-color: transparent;
border: 1px dashed #95a5a6;
color: #666;
}
.btn-icon {
background: none;
border: none;
cursor: pointer;
font-size: 0.9rem;
color: #666;
padding: 0.25rem;
}
.btn-icon:hover {
color: #333;
}
</style>

View File

@@ -1,190 +0,0 @@
<template>
<div class="kanban-card" :class="{ 'has-due-date': hasDueDate, 'overdue': isOverdue }">
<div class="card-labels" v-if="card.labels && card.labels.length > 0">
<span
v-for="label in card.labels"
:key="label.id"
class="card-label"
:style="{ backgroundColor: label.color }"
:title="label.name"
></span>
</div>
<div class="card-title">{{ card.title }}</div>
<div class="card-footer">
<div class="card-due-date" v-if="hasDueDate" :title="formattedDueDate">
<i class="far fa-clock"></i> {{ dueDateDisplay }}
</div>
<div class="card-assignee" v-if="card.assigned_username">
<span class="avatar" :title="card.assigned_username">
{{ getInitials(card.assigned_username) }}
</span>
</div>
</div>
</div>
</template>
<script>
import { computed } from 'vue';
export default {
name: 'KanbanCard',
props: {
card: {
type: Object,
required: true
}
},
setup(props) {
// Проверяем, есть ли срок выполнения
const hasDueDate = computed(() => {
return !!props.card.due_date;
});
// Проверяем, просрочена ли задача
const isOverdue = computed(() => {
if (!props.card.due_date) return false;
const dueDate = new Date(props.card.due_date);
const now = new Date();
return dueDate < now;
});
// Форматируем дату для отображения
const formattedDueDate = computed(() => {
if (!props.card.due_date) return '';
const dueDate = new Date(props.card.due_date);
return dueDate.toLocaleString('ru-RU', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
});
// Сокращенное отображение даты
const dueDateDisplay = computed(() => {
if (!props.card.due_date) return '';
const dueDate = new Date(props.card.due_date);
const now = new Date();
// Если сегодня
if (dueDate.toDateString() === now.toDateString()) {
return 'Сегодня ' + dueDate.toLocaleTimeString('ru-RU', {
hour: '2-digit',
minute: '2-digit'
});
}
// Если завтра
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
if (dueDate.toDateString() === tomorrow.toDateString()) {
return 'Завтра ' + dueDate.toLocaleTimeString('ru-RU', {
hour: '2-digit',
minute: '2-digit'
});
}
// В остальных случаях
return dueDate.toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'short'
});
});
// Получаем инициалы пользователя
const getInitials = (name) => {
if (!name) return '';
return name
.split(' ')
.map(part => part.charAt(0).toUpperCase())
.join('')
.substring(0, 2);
};
return {
hasDueDate,
isOverdue,
formattedDueDate,
dueDateDisplay,
getInitials
};
}
}
</script>
<style scoped>
.kanban-card {
background-color: white;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
padding: 0.75rem;
margin-bottom: 0.5rem;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.kanban-card:hover {
transform: translateY(-2px);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
}
.card-labels {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 0.5rem;
}
.card-label {
width: 32px;
height: 8px;
border-radius: 4px;
}
.card-title {
font-weight: 500;
margin-bottom: 0.5rem;
word-break: break-word;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.8rem;
color: #666;
}
.card-due-date {
display: flex;
align-items: center;
gap: 4px;
}
.has-due-date.overdue .card-due-date {
color: #e74c3c;
font-weight: bold;
}
.avatar {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background-color: #3498db;
color: white;
border-radius: 50%;
font-size: 0.7rem;
font-weight: bold;
}
</style>

View File

@@ -1,151 +0,0 @@
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { ethers } from 'ethers'
export function useEthereum() {
const address = ref(null)
const isConnected = ref(false)
const chainId = ref(null)
const provider = ref(null)
const signer = ref(null)
// Инициализация при загрузке компонента
onMounted(async () => {
// Проверяем, есть ли сохраненное состояние подключения
const savedAddress = localStorage.getItem('walletAddress')
const savedConnected = localStorage.getItem('isConnected') === 'true'
if (savedConnected && savedAddress) {
// Пробуем восстановить подключение
try {
await connect()
} catch (error) {
console.error('Failed to restore connection:', error)
// Очищаем сохраненное состояние при ошибке
localStorage.removeItem('walletAddress')
localStorage.removeItem('isConnected')
}
}
})
// Функция для подключения кошелька
async function connect() {
try {
// Проверяем, доступен ли MetaMask
if (typeof window.ethereum === 'undefined') {
throw new Error('MetaMask не установлен')
}
// Запрашиваем доступ к аккаунтам
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' })
if (accounts.length === 0) {
throw new Error('Нет доступных аккаунтов')
}
// Устанавливаем адрес
address.value = accounts[0]
try {
// Создаем провайдер и signer (для ethers v5)
provider.value = new ethers.providers.Web3Provider(window.ethereum)
signer.value = provider.value.getSigner()
// Получаем chainId напрямую из ethereum провайдера
const chainIdHex = await window.ethereum.request({ method: 'eth_chainId' })
chainId.value = parseInt(chainIdHex, 16).toString()
} catch (providerError) {
console.error('Ошибка при создании провайдера:', providerError)
// Продолжаем выполнение, даже если не удалось получить signer
}
isConnected.value = true
// Сохраняем информацию о подключении
localStorage.setItem('walletConnected', 'true')
localStorage.setItem('walletAddress', address.value)
return address.value
} catch (error) {
console.error('Connection error:', error)
throw error
}
}
// Функция для отключения кошелька
function disconnect() {
address.value = null
isConnected.value = false
provider.value = null
signer.value = null
chainId.value = null
// Очищаем localStorage
localStorage.removeItem('walletAddress')
localStorage.removeItem('isConnected')
}
// Функция для подписи сообщения
async function signMessage(message) {
if (!signer.value) {
throw new Error('Кошелек не подключен')
}
return await signer.value.signMessage(message)
}
// Обработчик изменения аккаунтов
async function handleAccountsChanged(accounts) {
if (accounts.length === 0) {
// Пользователь отключил аккаунт
address.value = null
isConnected.value = false
signer.value = null
} else {
// Пользователь сменил аккаунт
address.value = accounts[0]
isConnected.value = true
// Обновляем signer
if (provider.value) {
signer.value = provider.value.getSigner()
}
}
}
// Обработчик изменения сети
function handleChainChanged(chainIdHex) {
chainId.value = parseInt(chainIdHex, 16).toString()
window.location.reload()
}
// Инициализация при монтировании компонента
onMounted(() => {
// Проверяем, есть ли MetaMask
if (window.ethereum) {
// Добавляем слушатели событий
window.ethereum.on('accountsChanged', handleAccountsChanged)
window.ethereum.on('chainChanged', handleChainChanged)
window.ethereum.on('disconnect', disconnect)
}
})
// Очистка при размонтировании компонента
onUnmounted(() => {
if (window.ethereum) {
window.ethereum.removeListener('accountsChanged', handleAccountsChanged)
window.ethereum.removeListener('chainChanged', handleChainChanged)
window.ethereum.removeListener('disconnect', disconnect)
}
})
return {
address,
isConnected,
chainId,
provider,
signer,
connect,
disconnect,
signMessage
}
}

View File

@@ -0,0 +1,23 @@
import { ref } from 'vue';
export function useEthereum() {
const address = ref('');
const isConnected = ref(true);
async function connect() {
console.log('Имитация подключения к кошельку');
return { success: true };
}
async function getContract() {
console.log('Имитация получения контракта');
return {};
}
return {
address,
isConnected,
connect,
getContract,
};
}

View File

View File

View File

@@ -1,27 +1,22 @@
import { Buffer } from 'buffer'
globalThis.Buffer = Buffer
import { Buffer } from 'buffer';
globalThis.Buffer = Buffer;
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import axios from 'axios'
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';
import axios from 'axios';
// Вместо этого
axios.defaults.baseURL = import.meta.env.VITE_API_URL || 'http://localhost:8000'
// Используйте относительный путь
axios.defaults.baseURL = ''
// Включение передачи кук
axios.defaults.withCredentials = true
// Настройка axios
axios.defaults.baseURL = 'http://localhost:8000';
axios.defaults.withCredentials = true; // Важно для работы с сессиями
// Создаем и монтируем приложение Vue
const app = createApp(App)
const pinia = createPinia()
const app = createApp(App);
const pinia = createPinia();
app.use(pinia)
app.use(router)
app.use(pinia);
app.use(router);
// Не используем заглушки, так как сервер работает
// if (import.meta.env.DEV) {
@@ -33,4 +28,6 @@ app.use(router)
// ]).catch(err => console.error('Failed to load API mocks:', err));
// }
app.mount('#app')
console.log('API URL:', import.meta.env.VITE_API_URL);
app.mount('#app');

View File

@@ -1,69 +0,0 @@
// Заглушки для API в режиме разработки
export function setupMockServer() {
if (import.meta.env.DEV) {
// Перехватываем fetch запросы
const originalFetch = window.fetch;
window.fetch = async function(url, options) {
// Проверяем, является ли запрос API запросом
if (typeof url === 'string' && url.includes('/api/')) {
console.log('Перехвачен запрос:', url, options);
// Заглушка для сессии
if (url.includes('/api/session')) {
return new Response(JSON.stringify({
authenticated: true,
address: '0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b'
}), { status: 200, headers: { 'Content-Type': 'application/json' } });
}
// Заглушка для проверки доступности сервера
if (url.includes('/api/debug/ping')) {
return new Response(JSON.stringify({ status: 'ok' }),
{ status: 200, headers: { 'Content-Type': 'application/json' } });
}
// Заглушка для досок Канбан
if (url.includes('/api/kanban/boards')) {
return new Response(JSON.stringify({
ownBoards: [
{
id: 1,
title: 'Разработка DApp',
description: 'Задачи по разработке DApp',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
owner_address: '0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b',
is_public: true
},
{
id: 2,
title: 'Маркетинг',
description: 'Маркетинговые задачи',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
owner_address: '0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b',
is_public: false
}
],
sharedBoards: [],
publicBoards: []
}), { status: 200, headers: { 'Content-Type': 'application/json' } });
}
// Заглушка для чата с ИИ
if (url.includes('/api/chat/message') && options?.method === 'POST') {
const body = options.body ? JSON.parse(options.body) : {};
return new Response(JSON.stringify({
response: `Это тестовый ответ на ваше сообщение: "${body.message}". В данный момент сервер недоступен.`
}), { status: 200, headers: { 'Content-Type': 'application/json' } });
}
}
// Для всех остальных запросов используем оригинальный fetch
return originalFetch.apply(this, arguments);
};
console.log('Заглушки API настроены для режима разработки');
}
}

View File

@@ -1,68 +0,0 @@
// Импортируем axios для перехвата запросов
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
// Создаем экземпляр MockAdapter
const mock = new MockAdapter(axios, { delayResponse: 1000 });
// Мокаем запрос к API для получения nonce
mock.onGet(/\/api\/auth\/nonce/).reply((config) => {
const address = config.url.split('?address=')[1];
if (!address) {
return [400, { error: 'Address is required' }];
}
return [200, {
message: `Sign this message to authenticate with DApp for Business: ${Math.floor(Math.random() * 1000000)}`,
address
}];
});
// Мокаем запрос к API для верификации подписи
mock.onPost('/api/auth/verify').reply((config) => {
const { address } = JSON.parse(config.data);
// Проверяем, является ли адрес администратором (для тестирования)
const isAdmin = address.toLowerCase() === '0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b'.toLowerCase();
return [200, {
authenticated: true,
address,
isAdmin
}];
});
// Мокаем запрос к API для аутентификации по email
mock.onPost('/api/auth/email').reply((config) => {
const { email } = JSON.parse(config.data);
// Проверяем формат email
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return [400, { error: 'Invalid email format' }];
}
// Проверяем, является ли email администратором (для тестирования)
const isAdmin = email.toLowerCase() === 'admin@example.com';
return [200, {
authenticated: true,
address: email,
isAdmin
}];
});
// Мокаем запрос к API для проверки сессии
mock.onGet('/api/auth/check').reply(200, {
authenticated: false,
address: null,
isAdmin: false
});
// Мокаем запрос к API для выхода
mock.onPost('/api/auth/logout').reply(200, {
success: true
});
export default mock;

View File

@@ -1,40 +0,0 @@
// Импортируем axios для перехвата запросов
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
// Создаем экземпляр MockAdapter
const mock = new MockAdapter(axios, { delayResponse: 1000 });
// Мокаем запрос к API чата
mock.onPost('/api/chat/message').reply((config) => {
const { message, userId, language } = JSON.parse(config.data);
// Определяем язык ответа
const isRussian = language === 'ru';
// Простые ответы на разных языках
const responses = {
ru: [
'Я могу помочь вам с различными задачами. Что именно вас интересует?',
'Для полноценной работы рекомендую авторизоваться в системе.',
'Это интересный вопрос! Давайте разберемся подробнее.',
'Я ИИ-ассистент DApp for Business. Чем могу помочь?',
'Для доступа к расширенным функциям необходимо подключить кошелек или авторизоваться другим способом.'
],
en: [
'I can help you with various tasks. What are you interested in?',
'For full functionality, I recommend logging into the system.',
'That\'s an interesting question! Let\'s explore it in more detail.',
'I\'m the AI assistant for DApp for Business. How can I help?',
'To access advanced features, you need to connect your wallet or authorize in another way.'
]
};
// Выбираем случайный ответ из соответствующего языка
const randomIndex = Math.floor(Math.random() * responses[isRussian ? 'ru' : 'en'].length);
const reply = responses[isRussian ? 'ru' : 'en'][randomIndex];
return [200, { reply }];
});
export default mock;

Some files were not shown because too many files have changed in this diff Show More