Тестовый коммит после удаления husky
This commit is contained in:
16
.cursor/settings.json
Normal file
16
.cursor/settings.json
Normal 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
7
backend/.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 100
|
||||
}
|
||||
213
backend/app.js
213
backend/app.js
@@ -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'],
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'OPTIONS', 'DELETE', 'PUT', 'HEAD'],
|
||||
allowedHeaders: [
|
||||
'Content-Type',
|
||||
'X-Wallet-Address',
|
||||
'X-Wallet-Signature',
|
||||
'Cookie',
|
||||
'Authorization'
|
||||
],
|
||||
exposedHeaders: ['Set-Cookie']
|
||||
}));
|
||||
// Настройка 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', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'X-Auth-Nonce'],
|
||||
})
|
||||
);
|
||||
|
||||
// Настройка сессий
|
||||
app.use(session({
|
||||
store: new pgSession({
|
||||
pool: pool,
|
||||
tableName: 'session',
|
||||
createTableIfMissing: true
|
||||
}),
|
||||
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
cookie: {
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
sameSite: 'lax',
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 часа
|
||||
}
|
||||
}));
|
||||
app.use(
|
||||
session({
|
||||
store: new pgSession({
|
||||
pool: pool,
|
||||
tableName: 'sessions',
|
||||
createTableIfMissing: false,
|
||||
}),
|
||||
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
name: 'dapp.sid',
|
||||
cookie: {
|
||||
secure: false,
|
||||
httpOnly: true,
|
||||
maxAge: 30 * 24 * 60 * 60 * 1000,
|
||||
sameSite: 'lax',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// 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 };
|
||||
@@ -1,16 +0,0 @@
|
||||
# Пример документа для RAG
|
||||
|
||||
Это пример документа, который будет использоваться в RAG системе.
|
||||
|
||||
## Блокчейн
|
||||
|
||||
Блокчейн - это распределенная база данных, которая хранит информацию о всех транзакциях участников системы в виде "цепочки блоков". Каждый блок содержит набор транзакций и ссылку на предыдущий блок.
|
||||
|
||||
## Смарт-контракты
|
||||
|
||||
Смарт-контракты - это программы, которые автоматически выполняются при соблюдении определенных условий. Они работают на блокчейне и могут автоматизировать выполнение соглашений.
|
||||
|
||||
## Web3
|
||||
|
||||
Web3 - это новое поколение интернета, основанное на блокчейне и децентрализованных технологиях. Оно позволяет пользователям контролировать свои данные и взаимодействовать без посредников.
|
||||
|
||||
@@ -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
9
backend/docs/api.md
Normal 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
30
backend/eslint.config.js
Normal 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',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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"}
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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: 'Внутренняя ошибка сервера' });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
requireRole
|
||||
// Проверка роли пользователя
|
||||
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,
|
||||
requireAuth,
|
||||
requireAdmin,
|
||||
checkRole,
|
||||
};
|
||||
12
backend/middleware/logger.js
Normal file
12
backend/middleware/logger.js
Normal 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;
|
||||
22
backend/middleware/session.js
Normal file
22
backend/middleware/session.js
Normal 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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
26
backend/migrations/006_role_management.sql
Normal file
26
backend/migrations/006_role_management.sql
Normal 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);
|
||||
40
backend/migrations/007_user_identities_conversations.sql
Normal file
40
backend/migrations/007_user_identities_conversations.sql
Normal 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;
|
||||
|
||||
14
backend/migrations/008_chat_history.sql
Normal file
14
backend/migrations/008_chat_history.sql
Normal 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);
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 => ({
|
||||
id: token.id,
|
||||
walletAddress: token.wallet_address,
|
||||
role: token.role,
|
||||
createdAt: token.created_at,
|
||||
expiresAt: token.expires_at
|
||||
})));
|
||||
res.json(
|
||||
result.rows.map((token) => ({
|
||||
id: token.id,
|
||||
walletAddress: token.wallet_address,
|
||||
role: token.role,
|
||||
createdAt: token.created_at,
|
||||
expiresAt: token.expires_at,
|
||||
}))
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Tokens list error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -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
123
backend/routes/admin.js
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
// Явно сохраняем сессию
|
||||
req.session.save((err) => {
|
||||
if (err) {
|
||||
// Удалите или закомментируйте эти логи
|
||||
// console.error('Error saving session:', err);
|
||||
return res.status(500).json({ error: 'Failed to save session' });
|
||||
}
|
||||
|
||||
// Удалите или закомментируйте
|
||||
// console.log('Nonce saved in session:', {
|
||||
// nonce,
|
||||
// pendingAddress: address,
|
||||
// sessionID: req.sessionID
|
||||
// });
|
||||
|
||||
res.json({ message });
|
||||
// Сохраняем сессию и ждем завершения
|
||||
await new Promise((resolve, reject) => {
|
||||
req.session.save((err) => {
|
||||
if (err) {
|
||||
console.error('Ошибка при сохранении сессии:', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Проверяем, что 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
|
||||
if (pendingAddress.toLowerCase() !== address.toLowerCase()) {
|
||||
return res.status(400).json({ error: 'Address mismatch' });
|
||||
}
|
||||
|
||||
// Создаем сообщение для проверки подписи
|
||||
const message = `Sign this message to authenticate with DApp for Business. Nonce: ${nonce}`;
|
||||
|
||||
// Восстанавливаем адрес из подписи
|
||||
const recoveredAddress = ethers.verifyMessage(message, signature);
|
||||
|
||||
// Проверяем, что восстановленный адрес совпадает с предоставленным
|
||||
if (recoveredAddress.toLowerCase() !== address.toLowerCase()) {
|
||||
return res.status(400).json({ error: 'Invalid signature' });
|
||||
}
|
||||
|
||||
// Проверяем, существует ли пользователь в базе данных
|
||||
const user = await db.query('SELECT * FROM users WHERE address = $1', [address]);
|
||||
|
||||
let userId;
|
||||
let isAdmin = false;
|
||||
|
||||
if (user.rows.length === 0) {
|
||||
// Если пользователь не существует, создаем его
|
||||
const newUser = await db.query(
|
||||
'INSERT INTO users (address, created_at) VALUES ($1, NOW()) RETURNING id',
|
||||
[address]
|
||||
);
|
||||
userId = newUser.rows[0].id;
|
||||
} else {
|
||||
userId = user.rows[0].id;
|
||||
isAdmin = user.rows[0].is_admin || false;
|
||||
}
|
||||
|
||||
// Устанавливаем состояние аутентификации в сессии
|
||||
req.session.authenticated = true;
|
||||
req.session.address = address;
|
||||
req.session.isAdmin = isAdmin;
|
||||
req.session.authType = 'wallet';
|
||||
req.session.userId = userId;
|
||||
|
||||
// Удаляем nonce из сессии
|
||||
delete req.session.nonce;
|
||||
delete req.session.pendingAddress;
|
||||
|
||||
// Явно сохраняем сессию
|
||||
req.session.save((err) => {
|
||||
if (err) {
|
||||
// Удалите или закомментируйте эти логи
|
||||
// console.error('Error saving session:', err);
|
||||
return res.status(500).json({ error: 'Failed to save session' });
|
||||
// Проверяем наличие nonce в заголовке
|
||||
const headerNonce = req.headers['x-auth-nonce'];
|
||||
if (headerNonce) {
|
||||
console.log('Найден nonce в заголовке:', headerNonce);
|
||||
req.session.authNonce = headerNonce;
|
||||
req.session.pendingAddress = address.toLowerCase();
|
||||
}
|
||||
|
||||
// Удалите или закомментируйте
|
||||
// console.log('Authentication successful:', {
|
||||
// address,
|
||||
// isAdmin,
|
||||
// sessionID: req.sessionID
|
||||
// });
|
||||
// Если в запросе есть 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 expectedMessage = `Подтвердите вход в DApp for Business с nonce: ${req.session.authNonce}`;
|
||||
|
||||
// Проверяем, что адрес совпадает с ожидаемым
|
||||
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' });
|
||||
}
|
||||
|
||||
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.userId = user.id;
|
||||
req.session.authType = 'wallet';
|
||||
req.session.isAdmin = user.is_admin;
|
||||
req.session.role = user.role;
|
||||
req.session.authChannel = 'web';
|
||||
req.session.language = req.body.language || 'en';
|
||||
|
||||
// Удаляем временные данные
|
||||
delete req.session.authNonce;
|
||||
delete req.session.pendingAddress;
|
||||
|
||||
// Сохраняем сессию
|
||||
await new Promise((resolve, reject) => {
|
||||
req.session.save(err => {
|
||||
if (err) {
|
||||
console.error('Ошибка при сохранении сессии:', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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 => {
|
||||
if (err) {
|
||||
// Удалите или закомментируйте эти логи
|
||||
// console.error('Error destroying session:', err);
|
||||
return res.status(500).json({ error: 'Failed to logout' });
|
||||
}
|
||||
try {
|
||||
// Сохраняем sessionID перед удалением сессии
|
||||
const sessionID = req.sessionID;
|
||||
|
||||
res.json({ success: true });
|
||||
});
|
||||
// Удаляем сессию из хранилища
|
||||
req.session.destroy(async (err) => {
|
||||
if (err) {
|
||||
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 };
|
||||
@@ -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' });
|
||||
// Проверяем доступность Ollama
|
||||
console.log('Проверка доступности Ollama...');
|
||||
try {
|
||||
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 недоступен' });
|
||||
}
|
||||
|
||||
console.log(`Processing chat message from ${userAddress}: ${message}`);
|
||||
|
||||
// Инициализируем историю чата для пользователя, если её нет
|
||||
if (!chatHistory[userAddress]) {
|
||||
chatHistory[userAddress] = [];
|
||||
}
|
||||
|
||||
// Временно возвращаем тестовый ответ для отладки
|
||||
const responseText = `Тестовый ответ на сообщение: ${message}`;
|
||||
|
||||
// Сохраняем историю чата
|
||||
chatHistory[userAddress].push({
|
||||
type: 'human',
|
||||
text: message
|
||||
// Создаем экземпляр ChatOllama
|
||||
const chat = new ChatOllama({
|
||||
baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
|
||||
model: process.env.OLLAMA_MODEL || 'mistral',
|
||||
system: systemPrompt
|
||||
});
|
||||
|
||||
chatHistory[userAddress].push({
|
||||
type: 'ai',
|
||||
text: responseText
|
||||
});
|
||||
console.log('Отправка запроса к Ollama...');
|
||||
|
||||
return res.json({ response: responseText });
|
||||
// Получаем ответ от модели
|
||||
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: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: process.env.OLLAMA_MODEL || 'mistral',
|
||||
prompt: message,
|
||||
system: systemPrompt,
|
||||
stream: false
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
aiResponse = data.response;
|
||||
console.log('Ответ AI (альтернативный метод):', aiResponse);
|
||||
} catch (fallbackError) {
|
||||
console.error('Ошибка при использовании альтернативного метода:', fallbackError);
|
||||
throw error; // Выбрасываем исходную ошибку
|
||||
}
|
||||
}
|
||||
|
||||
// Отправляем ответ клиенту
|
||||
res.json({
|
||||
reply: aiResponse,
|
||||
language: detectedLanguage
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Подробная ошибка:', error.stack);
|
||||
console.error('Chat error:', error);
|
||||
res.status(500).json({
|
||||
error: "Извините, произошла ошибка при обработке вашего запроса. Пожалуйста, попробуйте позже."
|
||||
});
|
||||
logger.error('Error processing message:', error);
|
||||
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
|
||||
}
|
||||
});
|
||||
|
||||
// Добавьте новый эндпоинт для проверки сессии
|
||||
router.get('/check-session', (req, res) => {
|
||||
// Добавьте этот маршрут для проверки доступных моделей
|
||||
router.get('/models', async (req, res) => {
|
||||
try {
|
||||
console.log('Проверка сессии в chat.js:', {
|
||||
sessionID: req.sessionID,
|
||||
session: req.session ? {
|
||||
isAuthenticated: req.session.isAuthenticated,
|
||||
authenticated: req.session.authenticated,
|
||||
address: req.session.address
|
||||
} : null,
|
||||
cookies: req.cookies,
|
||||
headers: {
|
||||
cookie: req.headers.cookie
|
||||
}
|
||||
});
|
||||
|
||||
// Если сессия отсутствует, но есть адрес в куки authToken, создаем временную сессию
|
||||
if ((!req.session || (!req.session.isAuthenticated && !req.session.authenticated)) && req.cookies.authToken) {
|
||||
console.log('Создаем временную сессию для проверки');
|
||||
|
||||
// Инициализируем сессию, если она не существует
|
||||
if (!req.session) {
|
||||
req.session = {};
|
||||
}
|
||||
|
||||
req.session.isAuthenticated = true;
|
||||
req.session.authenticated = true;
|
||||
req.session.isAdmin = true;
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'Temporary session created',
|
||||
isAdmin: true
|
||||
});
|
||||
}
|
||||
|
||||
if (!req.session) {
|
||||
return res.status(401).json({ error: 'No session' });
|
||||
}
|
||||
|
||||
if (!req.session.isAuthenticated && !req.session.authenticated) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
const ollama = new Ollama();
|
||||
const models = await ollama.list();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
address: req.session.address,
|
||||
isAdmin: req.session.isAdmin
|
||||
models: models.models.map((model) => model.name),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Ошибка при проверке сессии:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
console.error('Ошибка при получении списка моделей:', error);
|
||||
res.status(500).json({ success: false, message: 'Ошибка сервера' });
|
||||
}
|
||||
});
|
||||
|
||||
// Добавьте новый эндпоинт для прямой отправки сообщений в Ollama
|
||||
router.post('/ollama', async (req, res) => {
|
||||
// Маршрут для получения истории диалогов (доступен пользователю для своих диалогов)
|
||||
router.get('/history', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { message, model = 'mistral' } = req.body;
|
||||
const userId = req.session.userId;
|
||||
const { limit = 50, offset = 0 } = req.query;
|
||||
|
||||
console.log(`Отправка сообщения в Ollama (${model}):`, message);
|
||||
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]);
|
||||
|
||||
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
|
||||
});
|
||||
res.json(result.rows);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при отправке сообщения в Ollama:', error);
|
||||
res.status(500).json({
|
||||
error: "Ошибка при отправке сообщения в Ollama. Убедитесь, что сервер Ollama запущен."
|
||||
});
|
||||
logger.error('Error fetching chat history:', error);
|
||||
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
|
||||
}
|
||||
});
|
||||
|
||||
// Проверьте, что маршрут правильно настроен
|
||||
router.post('/message', async (req, res) => {
|
||||
// Маршрут для получения всех диалогов (только для админов)
|
||||
router.get('/admin/history', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const { message } = req.body;
|
||||
const { limit = 50, offset = 0, userId } = req.query;
|
||||
|
||||
if (!message) {
|
||||
return res.status(400).json({ error: 'Message is required' });
|
||||
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: 'Внутренняя ошибка сервера' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
0
backend/routes/conversations.js
Normal file
0
backend/routes/conversations.js
Normal 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) => {
|
||||
// Маршрут для проверки состояния сервера
|
||||
router.get('/status', (req, res) => {
|
||||
res.json({
|
||||
message: 'pong',
|
||||
timestamp: new Date().toISOString(),
|
||||
server: {
|
||||
port: process.env.PORT || 8080,
|
||||
address: req.socket.localAddress,
|
||||
hostname: require('os').hostname()
|
||||
}
|
||||
status: 'ok',
|
||||
uptime: process.uptime(),
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
});
|
||||
|
||||
// Тестовый эндпоинт для проверки 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'
|
||||
});
|
||||
}
|
||||
// Маршрут для проверки сессии
|
||||
router.get('/session', (req, res) => {
|
||||
res.json({
|
||||
session: req.session,
|
||||
authenticated: req.session.authenticated || false,
|
||||
});
|
||||
});
|
||||
|
||||
// Тестовый эндпоинт для проверки доступности Ollama
|
||||
router.get('/ollama-status', async (req, res) => {
|
||||
// Маршрут для проверки содержимого таблицы session
|
||||
router.get('/sessions', async (req, res) => {
|
||||
try {
|
||||
const { checkOllamaAvailability } = require('../services/ollama');
|
||||
|
||||
// Проверяем доступность Ollama
|
||||
const isAvailable = await checkOllamaAvailability();
|
||||
|
||||
if (isAvailable) {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
message: 'Ollama доступен'
|
||||
});
|
||||
} else {
|
||||
res.status(503).json({
|
||||
status: 'error',
|
||||
message: 'Ollama недоступен'
|
||||
});
|
||||
}
|
||||
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' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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
246
backend/routes/messages.js
Normal 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
56
backend/routes/roles.js
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
125
backend/scripts/check-ethers-v6-compatibility.js
Normal file
125
backend/scripts/check-ethers-v6-compatibility.js
Normal 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();
|
||||
34
backend/scripts/check-ollama-models.js
Normal file
34
backend/scripts/check-ollama-models.js
Normal 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();
|
||||
@@ -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()}`);
|
||||
|
||||
21
backend/scripts/check-tokens.js
Normal file
21
backend/scripts/check-tokens.js
Normal 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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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('База данных инициализирована успешно');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
53
backend/scripts/update-user-roles.js
Normal file
53
backend/scripts/update-user-roles.js
Normal 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);
|
||||
});
|
||||
@@ -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'],
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
|
||||
exposedHeaders: ['Set-Cookie']
|
||||
}));
|
||||
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-Auth-Nonce'],
|
||||
})
|
||||
);
|
||||
|
||||
// Добавьте после настройки CORS
|
||||
app.use(helmet());
|
||||
@@ -77,18 +77,23 @@ app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// 3. Затем сессии
|
||||
app.use(session({
|
||||
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
||||
resave: true,
|
||||
saveUninitialized: true,
|
||||
cookie: {
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
sameSite: 'lax',
|
||||
maxAge: 24 * 60 * 60 * 1000
|
||||
},
|
||||
store: sessionStore
|
||||
}));
|
||||
app.use(
|
||||
session({
|
||||
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production', // В разработке можно установить false
|
||||
sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax',
|
||||
maxAge: 24 * 60 * 60 * 1000, // 1 день
|
||||
},
|
||||
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 () => {
|
||||
console.log(`Server is running on port ${PORT}`);
|
||||
console.log('Server address:', server.address());
|
||||
let server;
|
||||
|
||||
// Инициализируем сервисы без блокировки запуска сервера
|
||||
initServices().catch(err => {
|
||||
console.error('Ошибка при инициализации сервисов:', err);
|
||||
checkDatabaseStructure().then(() => {
|
||||
// Запускаем сервер
|
||||
server = app.listen(PORT, () => {
|
||||
console.log(`Server is running on port ${PORT}`);
|
||||
console.log('Server address:', server.address());
|
||||
});
|
||||
|
||||
// Проверяем доступность 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);
|
||||
|
||||
158
backend/services/ai-assistant.js
Normal file
158
backend/services/ai-assistant.js
Normal 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,
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка новых писем
|
||||
*/
|
||||
function checkEmails() {
|
||||
const imap = new Imap(imapConfig);
|
||||
|
||||
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 {
|
||||
// Обработка письма
|
||||
await processEmail(parsed);
|
||||
|
||||
// Пометить как прочитанное
|
||||
imap.setFlags(results, ['\\Seen'], (err) => {
|
||||
if (err) {
|
||||
console.error('Error marking email as read:', err);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка полученного письма
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
// В обработчике команд добавьте код для связывания аккаунтов
|
||||
async function processCommand(email, command, args) {
|
||||
if (command === 'link' && args.length > 0) {
|
||||
const ethAddress = args[0];
|
||||
/**
|
||||
* Отправка 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,
|
||||
});
|
||||
|
||||
// Проверяем формат Ethereum-адреса
|
||||
if (!/^0x[a-fA-F0-9]{40}$/.test(ethAddress)) {
|
||||
return 'Неверный формат Ethereum-адреса. Используйте формат 0x...';
|
||||
}
|
||||
|
||||
try {
|
||||
// Получаем ID пользователя по Ethereum-адресу
|
||||
const userId = await getUserIdByIdentity('ethereum', ethAddress);
|
||||
|
||||
if (!userId) {
|
||||
return 'Пользователь с таким Ethereum-адресом не найден. Сначала войдите через веб-интерфейс.';
|
||||
}
|
||||
|
||||
// Связываем Email-аккаунт с пользователем
|
||||
const success = await linkIdentity(userId, 'email', email);
|
||||
|
||||
if (success) {
|
||||
return `Ваш Email-аккаунт успешно связан с Ethereum-адресом ${ethAddress}`;
|
||||
} else {
|
||||
return 'Не удалось связать аккаунты. Возможно, этот Email-аккаунт уже связан с другим пользователем.';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при связывании аккаунтов:', error);
|
||||
return 'Произошла ошибка при связывании аккаунтов. Попробуйте позже.';
|
||||
}
|
||||
console.log('Email sent:', info.messageId);
|
||||
return info;
|
||||
} catch (error) {
|
||||
console.error('Error sending email:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Обработка других команд...
|
||||
}
|
||||
|
||||
module.exports = EmailBotService;
|
||||
module.exports = {
|
||||
initEmailBot,
|
||||
sendEmail,
|
||||
checkEmails,
|
||||
};
|
||||
|
||||
32
backend/services/index.js
Normal file
32
backend/services/index.js
Normal 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,
|
||||
// ... другие экспорты
|
||||
};
|
||||
@@ -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,
|
||||
{
|
||||
returnSourceDocuments: true,
|
||||
prompt: prompt,
|
||||
inputKey: "query",
|
||||
outputKey: "text",
|
||||
verbose: true
|
||||
}
|
||||
);
|
||||
const chain = RetrievalQAChain.fromLLM(model, retriever, {
|
||||
returnSourceDocuments: true,
|
||||
prompt: prompt,
|
||||
inputKey: 'query',
|
||||
outputKey: 'text',
|
||||
verbose: true,
|
||||
});
|
||||
console.log('Цепочка для поиска и ответа создана');
|
||||
|
||||
return chain;
|
||||
|
||||
262
backend/services/telegram-service.js
Normal file
262
backend/services/telegram-service.js
Normal 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,
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
console.log(`Найдено ${files.length} файлов в директории с документами`);
|
||||
|
||||
// Загружаем документы из директории
|
||||
const loader = new DirectoryLoader(
|
||||
DOCS_DIR,
|
||||
{
|
||||
".txt": (path) => new TextLoader(path),
|
||||
".md": (path) => new TextLoader(path),
|
||||
if (indexFiles.length > 0 && indexFiles.includes('hnswlib.index')) {
|
||||
// Загрузка существующего индекса
|
||||
console.log('Loading existing vector store...');
|
||||
try {
|
||||
vectorStore = await HNSWLib.load(VECTOR_STORE_PATH, embeddings);
|
||||
console.log('Vector store loaded successfully');
|
||||
} catch (loadError) {
|
||||
console.error('Error loading existing vector store:', loadError);
|
||||
console.log('Creating new vector store...');
|
||||
await createVectorStore();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// Создание нового индекса
|
||||
console.log('Creating new vector store...');
|
||||
await createVectorStore();
|
||||
}
|
||||
|
||||
return vectorStore;
|
||||
} catch (error) {
|
||||
console.error('Error initializing vector store:', error);
|
||||
// Создаем пустой векторный индекс в случае ошибки
|
||||
vectorStore = new HNSWLib(embeddings, {
|
||||
space: 'cosine',
|
||||
numDimensions: 4096, // Размерность для Ollama embeddings (зависит от модели)
|
||||
});
|
||||
await vectorStore.save(VECTOR_STORE_PATH);
|
||||
return vectorStore;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание нового векторного хранилища из документов
|
||||
*/
|
||||
async function createVectorStore() {
|
||||
try {
|
||||
// Проверяем наличие директории documents
|
||||
const docsPath = path.join(__dirname, '../data/documents');
|
||||
|
||||
// Если директория documents не существует, проверяем директорию docs
|
||||
if (!fs.existsSync(docsPath)) {
|
||||
const altDocsPath = path.join(__dirname, '../data/docs');
|
||||
|
||||
// Если директория docs существует, используем ее
|
||||
if (fs.existsSync(altDocsPath)) {
|
||||
console.log(`Using documents directory at ${altDocsPath}`);
|
||||
return await processDocumentsDirectory(altDocsPath);
|
||||
}
|
||||
|
||||
// Иначе создаем директорию documents
|
||||
fs.mkdirSync(docsPath, { recursive: true });
|
||||
console.log(`Created documents directory at ${docsPath}`);
|
||||
|
||||
// Создание примера документа
|
||||
const sampleDocPath = path.join(docsPath, 'sample.txt');
|
||||
fs.writeFileSync(sampleDocPath, 'Это пример документа для векторного хранилища.');
|
||||
}
|
||||
|
||||
return await processDocumentsDirectory(docsPath);
|
||||
} catch (error) {
|
||||
console.error('Error creating vector store:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка директории с документами
|
||||
* @param {string} docsPath - Путь к директории с документами
|
||||
*/
|
||||
async function processDocumentsDirectory(docsPath) {
|
||||
try {
|
||||
// Загрузка документов
|
||||
const loader = new DirectoryLoader(docsPath, {
|
||||
'.txt': (path) => new TextLoader(path),
|
||||
'.pdf': (path) => new PDFLoader(path),
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
const splitDocs = await textSplitter.splitDocuments(docs);
|
||||
console.log(`Split into ${splitDocs.length} chunks`);
|
||||
|
||||
// Создание векторного хранилища
|
||||
vectorStore = await HNSWLib.fromDocuments(splitDocs, embeddings);
|
||||
}
|
||||
|
||||
// Разбиваем документы на чанки
|
||||
// Сохранение векторного хранилища
|
||||
await vectorStore.save(VECTOR_STORE_PATH);
|
||||
console.log('Vector store created and saved successfully');
|
||||
|
||||
return vectorStore;
|
||||
} catch (error) {
|
||||
console.error('Error processing documents directory:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение векторного хранилища
|
||||
* @returns {HNSWLib|null} Векторное хранилище
|
||||
*/
|
||||
function getVectorStore() {
|
||||
return vectorStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Поиск похожих документов
|
||||
* @param {string} query - Запрос для поиска
|
||||
* @param {number} k - Количество результатов
|
||||
* @returns {Promise<Array>} - Массив похожих документов
|
||||
*/
|
||||
async function similaritySearch(query, k = 5) {
|
||||
if (!vectorStore) {
|
||||
await initializeVectorStore();
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await vectorStore.similaritySearch(query, k);
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('Error performing similarity search:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавление нового документа в векторное хранилище
|
||||
* @param {string} text - Текст документа
|
||||
* @param {Object} metadata - Метаданные документа
|
||||
* @returns {Promise<boolean>} - Успешность добавления
|
||||
*/
|
||||
async function addDocument(text, metadata = {}) {
|
||||
if (!vectorStore) {
|
||||
await initializeVectorStore();
|
||||
}
|
||||
|
||||
try {
|
||||
// Разделение документа на чанки
|
||||
const textSplitter = new RecursiveCharacterTextSplitter({
|
||||
chunkSize: 1000,
|
||||
chunkOverlap: 200,
|
||||
});
|
||||
|
||||
console.log('Разбиение документов на чанки...');
|
||||
const splitDocs = await textSplitter.splitDocuments(docs);
|
||||
console.log(`Документы разбиты на ${splitDocs.length} чанков`);
|
||||
const docs = await textSplitter.createDocuments([text], [metadata]);
|
||||
|
||||
// Создаем эмбеддинги с помощью Ollama
|
||||
console.log('Создание эмбеддингов...');
|
||||
const embeddings = new OllamaEmbeddings({
|
||||
model: "mistral",
|
||||
baseUrl: "http://localhost:11434",
|
||||
});
|
||||
// Добавление документов в векторное хранилище
|
||||
await vectorStore.addDocuments(docs);
|
||||
|
||||
// Проверяем, существует ли уже векторное хранилище
|
||||
if (fs.existsSync(path.join(VECTOR_STORE_DIR, 'hnswlib.index'))) {
|
||||
console.log('Загрузка существующего векторного хранилища...');
|
||||
try {
|
||||
vectorStore = await HNSWLib.load(
|
||||
VECTOR_STORE_DIR,
|
||||
embeddings
|
||||
);
|
||||
console.log('Векторное хранилище успешно загружено');
|
||||
return vectorStore;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке векторного хранилища:', error);
|
||||
console.log('Создание нового векторного хранилища...');
|
||||
}
|
||||
}
|
||||
// Сохранение обновленного векторного хранилища
|
||||
await vectorStore.save(VECTOR_STORE_PATH);
|
||||
|
||||
// Создаем новое векторное хранилище
|
||||
console.log('Создание нового векторного хранилища...');
|
||||
vectorStore = await HNSWLib.fromDocuments(
|
||||
splitDocs,
|
||||
embeddings
|
||||
);
|
||||
|
||||
// Сохраняем векторное хранилище
|
||||
console.log('Сохранение векторного хранилища...');
|
||||
await vectorStore.save(VECTOR_STORE_DIR);
|
||||
console.log('Векторное хранилище успешно сохранено');
|
||||
|
||||
return vectorStore;
|
||||
console.log('Document added to vector store successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при инициализации векторного хранилища:', error);
|
||||
console.log('Приложение продолжит работу без векторного хранилища');
|
||||
// Возвращаем заглушку вместо реального хранилища
|
||||
return {
|
||||
addDocuments: async () => console.log('Векторное хранилище недоступно: addDocuments'),
|
||||
similaritySearch: async () => {
|
||||
console.log('Векторное хранилище недоступно: similaritySearch');
|
||||
return [];
|
||||
}
|
||||
};
|
||||
console.error('Error adding document to vector store:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для получения экземпляра векторного хранилища
|
||||
async function getVectorStore() {
|
||||
if (!vectorStore) {
|
||||
vectorStore = await initializeVectorStore();
|
||||
}
|
||||
return vectorStore;
|
||||
}
|
||||
|
||||
module.exports = { initializeVectorStore, getVectorStore };
|
||||
module.exports = {
|
||||
initializeVectorStore,
|
||||
getVectorStore,
|
||||
similaritySearch,
|
||||
addDocument,
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
46
backend/utils/contracts.js
Normal file
46
backend/utils/contracts.js
Normal 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
2
backend/utils/db.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// Реэкспорт основного модуля db
|
||||
module.exports = require('../db');
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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
4
backend/utils/wallet.js
Normal 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
10
frontend/.prettierrc
Normal 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
64
frontend/eslint.config.js
Normal 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'],
|
||||
},
|
||||
},
|
||||
];
|
||||
2064
frontend/package-lock.json
generated
2064
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
<main class="main-content">
|
||||
<div v-if="isLoading" class="loading">
|
||||
Загрузка...
|
||||
</div>
|
||||
<router-view v-else />
|
||||
</main>
|
||||
</div>
|
||||
<navigation />
|
||||
<main class="main-content">
|
||||
<router-view />
|
||||
</main>
|
||||
</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;
|
||||
// Следим за изменением статуса аутентификации
|
||||
watch(
|
||||
() => authStore.isAuthenticated,
|
||||
(isAuthenticated) => {
|
||||
if (isAuthenticated) {
|
||||
console.log('Пользователь авторизован, проверяем куки');
|
||||
const cookies = document.cookie;
|
||||
if (!cookies.includes('connect.sid')) {
|
||||
console.log('Куки не установлены после авторизации, пробуем обновить сессию');
|
||||
refreshSession();
|
||||
}
|
||||
|
||||
// Если опции подключения еще не отображаются, имитируем отправку сообщения
|
||||
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();
|
||||
}
|
||||
}
|
||||
}, 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
4
frontend/src/api/auth.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import axios from 'axios';
|
||||
|
||||
// Настройка axios для работы с куками
|
||||
axios.defaults.withCredentials = true;
|
||||
@@ -1,40 +1,42 @@
|
||||
<template>
|
||||
<div class="access-control">
|
||||
<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">
|
||||
<div v-if="error" class="error-message">
|
||||
{{ 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 v-else-if="loading" class="loading-message">Загрузка...</div>
|
||||
<div v-else>
|
||||
<div v-if="!isConnected" class="alert alert-warning">
|
||||
Подключите ваш кошелек для проверки доступа
|
||||
</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>
|
||||
</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, () => {
|
||||
checkAccess();
|
||||
});
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</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>
|
||||
<button
|
||||
@click="revokeToken(token.id)"
|
||||
class="btn btn-sm btn-danger"
|
||||
>
|
||||
Отозвать
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="token in tokens" :key="token.id">
|
||||
<td>{{ token.id }}</td>
|
||||
<td>{{ token.owner }}</td>
|
||||
<td>{{ getRoleName(token.role) }}</td>
|
||||
<td>
|
||||
<button @click="revokeToken(token.id)">Отозвать</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<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;
|
||||
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
console.log('Загрузка токенов...');
|
||||
loading.value = true;
|
||||
|
||||
// Добавляем 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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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,80 +46,85 @@
|
||||
</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();
|
||||
},
|
||||
// Получение связанных аккаунтов
|
||||
async function fetchLinkedAccounts() {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
methods: {
|
||||
async loadIdentities() {
|
||||
try {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
const response = await axios.get('/api/identities', {
|
||||
withCredentials: true
|
||||
});
|
||||
|
||||
this.identities = response.data.identities;
|
||||
} catch (error) {
|
||||
console.error('Error loading identities:', error);
|
||||
this.error = 'Не удалось загрузить связанные аккаунты. Попробуйте позже.';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async 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('Не удалось отвязать аккаунт. Попробуйте позже.');
|
||||
}
|
||||
},
|
||||
|
||||
getIdentityTypeLabel(type) {
|
||||
const labels = {
|
||||
ethereum: 'Ethereum',
|
||||
telegram: 'Telegram',
|
||||
email: 'Email'
|
||||
};
|
||||
|
||||
return labels[type] || type;
|
||||
},
|
||||
|
||||
formatIdentityValue(identity) {
|
||||
if (identity.identity_type === 'ethereum') {
|
||||
// Сокращаем Ethereum-адрес
|
||||
const value = identity.identity_value;
|
||||
return `${value.substring(0, 6)}...${value.substring(value.length - 4)}`;
|
||||
}
|
||||
|
||||
return identity.identity_value;
|
||||
}
|
||||
const response = await axios.get(`${import.meta.env.VITE_API_URL}/api/identities/linked`, {
|
||||
withCredentials: true,
|
||||
});
|
||||
identities.value = response.data;
|
||||
} catch (err) {
|
||||
console.error('Ошибка при получении связанных аккаунтов:', err);
|
||||
error.value = 'Не удалось загрузить связанные аккаунты. Попробуйте позже.';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Отвязывание аккаунта
|
||||
async function unlinkAccount(identity) {
|
||||
try {
|
||||
await axios.post(
|
||||
`${import.meta.env.VITE_API_URL}/api/identities/unlink`,
|
||||
{
|
||||
type: identity.identity_type,
|
||||
value: identity.identity_value,
|
||||
},
|
||||
{
|
||||
withCredentials: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Обновляем список после отвязки
|
||||
await fetchLinkedAccounts();
|
||||
} catch (err) {
|
||||
console.error('Ошибка при отвязке аккаунта:', err);
|
||||
error.value = 'Не удалось отвязать аккаунт. Попробуйте позже.';
|
||||
}
|
||||
}
|
||||
|
||||
// Форматирование типа идентификатора
|
||||
function getIdentityTypeLabel(type) {
|
||||
const labels = {
|
||||
ethereum: 'Ethereum',
|
||||
telegram: 'Telegram',
|
||||
email: 'Email',
|
||||
};
|
||||
|
||||
return labels[type] || type;
|
||||
}
|
||||
|
||||
// Форматирование значения идентификатора
|
||||
function formatIdentityValue(identity) {
|
||||
if (identity.identity_type === 'ethereum') {
|
||||
// Сокращаем Ethereum-адрес
|
||||
const value = identity.identity_value;
|
||||
return `${value.substring(0, 6)}...${value.substring(value.length - 4)}`;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
133
frontend/src/components/Navigation.vue
Normal file
133
frontend/src/components/Navigation.vue
Normal 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>
|
||||
161
frontend/src/components/RoleManager.vue
Normal file
161
frontend/src/components/RoleManager.vue
Normal 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>
|
||||
@@ -1,51 +1,98 @@
|
||||
<template>
|
||||
<button @click.stop.prevent="connect" class="auth-btn wallet-btn">
|
||||
<span class="auth-icon">💼</span> Подключить кошелек
|
||||
</button>
|
||||
<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>
|
||||
225
frontend/src/components/chat/ConversationList.vue
Normal file
225
frontend/src/components/chat/ConversationList.vue
Normal 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>
|
||||
134
frontend/src/components/chat/MessageInput.vue
Normal file
134
frontend/src/components/chat/MessageInput.vue
Normal 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>
|
||||
194
frontend/src/components/chat/MessageThread.vue
Normal file
194
frontend/src/components/chat/MessageThread.vue
Normal 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>
|
||||
0
frontend/src/components/identity/EmailConnect.vue
Normal file
0
frontend/src/components/identity/EmailConnect.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
23
frontend/src/composables/useEthereum.js
Normal file
23
frontend/src/composables/useEthereum.js
Normal 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,
|
||||
};
|
||||
}
|
||||
0
frontend/src/locales/en.json
Normal file
0
frontend/src/locales/en.json
Normal file
0
frontend/src/locales/ru.json
Normal file
0
frontend/src/locales/ru.json
Normal 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');
|
||||
|
||||
@@ -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 настроены для режима разработки');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
Reference in New Issue
Block a user