Описание изменений
This commit is contained in:
182
backend/routes/access.js
Normal file
182
backend/routes/access.js
Normal file
@@ -0,0 +1,182 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { Pool } = require('pg');
|
||||
|
||||
// Подключение к БД
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
|
||||
});
|
||||
|
||||
// Проверка доступа
|
||||
router.get('/check', async (req, res) => {
|
||||
const walletAddress = req.headers['x-wallet-address'];
|
||||
|
||||
if (!walletAddress) {
|
||||
return res.status(400).json({ error: 'No wallet address provided' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Проверяем наличие активного токена для адреса
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM access_tokens WHERE wallet_address = $1 AND expires_at > NOW()',
|
||||
[walletAddress.toLowerCase()]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.json({ hasAccess: false });
|
||||
}
|
||||
|
||||
const token = result.rows[0];
|
||||
|
||||
res.json({
|
||||
hasAccess: true,
|
||||
tokenId: token.id,
|
||||
role: token.role,
|
||||
expiresAt: token.expires_at
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Access check error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Проверка прав администратора
|
||||
router.get('/admin-only', async (req, res) => {
|
||||
const walletAddress = req.headers['x-wallet-address'];
|
||||
|
||||
if (!walletAddress) {
|
||||
return res.status(400).json({ error: 'No wallet address provided' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Временное решение: разрешаем доступ для всех
|
||||
console.log('Admin access requested by:', walletAddress);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Admin check error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Получение списка токенов
|
||||
router.get('/tokens', async (req, res) => {
|
||||
const walletAddress = req.headers['x-wallet-address'];
|
||||
|
||||
if (!walletAddress) {
|
||||
return res.status(400).json({ error: 'No wallet address provided' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Проверяем права администратора
|
||||
const adminCheck = await pool.query(
|
||||
'SELECT * FROM access_tokens WHERE wallet_address = $1 AND role = $2 AND expires_at > NOW()',
|
||||
[walletAddress.toLowerCase(), 'ADMIN']
|
||||
);
|
||||
|
||||
if (adminCheck.rows.length === 0) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
|
||||
// Получаем список всех токенов
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM access_tokens ORDER BY created_at DESC'
|
||||
);
|
||||
|
||||
res.json(result.rows.map(token => ({
|
||||
id: token.id,
|
||||
walletAddress: token.wallet_address,
|
||||
role: token.role,
|
||||
createdAt: token.created_at,
|
||||
expiresAt: token.expires_at
|
||||
})));
|
||||
} catch (error) {
|
||||
console.error('Tokens list error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Создание токена
|
||||
router.post('/tokens', async (req, res) => {
|
||||
const walletAddress = req.headers['x-wallet-address'];
|
||||
|
||||
if (!walletAddress) {
|
||||
return res.status(400).json({ error: 'No wallet address provided' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Проверяем права администратора
|
||||
const adminCheck = await pool.query(
|
||||
'SELECT * FROM access_tokens WHERE wallet_address = $1 AND role = $2 AND expires_at > NOW()',
|
||||
[walletAddress.toLowerCase(), 'ADMIN']
|
||||
);
|
||||
|
||||
if (adminCheck.rows.length === 0) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
|
||||
const { walletAddress: targetAddress, role, expiresInDays } = req.body;
|
||||
|
||||
if (!targetAddress || !role || !expiresInDays) {
|
||||
return res.status(400).json({ error: 'Missing required fields' });
|
||||
}
|
||||
|
||||
// Вычисляем дату истечения
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setDate(expiresAt.getDate() + parseInt(expiresInDays));
|
||||
|
||||
// Создаем токен
|
||||
const result = await pool.query(
|
||||
'INSERT INTO access_tokens (wallet_address, role, expires_at) VALUES ($1, $2, $3) RETURNING *',
|
||||
[targetAddress.toLowerCase(), role, expiresAt]
|
||||
);
|
||||
|
||||
res.json({
|
||||
id: result.rows[0].id,
|
||||
walletAddress: result.rows[0].wallet_address,
|
||||
role: result.rows[0].role,
|
||||
createdAt: result.rows[0].created_at,
|
||||
expiresAt: result.rows[0].expires_at
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Token creation error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Отзыв токена
|
||||
router.delete('/tokens/:id', async (req, res) => {
|
||||
const walletAddress = req.headers['x-wallet-address'];
|
||||
|
||||
if (!walletAddress) {
|
||||
return res.status(400).json({ error: 'No wallet address provided' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Проверяем права администратора
|
||||
const adminCheck = await pool.query(
|
||||
'SELECT * FROM access_tokens WHERE wallet_address = $1 AND role = $2 AND expires_at > NOW()',
|
||||
[walletAddress.toLowerCase(), 'ADMIN']
|
||||
);
|
||||
|
||||
if (adminCheck.rows.length === 0) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
|
||||
// Удаляем токен
|
||||
await pool.query(
|
||||
'DELETE FROM access_tokens WHERE id = $1',
|
||||
[id]
|
||||
);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Token revocation error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,855 +0,0 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { ChatOllama } = require('@langchain/ollama');
|
||||
const { PGVectorStore } = require('@langchain/community/vectorstores/pgvector');
|
||||
const { OllamaEmbeddings } = require('@langchain/ollama');
|
||||
const { RunnableSequence } = require('@langchain/core/runnables');
|
||||
const { StringOutputParser } = require('@langchain/core/output_parsers');
|
||||
const { PromptTemplate } = require('@langchain/core/prompts');
|
||||
const { Pool } = require('pg');
|
||||
const { ethers } = require('ethers');
|
||||
const contractABI = require('../artifacts/contracts/MyContract.sol/MyContract.json').abi;
|
||||
const crypto = require('crypto');
|
||||
const TelegramBotService = require('../services/telegramBot');
|
||||
require('dotenv').config();
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
|
||||
});
|
||||
|
||||
const chat = new ChatOllama({
|
||||
model: 'mistral',
|
||||
baseUrl: 'http://localhost:11434',
|
||||
temperature: 0.7,
|
||||
format: 'json'
|
||||
});
|
||||
|
||||
const embeddings = new OllamaEmbeddings({
|
||||
model: 'mistral',
|
||||
baseUrl: 'http://localhost:11434',
|
||||
requestOptions: {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
},
|
||||
dimensions: 4096,
|
||||
stripNewLines: true,
|
||||
maxConcurrency: 1,
|
||||
maxRetries: 3,
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
let vectorStore;
|
||||
let contract;
|
||||
|
||||
async function initVectorStore() {
|
||||
try {
|
||||
console.log('Начинаем инициализацию векторного хранилища...');
|
||||
vectorStore = await PGVectorStore.initialize(
|
||||
embeddings,
|
||||
{
|
||||
postgresConnectionOptions: {
|
||||
connectionString: process.env.DATABASE_URL
|
||||
},
|
||||
tableName: 'documents',
|
||||
columns: {
|
||||
idColumnName: 'id',
|
||||
vectorColumnName: 'embedding',
|
||||
contentColumnName: 'content',
|
||||
metadataColumnName: 'metadata',
|
||||
}
|
||||
}
|
||||
);
|
||||
console.log('Векторное хранилище инициализировано:', {
|
||||
tableName: 'documents',
|
||||
columns: {
|
||||
structure: (await pool.query(`
|
||||
SELECT
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable,
|
||||
column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'documents'
|
||||
ORDER BY ordinal_position
|
||||
`)).rows.map(row => ({
|
||||
name: row.column_name,
|
||||
type: row.data_type,
|
||||
nullable: row.is_nullable,
|
||||
default: row.column_default
|
||||
}))
|
||||
},
|
||||
config: {
|
||||
tableName: vectorStore.tableName,
|
||||
columns: vectorStore.columns,
|
||||
client: vectorStore.client ? 'Connected' : 'Not connected',
|
||||
embeddings: vectorStore.embeddings ? 'Initialized' : 'Not initialized'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Ошибка инициализации векторного хранилища:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function initContract() {
|
||||
try {
|
||||
const provider = new ethers.JsonRpcProvider(process.env.ETHEREUM_NETWORK_URL);
|
||||
// Проверяем подключение к сети
|
||||
const network = await provider.getNetwork();
|
||||
console.log('Подключены к сети:', network.chainId);
|
||||
|
||||
contract = new ethers.Contract(
|
||||
process.env.CONTRACT_ADDRESS,
|
||||
contractABI,
|
||||
provider
|
||||
);
|
||||
|
||||
// Проверяем что контракт существует
|
||||
const code = await provider.getCode(process.env.CONTRACT_ADDRESS);
|
||||
if (code === '0x') {
|
||||
throw new Error('Contract not deployed at this address');
|
||||
}
|
||||
|
||||
// Проверяем подключение
|
||||
const owner = await contract.owner();
|
||||
console.log('Владелец контракта:', owner);
|
||||
console.log('Контракт инициализирован:', process.env.CONTRACT_ADDRESS);
|
||||
} catch (error) {
|
||||
console.error('Ошибка инициализации контракта:', error);
|
||||
// Если контракт не найден, не пытаемся переподключиться
|
||||
if (error.message.includes('not deployed')) {
|
||||
console.error('Контракт не найден по указанному адресу');
|
||||
return;
|
||||
}
|
||||
// Пробуем переподключиться через 5 секунд
|
||||
setTimeout(initContract, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализируем при старте
|
||||
initVectorStore().catch(console.error);
|
||||
initContract().catch(console.error);
|
||||
|
||||
// Проверяем подключение к БД при старте
|
||||
pool.connect((err, client, release) => {
|
||||
if (err) {
|
||||
console.error('Ошибка подключения к PostgreSQL:', err);
|
||||
} else {
|
||||
console.log('Успешное подключение к PostgreSQL');
|
||||
release();
|
||||
}
|
||||
});
|
||||
|
||||
// Middleware для проверки аутентификации
|
||||
function requireAuth(req, res, next) {
|
||||
if (!req.session?.siwe?.address) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
// Генерация случайного nonce
|
||||
function generateNonce() {
|
||||
return crypto.randomBytes(16).toString('base64').replace(/[^a-zA-Z0-9]/g, '');
|
||||
}
|
||||
|
||||
// Получение nonce для подписи
|
||||
router.get('/nonce', (req, res) => {
|
||||
try {
|
||||
setCorsHeaders(res);
|
||||
const nonce = generateNonce();
|
||||
console.log('Сгенерирован новый nonce:', nonce);
|
||||
res.json({ nonce });
|
||||
} catch (error) {
|
||||
console.error('Ошибка генерации nonce:', error);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Верификация подписи
|
||||
router.post('/verify', async (req, res) => {
|
||||
try {
|
||||
const { message, signature } = req.body;
|
||||
|
||||
// Обновляем данные сессии
|
||||
Object.assign(req.session, {
|
||||
authenticated: true,
|
||||
siwe: message,
|
||||
userAddress: message.address,
|
||||
cookie: {
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000
|
||||
}
|
||||
});
|
||||
|
||||
// Ждем сохранения
|
||||
await new Promise((resolve) => {
|
||||
req.session.save(resolve);
|
||||
});
|
||||
|
||||
console.log('Session saved:', {
|
||||
id: req.sessionID,
|
||||
authenticated: req.session.authenticated,
|
||||
address: req.session.userAddress
|
||||
});
|
||||
|
||||
// Проверяем права админа сразу после входа
|
||||
const contractOwner = await contract.owner();
|
||||
const isAdmin = message.address.toLowerCase() === contractOwner.toLowerCase();
|
||||
|
||||
console.log('Проверка прав после входа:', {
|
||||
userAddress: message.address,
|
||||
contractOwner,
|
||||
isAdmin
|
||||
});
|
||||
|
||||
res.json({
|
||||
ok: true,
|
||||
isAdmin
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Verify error:', error);
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Создаем шаблон промпта для RAG
|
||||
const TEMPLATE = `Вы - ассистент в DApp приложении. Используйте следующий контекст для ответа:
|
||||
|
||||
Контекст: {context}
|
||||
Вопрос пользователя: {question}
|
||||
|
||||
Отвечайте кратко и по существу, основываясь на предоставленном контексте. Если контекст пустой или не релевантный,
|
||||
используйте свои базовые знания о DApp и блокчейне.`;
|
||||
|
||||
const prompt = PromptTemplate.fromTemplate(TEMPLATE);
|
||||
|
||||
// Создаем RAG цепочку
|
||||
const chain = RunnableSequence.from([
|
||||
{
|
||||
context: async (input) => {
|
||||
try {
|
||||
const results = await vectorStore.similaritySearch(
|
||||
input.question,
|
||||
1,
|
||||
{ type: 'approved_chat' }
|
||||
);
|
||||
if (!results.length) return '';
|
||||
return results
|
||||
.filter(doc => doc.pageContent)
|
||||
.map(doc => doc.pageContent)
|
||||
.join('\n\n');
|
||||
} catch (error) {
|
||||
console.error('Ошибка поиска контекста:', error);
|
||||
return '';
|
||||
}
|
||||
},
|
||||
question: (input) => input.message
|
||||
},
|
||||
prompt,
|
||||
chat,
|
||||
new StringOutputParser()
|
||||
]);
|
||||
|
||||
// Функция проверки работоспособности эмбеддингов
|
||||
async function checkEmbeddings() {
|
||||
try {
|
||||
const testEmbed = await embeddings.embedQuery('test');
|
||||
console.log('Эмбеддинги работают, размерность:', testEmbed.length);
|
||||
if (testEmbed.length !== 4096) {
|
||||
throw new Error(`Неверная размерность: ${testEmbed.length}, ожидалось: 4096`);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Ошибка эмбеддингов:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
router.post('/chat', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { message } = req.body;
|
||||
const userAddress = req.session.siwe.address;
|
||||
|
||||
// Получаем или создаем пользователя
|
||||
let userResult = await pool.query(
|
||||
'SELECT id FROM users WHERE LOWER(address) = LOWER($1)',
|
||||
[userAddress]
|
||||
);
|
||||
|
||||
if (userResult.rows.length === 0) {
|
||||
userResult = await pool.query(
|
||||
'INSERT INTO users (address) VALUES (LOWER($1)) RETURNING id',
|
||||
[userAddress]
|
||||
);
|
||||
}
|
||||
|
||||
const userId = userResult.rows[0].id;
|
||||
|
||||
// Создаем входные данные для chain
|
||||
const input = {
|
||||
message: message,
|
||||
question: message
|
||||
};
|
||||
|
||||
// Проверяем эмбеддинги перед использованием
|
||||
if (!await checkEmbeddings()) {
|
||||
console.warn('Embeddings service unavailable, continuing without context');
|
||||
try {
|
||||
const response = await chain.invoke(input);
|
||||
|
||||
// Сохраняем в базу без контекста
|
||||
await pool.query(
|
||||
'INSERT INTO chat_history (user_id, message, response) VALUES ($1, $2, $3)',
|
||||
[userId, message, response]
|
||||
);
|
||||
|
||||
return res.json({ response });
|
||||
} catch (error) {
|
||||
console.error('Ошибка генерации ответа:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await chain.invoke(input);
|
||||
|
||||
// Сохраняем в базу с обработкой ошибок
|
||||
try {
|
||||
// Получаем похожие документы
|
||||
const similarDocs = await vectorStore.similaritySearch(
|
||||
message,
|
||||
1,
|
||||
{ type: 'approved_chat' }
|
||||
);
|
||||
|
||||
// Извлекаем ID чатов из метаданных
|
||||
const contextIds = similarDocs
|
||||
.map(doc => doc.metadata?.chatId)
|
||||
.filter(id => typeof id === 'number');
|
||||
|
||||
await pool.query(
|
||||
'INSERT INTO chat_history (user_id, message, response, context_docs) VALUES ($1, $2, $3, $4::integer[])',
|
||||
[userId, message, response, contextIds]
|
||||
);
|
||||
} catch (dbError) {
|
||||
console.error('Ошибка сохранения в БД:', dbError);
|
||||
// Продолжаем выполнение даже при ошибке сохранения
|
||||
}
|
||||
|
||||
res.json({ response });
|
||||
} catch (error) {
|
||||
console.error('Ошибка чата:', error);
|
||||
res.status(500).json({
|
||||
error: error.message,
|
||||
details: error.stack
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Получение истории чата
|
||||
router.get('/chat/history', requireAuth, async (req, res) => {
|
||||
try {
|
||||
setCorsHeaders(res);
|
||||
|
||||
const userAddress = req.session.siwe.address;
|
||||
|
||||
// Получаем историю чата пользователя
|
||||
const result = await pool.query(
|
||||
`SELECT ch.*
|
||||
FROM chat_history ch
|
||||
JOIN users u ON ch.user_id = u.id
|
||||
WHERE LOWER(u.address) = LOWER($1)
|
||||
ORDER BY ch.created_at DESC`,
|
||||
[userAddress]
|
||||
);
|
||||
|
||||
res.json({ history: result.rows });
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения истории:', error);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Получение списка пользователей
|
||||
router.get('/users', requireAuth, async (req, res) => {
|
||||
try {
|
||||
console.log('Запрос списка пользователей');
|
||||
const users = await pool.query(
|
||||
'SELECT id, LOWER(address) as address, created_at FROM users ORDER BY created_at DESC'
|
||||
);
|
||||
console.log('Найдено пользователей:', users.rows);
|
||||
res.json({ users: users.rows });
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения пользователей:', error);
|
||||
res.status(500).json({ error: 'Ошибка сервера' });
|
||||
}
|
||||
});
|
||||
|
||||
// Проверка на админа
|
||||
router.get('/admin/check', requireAuth, async (req, res) => {
|
||||
try {
|
||||
if (!contract) {
|
||||
await initContract();
|
||||
if (!contract) {
|
||||
throw new Error('Contract not initialized');
|
||||
}
|
||||
}
|
||||
|
||||
// Получаем адрес из сессии
|
||||
const userAddress = req.session.siwe.address;
|
||||
console.log('Проверка админа, адрес из сессии:', userAddress);
|
||||
|
||||
const contractOwner = await contract.owner();
|
||||
console.log('Проверка админа:', {
|
||||
userAddress,
|
||||
contractOwner
|
||||
});
|
||||
|
||||
const isAdmin = userAddress.toLowerCase() === contractOwner.toLowerCase();
|
||||
console.log('Результат проверки админа:', isAdmin);
|
||||
|
||||
res.json({ isAdmin });
|
||||
} catch (error) {
|
||||
console.error('Ошибка проверки админа:', error);
|
||||
res.status(500).json({
|
||||
error: 'Server error',
|
||||
details: error.message,
|
||||
code: error.code
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Общая функция для установки CORS заголовков
|
||||
function setCorsHeaders(res) {
|
||||
res.header('Access-Control-Allow-Origin', 'http://127.0.0.1:5173');
|
||||
res.header('Access-Control-Allow-Credentials', 'true');
|
||||
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
|
||||
res.header('Access-Control-Allow-Headers', 'Content-Type, Accept');
|
||||
}
|
||||
|
||||
// Получение всех чатов для админа
|
||||
router.get('/admin/chats', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
setCorsHeaders(res);
|
||||
|
||||
const chats = await pool.query(`
|
||||
SELECT
|
||||
ch.id,
|
||||
LOWER(u.address) as address,
|
||||
ch.message,
|
||||
ch.response,
|
||||
ch.created_at,
|
||||
ch.context_docs,
|
||||
EXISTS (
|
||||
SELECT 1 FROM documents d
|
||||
WHERE d.metadata->>'chatId' = ch.id::text
|
||||
AND d.metadata->>'type' = 'approved_chat'
|
||||
) as is_approved
|
||||
FROM chat_history ch
|
||||
JOIN users u ON ch.user_id = u.id
|
||||
ORDER BY ch.created_at DESC
|
||||
`);
|
||||
|
||||
console.log('Получено чатов:', chats.rows.length);
|
||||
if (chats.rows.length > 0) {
|
||||
console.log('Пример чата:', {
|
||||
id: chats.rows[0].id,
|
||||
address: chats.rows[0].address,
|
||||
is_approved: chats.rows[0].is_approved
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ chats: chats.rows });
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения чатов:', error);
|
||||
res.status(500).json({
|
||||
error: 'Server error',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Одобрение чата для обучения
|
||||
router.post('/admin/approve', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const userAddress = req.session.siwe.address;
|
||||
const contractOwner = await contract.owner();
|
||||
|
||||
if (userAddress.toLowerCase() !== contractOwner.toLowerCase()) {
|
||||
return res.status(403).json({ error: 'Not authorized' });
|
||||
}
|
||||
|
||||
const { chatId } = req.body;
|
||||
|
||||
// Обновляем статус в базе
|
||||
await pool.query(
|
||||
'UPDATE chat_history SET is_approved = true WHERE id = $1',
|
||||
[chatId]
|
||||
);
|
||||
|
||||
// Добавляем в векторное хранилище для обучения
|
||||
const chat = await pool.query(
|
||||
`SELECT message, response FROM chat_history WHERE id = $1`,
|
||||
[chatId]
|
||||
);
|
||||
|
||||
if (chat.rows.length > 0) {
|
||||
const { message, response } = chat.rows[0];
|
||||
console.log('Добавляем в векторное хранилище:', {
|
||||
message: message.substring(0, 50) + '...',
|
||||
response: response.substring(0, 50) + '...',
|
||||
chatId
|
||||
});
|
||||
|
||||
const document = {
|
||||
pageContent: `Q: ${message}\nA: ${response}`,
|
||||
metadata: {
|
||||
type: 'approved_chat',
|
||||
approvedBy: userAddress,
|
||||
chatId: chatId
|
||||
}
|
||||
};
|
||||
|
||||
// Проверяем работу эмбеддингов
|
||||
try {
|
||||
const testEmbedding = await embeddings.embedQuery('test');
|
||||
console.log('Эмбеддинги работают, размерность:', testEmbedding.length);
|
||||
} catch (error) {
|
||||
console.error('Ошибка проверки эмбеддингов:', error);
|
||||
throw new Error('Embeddings error: ' + error.message);
|
||||
}
|
||||
|
||||
console.log('Документ для добавления:', {
|
||||
pageContent: document.pageContent.substring(0, 100) + '...',
|
||||
metadata: document.metadata,
|
||||
vectorStore: {
|
||||
tableName: vectorStore.tableName,
|
||||
columns: vectorStore.columns
|
||||
}
|
||||
});
|
||||
|
||||
// Проверяем существование таблицы и её структуру
|
||||
const tableInfo = await pool.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'documents'
|
||||
);
|
||||
`);
|
||||
console.log('Таблица documents существует:', tableInfo.rows[0].exists);
|
||||
|
||||
if (tableInfo.rows[0].exists) {
|
||||
const columns = await pool.query(`
|
||||
SELECT column_name, data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'documents'
|
||||
ORDER BY ordinal_position;
|
||||
`);
|
||||
console.log('Структура таблицы documents:',
|
||||
columns.rows.map(row => `${row.column_name} (${row.data_type})`)
|
||||
);
|
||||
}
|
||||
|
||||
await vectorStore.addDocuments([
|
||||
document
|
||||
]);
|
||||
|
||||
// Проверяем, что документ добавлен
|
||||
const added = await vectorStore.similaritySearch(
|
||||
document.pageContent,
|
||||
1,
|
||||
{ chatId: chatId }
|
||||
);
|
||||
console.log('Проверка добавления документа:', {
|
||||
found: added.length > 0,
|
||||
document: added[0]?.pageContent.substring(0, 100) + '...'
|
||||
});
|
||||
|
||||
console.log('Успешно добавлено в векторное хранилище');
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Ошибка одобрения:', error);
|
||||
res.status(500).json({
|
||||
error: 'Server error',
|
||||
details: error.message,
|
||||
code: error.code
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Улучшаем проверку авторизации админа
|
||||
async function requireAdmin(req, res, next) {
|
||||
if (!req.session?.siwe?.address) {
|
||||
return res.status(401).json({
|
||||
error: 'Not authenticated',
|
||||
details: 'Please sign in first'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Получаем адреса
|
||||
const userAddress = req.session.siwe.address;
|
||||
const contractOwner = await contract.owner();
|
||||
|
||||
console.log('Проверка админа:', {
|
||||
userAddress: userAddress,
|
||||
contractOwner: contractOwner
|
||||
});
|
||||
|
||||
if (userAddress.toLowerCase() !== contractOwner.toLowerCase()) {
|
||||
return res.status(403).json({
|
||||
error: 'Not authorized',
|
||||
details: 'Only contract owner can access this endpoint'
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Ошибка проверки админа:', error);
|
||||
return res.status(500).json({
|
||||
error: 'Server error',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Получение векторного хранилища для админа
|
||||
router.get('/admin/vectors', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
setCorsHeaders(res);
|
||||
|
||||
// Добавляем колонку created_at если её нет
|
||||
await pool.query(`
|
||||
ALTER TABLE documents
|
||||
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
`);
|
||||
console.log('Проверена/добавлена колонка created_at');
|
||||
|
||||
// Проверяем структуру таблицы
|
||||
const tableInfo = await pool.query(`
|
||||
SELECT column_name, data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'documents'
|
||||
`);
|
||||
console.log('Структура таблицы documents:', tableInfo.rows);
|
||||
|
||||
// Получаем все документы из векторного хранилища
|
||||
const documents = await pool.query(`
|
||||
SELECT
|
||||
d.id,
|
||||
d.content,
|
||||
d.metadata,
|
||||
length(d.embedding::text) as embedding_size,
|
||||
COALESCE(d.created_at, CURRENT_TIMESTAMP) as created_at,
|
||||
CASE
|
||||
WHEN d.metadata->>'type' = 'approved_chat' THEN true
|
||||
ELSE false
|
||||
END as is_approved
|
||||
FROM documents d
|
||||
ORDER BY d.created_at DESC NULLS LAST
|
||||
`);
|
||||
|
||||
// Форматируем ответ
|
||||
const vectors = documents.rows.map(doc => ({
|
||||
id: doc.id,
|
||||
content: doc.content,
|
||||
metadata: doc.metadata,
|
||||
embedding_size: doc.embedding ? 4096 : 0, // Фиксированный размер для mistral
|
||||
created: doc.created_at,
|
||||
is_approved: doc.is_approved
|
||||
}));
|
||||
|
||||
console.log('Получено векторов:', vectors.length);
|
||||
console.log('Пример вектора:', vectors[0]);
|
||||
res.json({ vectors });
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения векторов:', error);
|
||||
console.error('Детали ошибки:', {
|
||||
code: error.code,
|
||||
detail: error.detail,
|
||||
hint: error.hint,
|
||||
position: error.position
|
||||
});
|
||||
res.status(500).json({
|
||||
error: 'Server error',
|
||||
details: error.message,
|
||||
code: error.code
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Обработка CORS preflight запросов для админских роутов
|
||||
router.options('/admin/*', (req, res) => {
|
||||
setCorsHeaders(res);
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
// Очистка кэша и данных
|
||||
router.post('/admin/clear-cache', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
setCorsHeaders(res);
|
||||
|
||||
// Очищаем таблицы
|
||||
await pool.query('TRUNCATE TABLE documents CASCADE');
|
||||
await pool.query('TRUNCATE TABLE chat_history CASCADE');
|
||||
await pool.query('TRUNCATE TABLE users CASCADE');
|
||||
|
||||
// Сбрасываем автоинкремент
|
||||
await pool.query('ALTER SEQUENCE documents_id_seq RESTART WITH 1');
|
||||
await pool.query('ALTER SEQUENCE chat_history_id_seq RESTART WITH 1');
|
||||
await pool.query('ALTER SEQUENCE users_id_seq RESTART WITH 1');
|
||||
|
||||
// Реинициализируем векторное хранилище
|
||||
await initVectorStore();
|
||||
|
||||
console.log('Кэш и данные очищены');
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Ошибка очистки кэша:', error);
|
||||
res.status(500).json({
|
||||
error: 'Server error',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Выход из системы
|
||||
router.post('/signout', requireAuth, async (req, res) => {
|
||||
try {
|
||||
setCorsHeaders(res);
|
||||
|
||||
// Уничтожаем сессию
|
||||
req.session.destroy((err) => {
|
||||
if (err) {
|
||||
console.error('Ошибка при удалении сессии:', err);
|
||||
return res.status(500).json({ error: 'Failed to destroy session' });
|
||||
}
|
||||
|
||||
console.log('Сессия успешно завершена');
|
||||
res.json({ success: true });
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Ошибка выхода:', error);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Проверка сессии
|
||||
router.get('/session', (req, res) => {
|
||||
try {
|
||||
setCorsHeaders(res);
|
||||
|
||||
if (req.session?.authenticated && req.session?.siwe?.address) {
|
||||
res.json({
|
||||
authenticated: true,
|
||||
address: req.session.siwe.address
|
||||
});
|
||||
} else {
|
||||
res.json({
|
||||
authenticated: false
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка проверки сессии:', error);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Создание нового пользователя
|
||||
router.post('/users', async (req, res) => {
|
||||
try {
|
||||
setCorsHeaders(res);
|
||||
|
||||
const { address } = req.body;
|
||||
|
||||
// Проверяем существование пользователя
|
||||
const existingUser = await pool.query(
|
||||
'SELECT * FROM users WHERE address = $1',
|
||||
[address.toLowerCase()]
|
||||
);
|
||||
|
||||
if (existingUser.rows.length > 0) {
|
||||
return res.json({ user: existingUser.rows[0] });
|
||||
}
|
||||
|
||||
// Создаем нового пользователя
|
||||
const result = await pool.query(
|
||||
'INSERT INTO users (address) VALUES ($1) RETURNING *',
|
||||
[address.toLowerCase()]
|
||||
);
|
||||
|
||||
res.json({ user: result.rows[0] });
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания пользователя:', error);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Создание необходимых таблиц при старте
|
||||
async function initializeTables() {
|
||||
try {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
address VARCHAR(42) NOT NULL UNIQUE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS chat_history (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id),
|
||||
message TEXT,
|
||||
response TEXT,
|
||||
is_user BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
console.log('Таблицы успешно инициализированы');
|
||||
|
||||
// Инициализируем vectorStore
|
||||
vectorStore = await PGVectorStore.initialize(
|
||||
embeddings,
|
||||
{
|
||||
postgresConnectionOptions: {
|
||||
connectionString: process.env.DATABASE_URL
|
||||
},
|
||||
tableName: 'documents',
|
||||
columns: {
|
||||
idColumnName: 'id',
|
||||
vectorColumnName: 'embedding',
|
||||
contentColumnName: 'content',
|
||||
metadataColumnName: 'metadata'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log('Векторное хранилище инициализировано:', {
|
||||
tableName: 'documents',
|
||||
columns: vectorStore.columns,
|
||||
config: {
|
||||
tableName: vectorStore.tableName,
|
||||
columns: vectorStore.columns,
|
||||
client: vectorStore.client ? 'Connected' : 'Not Connected',
|
||||
embeddings: vectorStore.embeddings ? 'Initialized' : 'Not Initialized'
|
||||
}
|
||||
});
|
||||
|
||||
// Создаем экземпляр TelegramBotService только после инициализации vectorStore
|
||||
if (vectorStore) {
|
||||
const telegramBot = new TelegramBotService(
|
||||
process.env.TELEGRAM_BOT_TOKEN,
|
||||
vectorStore
|
||||
);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при инициализации:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Вызываем инициализацию при старте
|
||||
initializeTables();
|
||||
|
||||
module.exports = router;
|
||||
357
backend/routes/auth.js
Normal file
357
backend/routes/auth.js
Normal file
@@ -0,0 +1,357 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { ethers } = require('ethers');
|
||||
const crypto = require('crypto');
|
||||
const db = require('../db');
|
||||
const logger = require('../utils/logger');
|
||||
const helmet = require('helmet');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
// Создайте лимитер для попыток аутентификации
|
||||
const authLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 минут
|
||||
max: 20, // Увеличьте лимит с 5 до 20
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
message: { error: 'Слишком много попыток аутентификации. Попробуйте позже.' }
|
||||
});
|
||||
|
||||
// Маршрут для получения nonce для подписи
|
||||
router.get('/nonce', async (req, res) => {
|
||||
try {
|
||||
const { address } = req.query;
|
||||
|
||||
// Удалите или закомментируйте эти логи
|
||||
// console.log('Nonce request:', {
|
||||
// address,
|
||||
// sessionID: req.sessionID,
|
||||
// session: req.session
|
||||
// });
|
||||
|
||||
if (!address) {
|
||||
return res.status(400).json({ error: 'Address is required' });
|
||||
}
|
||||
|
||||
// Генерируем случайный nonce
|
||||
const nonce = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
// Создаем сообщение для подписи
|
||||
const message = `Sign this message to authenticate with DApp for Business. Nonce: ${nonce}`;
|
||||
|
||||
// Сохраняем nonce в сессии
|
||||
req.session.nonce = nonce;
|
||||
req.session.pendingAddress = address;
|
||||
|
||||
// Получаем IP-адрес клиента
|
||||
const clientIP = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
||||
|
||||
// Сохраняем IP-адрес в сессии при генерации nonce
|
||||
req.session.clientIP = clientIP;
|
||||
|
||||
// Явно сохраняем сессию
|
||||
req.session.save((err) => {
|
||||
if (err) {
|
||||
// Удалите или закомментируйте эти логи
|
||||
// console.error('Error saving session:', err);
|
||||
return res.status(500).json({ error: 'Failed to save session' });
|
||||
}
|
||||
|
||||
// Удалите или закомментируйте
|
||||
// console.log('Nonce saved in session:', {
|
||||
// nonce,
|
||||
// pendingAddress: address,
|
||||
// sessionID: req.sessionID
|
||||
// });
|
||||
|
||||
res.json({ message });
|
||||
});
|
||||
} catch (error) {
|
||||
// Удалите или закомментируйте эти логи
|
||||
// console.error('Error generating nonce:', error);
|
||||
logger.error('Error generating nonce:', error);
|
||||
res.status(500).json({ error: 'Failed to generate nonce' });
|
||||
}
|
||||
});
|
||||
|
||||
// Маршрут для верификации подписи
|
||||
router.post('/verify', authLimiter, async (req, res) => {
|
||||
try {
|
||||
const { address, signature } = req.body;
|
||||
|
||||
if (!address || !signature) {
|
||||
return res.status(400).json({ error: 'Address and signature are required' });
|
||||
}
|
||||
|
||||
// Удалите или закомментируйте эти логи
|
||||
// console.log('Verify request:', {
|
||||
// address,
|
||||
// signature,
|
||||
// sessionID: req.sessionID,
|
||||
// session: {
|
||||
// nonce: req.session.nonce,
|
||||
// pendingAddress: req.session.pendingAddress
|
||||
// }
|
||||
// });
|
||||
|
||||
// Получаем nonce из сессии
|
||||
const nonce = req.session.nonce;
|
||||
const pendingAddress = req.session.pendingAddress;
|
||||
|
||||
if (!nonce || !pendingAddress) {
|
||||
return res.status(400).json({ error: 'No pending authentication request' });
|
||||
}
|
||||
|
||||
// Получаем IP-адрес клиента
|
||||
const clientIP = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
||||
|
||||
// Проверяем, что IP-адрес совпадает
|
||||
if (req.session.clientIP !== clientIP) {
|
||||
return res.status(400).json({ error: 'IP address mismatch' });
|
||||
}
|
||||
|
||||
// Проверяем, что адрес совпадает с тем, для которого был сгенерирован nonce
|
||||
if (pendingAddress.toLowerCase() !== address.toLowerCase()) {
|
||||
return res.status(400).json({ error: 'Address mismatch' });
|
||||
}
|
||||
|
||||
// Создаем сообщение для проверки подписи
|
||||
const message = `Sign this message to authenticate with DApp for Business. Nonce: ${nonce}`;
|
||||
|
||||
// Восстанавливаем адрес из подписи
|
||||
const recoveredAddress = ethers.verifyMessage(message, signature);
|
||||
|
||||
// Проверяем, что восстановленный адрес совпадает с предоставленным
|
||||
if (recoveredAddress.toLowerCase() !== address.toLowerCase()) {
|
||||
return res.status(400).json({ error: 'Invalid signature' });
|
||||
}
|
||||
|
||||
// Проверяем, существует ли пользователь в базе данных
|
||||
const user = await db.query('SELECT * FROM users WHERE address = $1', [address]);
|
||||
|
||||
let userId;
|
||||
let isAdmin = false;
|
||||
|
||||
if (user.rows.length === 0) {
|
||||
// Если пользователь не существует, создаем его
|
||||
const newUser = await db.query(
|
||||
'INSERT INTO users (address, created_at) VALUES ($1, NOW()) RETURNING id',
|
||||
[address]
|
||||
);
|
||||
userId = newUser.rows[0].id;
|
||||
} else {
|
||||
userId = user.rows[0].id;
|
||||
isAdmin = user.rows[0].is_admin || false;
|
||||
}
|
||||
|
||||
// Устанавливаем состояние аутентификации в сессии
|
||||
req.session.authenticated = true;
|
||||
req.session.address = address;
|
||||
req.session.isAdmin = isAdmin;
|
||||
req.session.authType = 'wallet';
|
||||
req.session.userId = userId;
|
||||
|
||||
// Удаляем nonce из сессии
|
||||
delete req.session.nonce;
|
||||
delete req.session.pendingAddress;
|
||||
|
||||
// Явно сохраняем сессию
|
||||
req.session.save((err) => {
|
||||
if (err) {
|
||||
// Удалите или закомментируйте эти логи
|
||||
// console.error('Error saving session:', err);
|
||||
return res.status(500).json({ error: 'Failed to save session' });
|
||||
}
|
||||
|
||||
// Удалите или закомментируйте
|
||||
// console.log('Authentication successful:', {
|
||||
// address,
|
||||
// isAdmin,
|
||||
// sessionID: req.sessionID
|
||||
// });
|
||||
|
||||
res.json({
|
||||
authenticated: true,
|
||||
address,
|
||||
isAdmin,
|
||||
authType: 'wallet'
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error verifying signature:', error);
|
||||
|
||||
// Более подробная обработка ошибок
|
||||
if (error.message.includes('invalid signature')) {
|
||||
return res.status(400).json({
|
||||
error: 'Недействительная подпись',
|
||||
message: 'Подпись не соответствует адресу. Пожалуйста, попробуйте снова.'
|
||||
});
|
||||
}
|
||||
|
||||
if (error.message.includes('invalid address')) {
|
||||
return res.status(400).json({
|
||||
error: 'Недействительный адрес',
|
||||
message: 'Указанный адрес имеет неверный формат.'
|
||||
});
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
error: 'Ошибка верификации подписи',
|
||||
message: 'Не удалось проверить подпись. Пожалуйста, попробуйте снова позже.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Маршрут для проверки состояния аутентификации
|
||||
router.get('/check', (req, res) => {
|
||||
// Удалите или закомментируйте эти логи
|
||||
// console.log('Session check:', {
|
||||
// session: req.session,
|
||||
// authenticated: req.session.authenticated
|
||||
// });
|
||||
|
||||
if (req.session.authenticated) {
|
||||
res.json({
|
||||
authenticated: true,
|
||||
address: req.session.address,
|
||||
isAdmin: req.session.isAdmin,
|
||||
authType: req.session.authType
|
||||
});
|
||||
} else {
|
||||
res.json({
|
||||
authenticated: false,
|
||||
address: null,
|
||||
isAdmin: false,
|
||||
authType: null
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Маршрут для выхода из системы
|
||||
router.post('/logout', (req, res) => {
|
||||
req.session.destroy(err => {
|
||||
if (err) {
|
||||
// Удалите или закомментируйте эти логи
|
||||
// console.error('Error destroying session:', err);
|
||||
return res.status(500).json({ error: 'Failed to logout' });
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
// Маршрут для авторизации через Telegram
|
||||
router.get('/telegram', (req, res) => {
|
||||
// Генерируем случайный токен для авторизации
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
// Сохраняем токен в сессии
|
||||
req.session.telegramToken = token;
|
||||
|
||||
// Создаем URL для авторизации через Telegram
|
||||
const botName = process.env.TELEGRAM_BOT_NAME || 'YourBotName';
|
||||
const authUrl = `https://t.me/${botName}?start=${token}`;
|
||||
|
||||
res.json({ authUrl });
|
||||
});
|
||||
|
||||
// Маршрут для авторизации через Email
|
||||
router.post('/email', async (req, res) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
|
||||
if (!email) {
|
||||
return res.status(400).json({ error: 'Email is required' });
|
||||
}
|
||||
|
||||
// Генерируем код подтверждения
|
||||
const verificationCode = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
|
||||
// Сохраняем код в сессии
|
||||
req.session.emailVerificationCode = verificationCode;
|
||||
req.session.pendingEmail = email;
|
||||
|
||||
// В реальном приложении здесь нужно отправить email с кодом подтверждения
|
||||
// Удалите или закомментируйте эти логи
|
||||
// console.log(`Verification code for ${email}: ${verificationCode}`);
|
||||
|
||||
res.json({ success: true, message: 'Verification code sent' });
|
||||
} catch (error) {
|
||||
// Удалите или закомментируйте эти логи
|
||||
// console.error('Error sending verification code:', error);
|
||||
logger.error('Error sending verification code:', error);
|
||||
res.status(500).json({ error: 'Failed to send verification code' });
|
||||
}
|
||||
});
|
||||
|
||||
// Маршрут для проверки кода подтверждения Email
|
||||
router.post('/email/verify', async (req, res) => {
|
||||
try {
|
||||
const { email, code } = req.body;
|
||||
|
||||
if (!email || !code) {
|
||||
return res.status(400).json({ error: 'Email and code are required' });
|
||||
}
|
||||
|
||||
// Получаем код из сессии
|
||||
const verificationCode = req.session.emailVerificationCode;
|
||||
const pendingEmail = req.session.pendingEmail;
|
||||
|
||||
if (!verificationCode || !pendingEmail) {
|
||||
return res.status(400).json({ error: 'No pending verification' });
|
||||
}
|
||||
|
||||
// Проверяем, что email совпадает с тем, для которого был сгенерирован код
|
||||
if (pendingEmail !== email) {
|
||||
return res.status(400).json({ error: 'Email mismatch' });
|
||||
}
|
||||
|
||||
// Проверяем код
|
||||
if (verificationCode !== code) {
|
||||
return res.status(400).json({ error: 'Invalid verification code' });
|
||||
}
|
||||
|
||||
// Проверяем, существует ли пользователь в базе данных
|
||||
const user = await db.query('SELECT * FROM users WHERE email = $1', [email]);
|
||||
|
||||
let userId;
|
||||
let isAdmin = false;
|
||||
|
||||
if (user.rows.length === 0) {
|
||||
// Если пользователь не существует, создаем его
|
||||
const newUser = await db.query(
|
||||
'INSERT INTO users (email, created_at) VALUES ($1, NOW()) RETURNING id',
|
||||
[email]
|
||||
);
|
||||
userId = newUser.rows[0].id;
|
||||
} else {
|
||||
userId = user.rows[0].id;
|
||||
isAdmin = user.rows[0].is_admin || false;
|
||||
}
|
||||
|
||||
// Устанавливаем состояние аутентификации в сессии
|
||||
req.session.isAuthenticated = true;
|
||||
req.session.authenticated = true;
|
||||
req.session.address = email;
|
||||
req.session.userId = userId;
|
||||
req.session.isAdmin = isAdmin;
|
||||
req.session.authType = 'email';
|
||||
|
||||
// Удаляем код из сессии
|
||||
delete req.session.emailVerificationCode;
|
||||
delete req.session.pendingEmail;
|
||||
|
||||
res.json({
|
||||
authenticated: true,
|
||||
address: email,
|
||||
isAdmin,
|
||||
authType: 'email'
|
||||
});
|
||||
} catch (error) {
|
||||
// Удалите или закомментируйте эти логи
|
||||
// console.error('Error verifying email code:', error);
|
||||
logger.error('Error verifying email code:', error);
|
||||
res.status(500).json({ error: 'Failed to verify email code' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = { router };
|
||||
185
backend/routes/chat.js
Normal file
185
backend/routes/chat.js
Normal file
@@ -0,0 +1,185 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { checkAccess } = require('../utils/access-check');
|
||||
const { createOllamaChain, directOllamaQuery, checkOllamaAvailability, ChatOllama } = require('../services/ollama');
|
||||
const { getVectorStore } = require('../services/vectorStore');
|
||||
|
||||
// Хранилище истории чатов
|
||||
const chatHistory = {};
|
||||
|
||||
// Обработка чат-сообщений с проверкой сессии
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
console.log('Получен запрос в chat.js:', {
|
||||
body: req.body,
|
||||
session: req.session ? {
|
||||
id: req.sessionID,
|
||||
address: req.session.address,
|
||||
isAuthenticated: req.session.isAuthenticated,
|
||||
authenticated: req.session.authenticated
|
||||
} : null,
|
||||
cookies: req.cookies,
|
||||
headers: {
|
||||
cookie: req.headers.cookie,
|
||||
origin: req.headers.origin,
|
||||
referer: req.headers.referer,
|
||||
'content-type': req.headers['content-type']
|
||||
}
|
||||
});
|
||||
|
||||
// Проверяем, что тело запроса правильно парсится
|
||||
if (req.headers['content-type'] === 'application/json') {
|
||||
console.log('JSON body:', JSON.stringify(req.body));
|
||||
} else {
|
||||
console.log('Non-JSON body:', req.body);
|
||||
}
|
||||
|
||||
// ВАЖНО: Принимаем любой адрес из запроса без проверки сессии
|
||||
const userAddress = req.body.address || '0xdefault';
|
||||
|
||||
const { message } = req.body;
|
||||
|
||||
if (!message) {
|
||||
return res.status(400).json({ error: 'Message is required' });
|
||||
}
|
||||
|
||||
console.log(`Processing chat message from ${userAddress}: ${message}`);
|
||||
|
||||
// Инициализируем историю чата для пользователя, если её нет
|
||||
if (!chatHistory[userAddress]) {
|
||||
chatHistory[userAddress] = [];
|
||||
}
|
||||
|
||||
// Временно возвращаем тестовый ответ для отладки
|
||||
const responseText = `Тестовый ответ на сообщение: ${message}`;
|
||||
|
||||
// Сохраняем историю чата
|
||||
chatHistory[userAddress].push({
|
||||
type: 'human',
|
||||
text: message
|
||||
});
|
||||
|
||||
chatHistory[userAddress].push({
|
||||
type: 'ai',
|
||||
text: responseText
|
||||
});
|
||||
|
||||
return res.json({ response: responseText });
|
||||
} catch (error) {
|
||||
console.error('Подробная ошибка:', error.stack);
|
||||
console.error('Chat error:', error);
|
||||
res.status(500).json({
|
||||
error: "Извините, произошла ошибка при обработке вашего запроса. Пожалуйста, попробуйте позже."
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Добавьте новый эндпоинт для проверки сессии
|
||||
router.get('/check-session', (req, res) => {
|
||||
try {
|
||||
console.log('Проверка сессии в chat.js:', {
|
||||
sessionID: req.sessionID,
|
||||
session: req.session ? {
|
||||
isAuthenticated: req.session.isAuthenticated,
|
||||
authenticated: req.session.authenticated,
|
||||
address: req.session.address
|
||||
} : null,
|
||||
cookies: req.cookies,
|
||||
headers: {
|
||||
cookie: req.headers.cookie
|
||||
}
|
||||
});
|
||||
|
||||
// Если сессия отсутствует, но есть адрес в куки authToken, создаем временную сессию
|
||||
if ((!req.session || (!req.session.isAuthenticated && !req.session.authenticated)) && req.cookies.authToken) {
|
||||
console.log('Создаем временную сессию для проверки');
|
||||
|
||||
// Инициализируем сессию, если она не существует
|
||||
if (!req.session) {
|
||||
req.session = {};
|
||||
}
|
||||
|
||||
req.session.isAuthenticated = true;
|
||||
req.session.authenticated = true;
|
||||
req.session.isAdmin = true;
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'Temporary session created',
|
||||
isAdmin: true
|
||||
});
|
||||
}
|
||||
|
||||
if (!req.session) {
|
||||
return res.status(401).json({ error: 'No session' });
|
||||
}
|
||||
|
||||
if (!req.session.isAuthenticated && !req.session.authenticated) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
address: req.session.address,
|
||||
isAdmin: req.session.isAdmin
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Ошибка при проверке сессии:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Добавьте новый эндпоинт для прямой отправки сообщений в Ollama
|
||||
router.post('/ollama', async (req, res) => {
|
||||
try {
|
||||
const { message, model = 'mistral' } = req.body;
|
||||
|
||||
console.log(`Отправка сообщения в Ollama (${model}):`, message);
|
||||
|
||||
if (!message) {
|
||||
return res.status(400).json({ error: 'Message is required' });
|
||||
}
|
||||
|
||||
// Используем функцию directOllamaQuery вместо создания нового экземпляра ChatOllama
|
||||
const result = await directOllamaQuery(message, model);
|
||||
|
||||
console.log('Ответ от Ollama:', result);
|
||||
|
||||
// Возвращаем ответ клиенту
|
||||
res.json({
|
||||
response: result,
|
||||
model: model
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Ошибка при отправке сообщения в Ollama:', error);
|
||||
res.status(500).json({
|
||||
error: "Ошибка при отправке сообщения в Ollama. Убедитесь, что сервер Ollama запущен."
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Проверьте, что маршрут правильно настроен
|
||||
router.post('/message', async (req, res) => {
|
||||
try {
|
||||
const { message } = req.body;
|
||||
|
||||
if (!message) {
|
||||
return res.status(400).json({ error: 'Message is required' });
|
||||
}
|
||||
|
||||
console.log('Получено сообщение:', message);
|
||||
|
||||
// Здесь ваш код обработки сообщения
|
||||
// ...
|
||||
|
||||
// Временный ответ для тестирования
|
||||
res.json({
|
||||
response: `Это тестовый ответ на ваше сообщение: "${message}". Сервер работает.`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error processing message:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
32
backend/routes/contracts.js
Normal file
32
backend/routes/contracts.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { requireRole } = require('../middleware/auth');
|
||||
|
||||
// Получение информации о контрактах
|
||||
router.get('/', (req, res) => {
|
||||
res.json({
|
||||
message: 'Contracts API endpoint',
|
||||
contracts: [
|
||||
{
|
||||
name: 'AccessToken',
|
||||
address: process.env.ACCESS_TOKEN_ADDRESS
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
// Защищенный эндпоинт для получения детальной информации о контрактах
|
||||
router.get('/details', requireRole('ADMIN'), (req, res) => {
|
||||
res.json({
|
||||
message: 'Contract details endpoint',
|
||||
contracts: [
|
||||
{
|
||||
name: 'AccessToken',
|
||||
address: process.env.ACCESS_TOKEN_ADDRESS,
|
||||
network: process.env.ETHEREUM_NETWORK_URL.includes('sepolia') ? 'Sepolia' : 'Unknown'
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
189
backend/routes/debug.js
Normal file
189
backend/routes/debug.js
Normal file
@@ -0,0 +1,189 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
// Эндпоинт для отладки сессий
|
||||
router.get('/session', (req, res) => {
|
||||
try {
|
||||
console.log('Отладка сессии:', {
|
||||
sessionID: req.sessionID,
|
||||
session: req.session ? {
|
||||
isAuthenticated: req.session.isAuthenticated,
|
||||
authenticated: req.session.authenticated,
|
||||
address: req.session.address,
|
||||
isAdmin: req.session.isAdmin
|
||||
} : null,
|
||||
cookies: req.cookies,
|
||||
headers: {
|
||||
cookie: req.headers.cookie
|
||||
}
|
||||
});
|
||||
|
||||
res.json({
|
||||
sessionID: req.sessionID,
|
||||
session: req.session ? {
|
||||
isAuthenticated: req.session.isAuthenticated,
|
||||
authenticated: req.session.authenticated,
|
||||
address: req.session.address,
|
||||
isAdmin: req.session.isAdmin
|
||||
} : null,
|
||||
cookies: req.cookies
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Ошибка при отладке сессии:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Эндпоинт для создания тестовой сессии
|
||||
router.post('/create-session', (req, res) => {
|
||||
const { address } = req.body;
|
||||
|
||||
if (!address) {
|
||||
return res.status(400).json({ error: 'Address is required' });
|
||||
}
|
||||
|
||||
// Инициализируем сессию, если она не существует
|
||||
if (!req.session) {
|
||||
req.session = {};
|
||||
}
|
||||
|
||||
req.session.isAuthenticated = true;
|
||||
req.session.authenticated = true;
|
||||
req.session.address = address.toLowerCase();
|
||||
req.session.isAdmin = true;
|
||||
|
||||
// Сохраняем сессию
|
||||
req.session.save((err) => {
|
||||
if (err) {
|
||||
console.error('Ошибка сохранения тестовой сессии:', err);
|
||||
return res.status(500).json({ error: 'Session save error' });
|
||||
}
|
||||
|
||||
console.log('Тестовая сессия создана:', {
|
||||
sessionID: req.sessionID,
|
||||
session: {
|
||||
isAuthenticated: req.session.isAuthenticated,
|
||||
authenticated: req.session.authenticated,
|
||||
address: req.session.address,
|
||||
isAdmin: req.session.isAdmin
|
||||
}
|
||||
});
|
||||
|
||||
res.cookie('authToken', 'true', {
|
||||
maxAge: 86400000,
|
||||
httpOnly: false,
|
||||
secure: false,
|
||||
sameSite: 'lax',
|
||||
path: '/'
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
sessionID: req.sessionID,
|
||||
address: req.session.address,
|
||||
isAdmin: req.session.isAdmin
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Тестовый эндпоинт для отправки сообщений без проверки сессии
|
||||
router.post('/test-chat', (req, res) => {
|
||||
try {
|
||||
const { message, address } = req.body;
|
||||
|
||||
console.log('Тестовый чат-запрос:', {
|
||||
message,
|
||||
address,
|
||||
headers: {
|
||||
cookie: req.headers.cookie,
|
||||
'content-type': req.headers['content-type']
|
||||
},
|
||||
cookies: req.cookies,
|
||||
session: req.session ? {
|
||||
isAuthenticated: req.session.isAuthenticated,
|
||||
authenticated: req.session.authenticated,
|
||||
address: req.session.address,
|
||||
isAdmin: req.session.isAdmin
|
||||
} : null
|
||||
});
|
||||
|
||||
if (!message) {
|
||||
return res.status(400).json({ error: 'Message is required' });
|
||||
}
|
||||
|
||||
// Возвращаем тестовый ответ
|
||||
res.json({
|
||||
response: `Тестовый ответ на сообщение: ${message}`,
|
||||
receivedAddress: address,
|
||||
sessionAddress: req.session?.address
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Ошибка в тестовом чате:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Тестовый эндпоинт для проверки соединения
|
||||
router.get('/ping', (req, res) => {
|
||||
res.json({
|
||||
message: 'pong',
|
||||
timestamp: new Date().toISOString(),
|
||||
server: {
|
||||
port: process.env.PORT || 8080,
|
||||
address: req.socket.localAddress,
|
||||
hostname: require('os').hostname()
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Тестовый эндпоинт для проверки Ollama
|
||||
router.get('/ollama-test', async (req, res) => {
|
||||
try {
|
||||
const { directOllamaQuery } = require('../services/ollama');
|
||||
|
||||
// Тестовый запрос к Ollama
|
||||
const result = await directOllamaQuery('Привет, как дела?', 'mistral');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
response: result,
|
||||
model: 'mistral'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Ошибка при тестировании Ollama:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || 'Ошибка при тестировании Ollama'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Тестовый эндпоинт для проверки доступности Ollama
|
||||
router.get('/ollama-status', async (req, res) => {
|
||||
try {
|
||||
const { checkOllamaAvailability } = require('../services/ollama');
|
||||
|
||||
// Проверяем доступность Ollama
|
||||
const isAvailable = await checkOllamaAvailability();
|
||||
|
||||
if (isAvailable) {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
message: 'Ollama доступен'
|
||||
});
|
||||
} else {
|
||||
res.status(503).json({
|
||||
status: 'error',
|
||||
message: 'Ollama недоступен'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при проверке доступности Ollama:', error);
|
||||
res.status(500).json({
|
||||
status: 'error',
|
||||
message: error.message || 'Ошибка при проверке доступности Ollama'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
36
backend/routes/health.js
Normal file
36
backend/routes/health.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../db');
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
// Проверка соединения с базой данных
|
||||
const dbResult = await db.query('SELECT NOW()');
|
||||
|
||||
// Проверка состояния сервера
|
||||
const memoryUsage = process.memoryUsage();
|
||||
const uptime = process.uptime();
|
||||
|
||||
res.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date(),
|
||||
uptime: uptime,
|
||||
memory: {
|
||||
rss: Math.round(memoryUsage.rss / 1024 / 1024) + 'MB',
|
||||
heapTotal: Math.round(memoryUsage.heapTotal / 1024 / 1024) + 'MB',
|
||||
heapUsed: Math.round(memoryUsage.heapUsed / 1024 / 1024) + 'MB'
|
||||
},
|
||||
database: {
|
||||
connected: true,
|
||||
timestamp: dbResult.rows[0].now
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
status: 'error',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
75
backend/routes/identities.js
Normal file
75
backend/routes/identities.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { linkIdentity, getUserIdentities } = require('../utils/identity-linker');
|
||||
const { Pool } = require('pg');
|
||||
|
||||
// Подключение к БД
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
|
||||
});
|
||||
|
||||
// Middleware для проверки аутентификации
|
||||
function requireAuth(req, res, next) {
|
||||
if (!req.session || (!req.session.isAuthenticated && !req.session.authenticated)) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
// Получение связанных идентификаторов пользователя
|
||||
router.get('/', requireAuth, async (req, res) => {
|
||||
try {
|
||||
// Получаем ID пользователя по Ethereum-адресу
|
||||
const result = await pool.query(
|
||||
'SELECT id FROM users WHERE address = $1',
|
||||
[req.session.address]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
const userId = result.rows[0].id;
|
||||
|
||||
// Получаем все идентификаторы пользователя
|
||||
const identities = await getUserIdentities(userId);
|
||||
|
||||
res.json({ identities });
|
||||
} catch (error) {
|
||||
console.error('Error getting user identities:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Удаление связанного идентификатора
|
||||
router.delete('/:type/:value', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { type, value } = req.params;
|
||||
|
||||
// Получаем ID пользователя по Ethereum-адресу
|
||||
const result = await pool.query(
|
||||
'SELECT id FROM users WHERE address = $1',
|
||||
[req.session.address]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
const userId = result.rows[0].id;
|
||||
|
||||
// Удаляем идентификатор
|
||||
await pool.query(
|
||||
'DELETE FROM user_identities WHERE user_id = $1 AND identity_type = $2 AND identity_value = $3',
|
||||
[userId, type, value]
|
||||
);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error deleting user identity:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
340
backend/routes/kanban.js
Normal file
340
backend/routes/kanban.js
Normal file
@@ -0,0 +1,340 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { Pool } = require('pg');
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
|
||||
});
|
||||
|
||||
// Middleware для проверки аутентификации
|
||||
function requireAuth(req, res, next) {
|
||||
if (!req.session || (!req.session.isAuthenticated && !req.session.authenticated)) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
// Получение всех досок пользователя
|
||||
router.get('/boards', async (req, res) => {
|
||||
try {
|
||||
// Для разработки: если сессия не содержит адрес, используем тестовый
|
||||
const userAddress = (req.session.address || '0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b').toLowerCase();
|
||||
|
||||
console.log('Запрос досок для адреса:', userAddress);
|
||||
|
||||
// Проверяем, существует ли пользователь
|
||||
const userResult = await pool.query(
|
||||
'SELECT id FROM users WHERE address = $1',
|
||||
[userAddress]
|
||||
);
|
||||
|
||||
console.log('Результат запроса пользователя:', userResult.rows);
|
||||
|
||||
if (userResult.rows.length === 0) {
|
||||
console.log('Пользователь не найден, создаем нового');
|
||||
// Если пользователь не найден, создаем его
|
||||
const newUserResult = await pool.query(
|
||||
'INSERT INTO users (address, created_at) VALUES ($1, NOW()) RETURNING id',
|
||||
[userAddress]
|
||||
);
|
||||
console.log('Создан новый пользователь:', newUserResult.rows);
|
||||
}
|
||||
|
||||
// Получаем доски пользователя
|
||||
const ownBoardsQuery = 'SELECT kb.* FROM kanban_boards kb ' +
|
||||
'JOIN users u ON kb.owner_id = u.id ' +
|
||||
'WHERE u.address = $1 ' +
|
||||
'ORDER BY kb.updated_at DESC';
|
||||
|
||||
console.log('Запрос досок пользователя:', ownBoardsQuery);
|
||||
|
||||
const ownBoardsResult = await pool.query(ownBoardsQuery, [userAddress]);
|
||||
|
||||
console.log('Результат запроса досок пользователя:', ownBoardsResult.rows);
|
||||
|
||||
// Получаем доски, к которым у пользователя есть доступ
|
||||
const sharedBoardsResult = await pool.query(
|
||||
'SELECT kb.* FROM kanban_boards kb ' +
|
||||
'JOIN kanban_board_access kba ON kb.id = kba.board_id ' +
|
||||
'JOIN users u1 ON kba.user_id = u1.id ' +
|
||||
'JOIN users u2 ON kb.owner_id = u2.id ' +
|
||||
'WHERE u1.address = $1 AND u2.address != $1 ' +
|
||||
'ORDER BY kb.updated_at DESC',
|
||||
[userAddress]
|
||||
);
|
||||
|
||||
// Получаем публичные доски
|
||||
const publicBoardsResult = await pool.query(
|
||||
'SELECT kb.* FROM kanban_boards kb ' +
|
||||
'JOIN users u ON kb.owner_id = u.id ' +
|
||||
'WHERE kb.is_public = true AND u.address != $1 ' +
|
||||
'AND NOT EXISTS (' +
|
||||
' SELECT 1 FROM kanban_board_access kba ' +
|
||||
' JOIN users u2 ON kba.user_id = u2.id ' +
|
||||
' WHERE kba.board_id = kb.id AND u2.address = $1' +
|
||||
') ' +
|
||||
'ORDER BY kb.updated_at DESC',
|
||||
[userAddress]
|
||||
);
|
||||
|
||||
res.json({
|
||||
ownBoards: ownBoardsResult.rows,
|
||||
sharedBoards: sharedBoardsResult.rows,
|
||||
publicBoards: publicBoardsResult.rows
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching boards:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Создание новой доски
|
||||
router.post('/boards', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { title, description, isPublic } = req.body;
|
||||
|
||||
// Получаем ID пользователя
|
||||
let userResult = await pool.query(
|
||||
'SELECT id FROM users WHERE address = $1',
|
||||
[req.session.address]
|
||||
);
|
||||
|
||||
let userId;
|
||||
|
||||
if (userResult.rows.length === 0) {
|
||||
// Если пользователь не найден, создаем его
|
||||
const newUserResult = await pool.query(
|
||||
'INSERT INTO users (address, created_at, preferred_language) VALUES ($1, NOW(), $2) RETURNING id',
|
||||
[req.session.address, 'ru']
|
||||
);
|
||||
|
||||
userId = newUserResult.rows[0].id;
|
||||
} else {
|
||||
userId = userResult.rows[0].id;
|
||||
}
|
||||
|
||||
// Создаем новую доску
|
||||
const result = await pool.query(
|
||||
`INSERT INTO kanban_boards (title, description, owner_id, is_public, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, NOW(), NOW())
|
||||
RETURNING *`,
|
||||
[title, description, userId, isPublic]
|
||||
);
|
||||
|
||||
// Создаем стандартные колонки
|
||||
const columns = ['Backlog', 'In Progress', 'Review', 'Done'];
|
||||
for (let i = 0; i < columns.length; i++) {
|
||||
await pool.query(
|
||||
`INSERT INTO kanban_columns (board_id, title, position, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, NOW(), NOW())`,
|
||||
[result.rows[0].id, columns[i], i]
|
||||
);
|
||||
}
|
||||
|
||||
res.status(201).json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error('Error creating kanban board:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Получение конкретной доски со всеми колонками и карточками
|
||||
router.get('/boards/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const boardId = req.params.id;
|
||||
|
||||
// Получаем ID пользователя
|
||||
let userResult = await pool.query(
|
||||
'SELECT id FROM users WHERE address = $1',
|
||||
[req.session.address]
|
||||
);
|
||||
|
||||
let userId;
|
||||
|
||||
if (userResult.rows.length === 0) {
|
||||
// Если пользователь не найден, создаем его
|
||||
const newUserResult = await pool.query(
|
||||
'INSERT INTO users (address, created_at, preferred_language) VALUES ($1, NOW(), $2) RETURNING id',
|
||||
[req.session.address, 'ru']
|
||||
);
|
||||
|
||||
userId = newUserResult.rows[0].id;
|
||||
} else {
|
||||
userId = userResult.rows[0].id;
|
||||
}
|
||||
|
||||
// Проверяем доступ к доске
|
||||
const boardResult = await pool.query(
|
||||
'SELECT * FROM kanban_boards WHERE id = $1',
|
||||
[boardId]
|
||||
);
|
||||
|
||||
if (boardResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Board not found' });
|
||||
}
|
||||
|
||||
const board = boardResult.rows[0];
|
||||
|
||||
// Проверяем, имеет ли пользователь доступ к доске
|
||||
if (board.owner_id !== userId && !board.is_public) {
|
||||
const accessResult = await pool.query(
|
||||
'SELECT * FROM kanban_board_access WHERE board_id = $1 AND user_id = $2',
|
||||
[boardId, userId]
|
||||
);
|
||||
|
||||
if (accessResult.rows.length === 0) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
}
|
||||
|
||||
// Получаем колонки доски
|
||||
const columnsResult = await pool.query(
|
||||
'SELECT * FROM kanban_columns WHERE board_id = $1 ORDER BY position',
|
||||
[boardId]
|
||||
);
|
||||
|
||||
// Получаем карточки для всех колонок
|
||||
const cardsResult = await pool.query(
|
||||
`SELECT kc.*, u.address as assigned_address
|
||||
FROM kanban_cards kc
|
||||
LEFT JOIN users u ON kc.assigned_to = u.id
|
||||
WHERE kc.column_id IN (
|
||||
SELECT id FROM kanban_columns WHERE board_id = $1
|
||||
)
|
||||
ORDER BY kc.position`,
|
||||
[boardId]
|
||||
);
|
||||
|
||||
// Группируем карточки по колонкам
|
||||
const columns = columnsResult.rows.map(column => {
|
||||
const cards = cardsResult.rows.filter(card => card.column_id === column.id);
|
||||
return {
|
||||
...column,
|
||||
cards
|
||||
};
|
||||
});
|
||||
|
||||
res.json({
|
||||
...board,
|
||||
columns
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting kanban board:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Добавление колонки к доске
|
||||
router.post('/boards/:boardId/columns', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { boardId } = req.params;
|
||||
const { title, wipLimit } = req.body;
|
||||
|
||||
// Проверяем, существует ли доска
|
||||
const boardResult = await pool.query(
|
||||
'SELECT * FROM kanban_boards WHERE id = $1',
|
||||
[boardId]
|
||||
);
|
||||
|
||||
if (boardResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Board not found' });
|
||||
}
|
||||
|
||||
// Получаем максимальную позицию колонок
|
||||
const positionResult = await pool.query(
|
||||
'SELECT MAX(position) as max_position FROM kanban_columns WHERE board_id = $1',
|
||||
[boardId]
|
||||
);
|
||||
|
||||
const position = positionResult.rows[0].max_position ? positionResult.rows[0].max_position + 1 : 0;
|
||||
|
||||
// Создаем новую колонку
|
||||
const result = await pool.query(
|
||||
`INSERT INTO kanban_columns (board_id, title, position, wip_limit, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, NOW(), NOW())
|
||||
RETURNING *`,
|
||||
[boardId, title, position, wipLimit]
|
||||
);
|
||||
|
||||
res.status(201).json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error('Error creating column:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Получение колонок доски
|
||||
router.get('/boards/:boardId/columns', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { boardId } = req.params;
|
||||
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM kanban_columns WHERE board_id = $1 ORDER BY position',
|
||||
[boardId]
|
||||
);
|
||||
|
||||
res.json(result.rows);
|
||||
} catch (error) {
|
||||
console.error('Error getting columns:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Создание карточки
|
||||
router.post('/cards', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { title, description, columnId, dueDate } = req.body;
|
||||
|
||||
// Получаем ID пользователя
|
||||
let userResult = await pool.query(
|
||||
'SELECT id FROM users WHERE address = $1',
|
||||
[req.session.address]
|
||||
);
|
||||
|
||||
let userId;
|
||||
|
||||
if (userResult.rows.length === 0) {
|
||||
// Если пользователь не найден, создаем его
|
||||
const newUserResult = await pool.query(
|
||||
'INSERT INTO users (address, created_at, preferred_language) VALUES ($1, NOW(), $2) RETURNING id',
|
||||
[req.session.address, 'ru']
|
||||
);
|
||||
|
||||
userId = newUserResult.rows[0].id;
|
||||
} else {
|
||||
userId = userResult.rows[0].id;
|
||||
}
|
||||
|
||||
// Получаем максимальную позицию карточек в колонке
|
||||
const positionResult = await pool.query(
|
||||
'SELECT MAX(position) as max_position FROM kanban_cards WHERE column_id = $1',
|
||||
[columnId]
|
||||
);
|
||||
|
||||
const position = positionResult.rows[0].max_position ? positionResult.rows[0].max_position + 1 : 0;
|
||||
|
||||
// Создаем новую карточку
|
||||
const result = await pool.query(
|
||||
`INSERT INTO kanban_cards (column_id, title, description, position, due_date, created_by, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
|
||||
RETURNING *`,
|
||||
[columnId, title, description, position, dueDate, userId]
|
||||
);
|
||||
|
||||
// Получаем информацию о пользователе для отображения
|
||||
const cardWithUser = {
|
||||
...result.rows[0],
|
||||
assigned_address: null
|
||||
};
|
||||
|
||||
res.status(201).json(cardWithUser);
|
||||
} catch (error) {
|
||||
console.error('Error creating card:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Добавляем остальные маршруты для работы с колонками, карточками и т.д.
|
||||
// ...
|
||||
|
||||
module.exports = router;
|
||||
18
backend/routes/users.js
Normal file
18
backend/routes/users.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
// Получение списка пользователей
|
||||
router.get('/', (req, res) => {
|
||||
res.json({ message: 'Users API endpoint' });
|
||||
});
|
||||
|
||||
// Получение информации о пользователе
|
||||
router.get('/:address', (req, res) => {
|
||||
const { address } = req.params;
|
||||
res.json({
|
||||
address,
|
||||
message: 'User details endpoint'
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user