diff --git a/backend/app.js b/backend/app.js index cd821cc..7b658f2 100644 --- a/backend/app.js +++ b/backend/app.js @@ -12,6 +12,9 @@ const aiAssistant = require('./services/ai-assistant'); // Добавляем и const fs = require('fs'); const path = require('path'); const messagesRoutes = require('./routes/messages'); +const userTagsRoutes = require('./routes/userTags'); +const tagsInitRoutes = require('./routes/tagsInit'); +const tagsRoutes = require('./routes/tags'); // Проверка и создание директорий для хранения данных контрактов const ensureDirectoriesExist = () => { @@ -100,7 +103,8 @@ app.use(async (req, res, next) => { // Если сессия уже есть, используем её if (req.session.authenticated) { - return next(); + next(); + return; } // Проверяем заголовок авторизации @@ -150,14 +154,16 @@ app.use( // Логирование запросов app.use((req, res, next) => { + console.log('[APP] Глобальный лог:', req.method, req.originalUrl); logger.info(`${req.method} ${req.url}`); next(); }); // Маршруты API app.use('/api/tables', tablesRoutes); // ДОЛЖНО БЫТЬ ВЫШЕ! -app.use('/api', identitiesRoutes); +// app.use('/api', identitiesRoutes); app.use('/api/auth', authRoutes); +app.use('/api/users/:userId/tags', userTagsRoutes); app.use('/api/users', usersRoutes); app.use('/api/chat', chatRoutes); app.use('/api/admin', adminRoutes); @@ -167,6 +173,9 @@ app.use('/api/geocoding', geocodingRoutes); // Добавленное испол app.use('/api/dle', dleRoutes); // Добавляем маршрут DLE app.use('/api/settings', settingsRoutes); // Добавляем маршрут настроек app.use('/api/messages', messagesRoutes); +app.use('/api/tags', tagsInitRoutes); +app.use('/api/tags', tagsRoutes); +app.use('/api/identities', identitiesRoutes); const nonceStore = new Map(); // или любая другая реализация хранилища nonce diff --git a/backend/db.js b/backend/db.js index 5785645..fedf9e8 100644 --- a/backend/db.js +++ b/backend/db.js @@ -97,4 +97,4 @@ async function saveGuestMessageToDatabase(message, language, guestId) { } // Экспортируем функции для работы с базой данных -module.exports = { getQuery, pool, getPool, setPoolChangeCallback }; +module.exports = { query: pool.query.bind(pool), getQuery, pool, getPool, setPoolChangeCallback }; diff --git a/backend/db/migrations/034_create_tags_and_user_tags.sql b/backend/db/migrations/034_create_tags_and_user_tags.sql new file mode 100644 index 0000000..668c0d5 --- /dev/null +++ b/backend/db/migrations/034_create_tags_and_user_tags.sql @@ -0,0 +1,13 @@ +-- Создание справочника тегов +CREATE TABLE IF NOT EXISTS tags ( + id SERIAL PRIMARY KEY, + name VARCHAR(64) NOT NULL UNIQUE, + description TEXT +); + +-- Создание связующей таблицы "пользователь — тег" +CREATE TABLE IF NOT EXISTS user_tags ( + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, + PRIMARY KEY (user_id, tag_id) +); diff --git a/backend/routes/tags.js b/backend/routes/tags.js new file mode 100644 index 0000000..3927573 --- /dev/null +++ b/backend/routes/tags.js @@ -0,0 +1,61 @@ +const express = require('express'); +const router = express.Router(); +const db = require('../db'); + +// Получить все теги +router.get('/', async (req, res) => { + console.log('GET /api/tags'); + try { + const query = db.getQuery(); + const { rows } = await query('SELECT * FROM tags ORDER BY name'); + res.json(rows); + } catch (e) { + console.error('Ошибка в /api/tags:', e); + res.status(500).json({ error: e.message, stack: e.stack }); + } +}); + +// Создать тег +router.post('/', async (req, res) => { + console.log('POST /api/tags', req.body); + try { + const { name, description } = req.body; + const query = db.getQuery(); + const result = await query( + 'INSERT INTO tags (name, description) VALUES ($1, $2) RETURNING *', + [name, description] + ); + const row = result && result.rows && result.rows[0] ? result.rows[0] : null; + if (row) { + res.json(row); + } else { + res.status(500).json({ error: 'Не удалось создать тег', result }); + } + } catch (e) { + console.error('Ошибка в /api/tags (POST):', e); + res.status(500).json({ error: e.message, stack: e.stack }); + } +}); + +// Удалить тег и все его связи с пользователями +router.delete('/:tagId', async (req, res) => { + console.log('DELETE /api/tags/:id', req.params.tagId); + try { + const tagId = req.params.tagId; + const query = db.getQuery(); + // Сначала удаляем связи user_tags + await query('DELETE FROM user_tags WHERE tag_id = $1', [tagId]); + // Затем удаляем сам тег + const result = await query('DELETE FROM tags WHERE id = $1 RETURNING *', [tagId]); + if (result.rowCount > 0) { + res.json({ success: true }); + } else { + res.status(404).json({ error: 'Тег не найден' }); + } + } catch (e) { + console.error('Ошибка в /api/tags/:id (DELETE):', e); + res.status(500).json({ error: e.message, stack: e.stack }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/tagsInit.js b/backend/routes/tagsInit.js new file mode 100644 index 0000000..c9a5ed3 --- /dev/null +++ b/backend/routes/tagsInit.js @@ -0,0 +1,29 @@ +const express = require('express'); +const router = express.Router(); +const db = require('../db'); + +// Инициализация таблиц тегов +router.post('/init', async (req, res) => { + console.log('POST /api/tags/init'); + try { + const query = db.getQuery(); + await query(` + CREATE TABLE IF NOT EXISTS tags ( + id SERIAL PRIMARY KEY, + name VARCHAR(64) NOT NULL UNIQUE, + description TEXT + ); + CREATE TABLE IF NOT EXISTS user_tags ( + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, + PRIMARY KEY (user_id, tag_id) + ); + `); + res.json({ ok: true }); + } catch (e) { + console.error('Ошибка в /api/tags/init:', e); + res.status(500).json({ error: e.message, stack: e.stack }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/userTags.js b/backend/routes/userTags.js new file mode 100644 index 0000000..84bb2be --- /dev/null +++ b/backend/routes/userTags.js @@ -0,0 +1,91 @@ +const express = require('express'); +const router = express.Router(); +const db = require('../db'); + +// Инициализация таблиц тегов (если нужно) +router.post('/init', async (req, res) => { + console.log('POST /api/users/tags/init'); + try { + const query = db.getQuery(); + await query(` + CREATE TABLE IF NOT EXISTS tags ( + id SERIAL PRIMARY KEY, + name VARCHAR(64) NOT NULL UNIQUE, + description TEXT + ); + CREATE TABLE IF NOT EXISTS user_tags ( + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, + PRIMARY KEY (user_id, tag_id) + ); + `); + res.json({ ok: true }); + } catch (e) { + console.error('Ошибка в /api/users/tags/init:', e); + res.status(500).json({ error: e.message, stack: e.stack }); + } +}); + +// --- Работа с тегами пользователя --- + +// Получить теги пользователя +router.get('/:userId/tags', async (req, res) => { + console.log('GET /api/users/:id/tags', req.params.userId); + try { + const userId = req.params.userId; + const query = db.getQuery(); + const result = await query( + `SELECT t.* FROM tags t + JOIN user_tags ut ON ut.tag_id = t.id + WHERE ut.user_id = $1`, + [userId] + ); + const rows = result && result.rows ? result.rows : []; + res.json(rows); + } catch (e) { + console.error('Ошибка в /api/users/:id/tags (GET):', e); + res.status(500).json({ error: e.message, stack: e.stack }); + } +}); + +// Добавить тег пользователю +router.post('/:userId/tags', async (req, res) => { + console.log('POST /api/users/:id/tags', req.params.userId, req.body); + try { + const userId = req.params.userId; + const { tag_id } = req.body; + const query = db.getQuery(); + await query( + 'INSERT INTO user_tags (user_id, tag_id) VALUES ($1, $2) ON CONFLICT DO NOTHING', + [userId, tag_id] + ); + res.json({ ok: true }); + } catch (e) { + console.error('Ошибка в /api/users/:id/tags (POST):', e); + res.status(500).json({ error: e.message, stack: e.stack }); + } +}); + +// Удалить тег у пользователя +router.delete('/:userId/tags/:tagId', async (req, res) => { + console.log('DELETE /api/users/:id/tags/:tagId', req.params.userId, req.params.tagId); + try { + const userId = req.params.userId; + const tagId = req.params.tagId; + const query = db.getQuery(); + const result = await query( + 'DELETE FROM user_tags WHERE user_id = $1 AND tag_id = $2 RETURNING *', + [userId, tagId] + ); + if (result.rowCount > 0) { + res.json({ success: true }); + } else { + res.status(404).json({ error: 'Связь не найдена' }); + } + } catch (e) { + console.error('Ошибка в /api/users/:id/tags/:tagId (DELETE):', e); + res.status(500).json({ error: e.message, stack: e.stack }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/users.js b/backend/routes/users.js index cde3cec..ea6dc14 100644 --- a/backend/routes/users.js +++ b/backend/routes/users.js @@ -3,62 +3,22 @@ const router = express.Router(); const db = require('../db'); const logger = require('../utils/logger'); const { requireAuth } = require('../middleware/auth'); +const { deleteUserById } = require('../services/userDeleteService'); +const { broadcastContactsUpdate } = require('../wsHub'); // const userService = require('../services/userService'); +console.log('[users.js] ROUTER LOADED'); + +router.use((req, res, next) => { + console.log('[users.js] ROUTER REQUEST:', req.method, req.originalUrl); + next(); +}); + // Получение списка пользователей // 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', - }); -}); - -// Маршрут для обновления языка пользователя -router.post('/update-language', requireAuth, async (req, res, next) => { - 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.getQuery()('UPDATE users SET preferred_language = $1 WHERE id = $2', [language, userId]); - res.json({ success: true }); - } catch (error) { - logger.error('Error updating language:', error); - next(error); - } -}); - -// Маршрут для обновления имени и фамилии пользователя -router.post('/update-profile', requireAuth, async (req, res, next) => { - try { - const { firstName, lastName } = req.body; - const userId = req.session.userId; - if (firstName && firstName.length > 255) { - return res.status(400).json({ error: 'Имя слишком длинное (максимум 255 символов)' }); - } - if (lastName && lastName.length > 255) { - return res.status(400).json({ error: 'Фамилия слишком длинная (максимум 255 символов)' }); - } - await db.getQuery()('UPDATE users SET first_name = $1, last_name = $2 WHERE id = $3', [ - firstName || null, - lastName || null, - userId, - ]); - res.json({ success: true }); - } catch (error) { - logger.error('Error updating user profile:', error); - next(error); - } -}); - // Получить профиль текущего пользователя /* router.get('/profile', requireAuth, async (req, res) => { @@ -172,21 +132,51 @@ router.patch('/:id', async (req, res) => { // DELETE /api/users/:id — удалить контакт и все связанные данные router.delete('/:id', async (req, res) => { - const userId = req.params.id; - const client = await db.getPool().connect(); + console.log('[users.js] DELETE HANDLER', req.params.id); + const userId = Number(req.params.id); + console.log('[ROUTER] Перед вызовом deleteUserById для userId:', userId); try { - await client.query('BEGIN'); - await client.query('DELETE FROM user_identities WHERE user_id = $1', [userId]); - await client.query('DELETE FROM messages WHERE user_id = $1', [userId]); - // Добавьте другие связанные таблицы, если нужно - await client.query('DELETE FROM users WHERE id = $1', [userId]); - await client.query('COMMIT'); - res.json({ success: true }); + const deletedCount = await deleteUserById(userId); + console.log('[ROUTER] deleteUserById вернул:', deletedCount); + if (deletedCount === 0) { + return res.status(404).json({ success: false, deleted: 0, error: 'User not found' }); + } + broadcastContactsUpdate(); + res.json({ success: true, deleted: deletedCount }); } catch (e) { - await client.query('ROLLBACK'); + console.error('[DELETE] Ошибка при удалении пользователя:', e); res.status(500).json({ error: 'DB error', details: e.message }); - } finally { - client.release(); + } +}); + +// Получить пользователя по id +router.get('/:id', async (req, res, next) => { + const userId = req.params.id; + try { + const query = db.getQuery(); + // Получаем пользователя + const userResult = await query('SELECT id, first_name, last_name, created_at, preferred_language FROM users WHERE id = $1', [userId]); + if (userResult.rows.length === 0) { + return res.status(404).json({ error: 'User not found' }); + } + const user = userResult.rows[0]; + // Получаем идентификаторы + const identitiesResult = await query('SELECT provider, provider_id FROM user_identities WHERE user_id = $1', [userId]); + const identityMap = {}; + for (const id of identitiesResult.rows) { + identityMap[id.provider] = id.provider_id; + } + res.json({ + id: user.id, + name: [user.first_name, user.last_name].filter(Boolean).join(' ') || null, + email: identityMap.email || null, + telegram: identityMap.telegram || null, + wallet: identityMap.wallet || null, + created_at: user.created_at, + preferred_language: user.preferred_language || [] + }); + } catch (e) { + res.status(500).json({ error: e.message }); } }); diff --git a/backend/server.js b/backend/server.js index 46b09c8..315a666 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,22 +1,8 @@ require('dotenv').config(); -const express = require('express'); -const cors = require('cors'); -const { ethers } = require('ethers'); -const session = require('express-session'); const { app, nonceStore } = require('./app'); -const usersRouter = require('./routes/users'); -const authRouter = require('./routes/auth'); -const identitiesRouter = require('./routes/identities'); -const chatRouter = require('./routes/chat'); -const { pool } = require('./db'); -const helmet = require('helmet'); -const { getBot, stopBot } = require('./services/telegramBot'); -const pgSession = require('connect-pg-simple')(session); -const authService = require('./services/auth-service'); +const http = require('http'); +const { initWSS } = require('./wsHub'); const logger = require('./utils/logger'); -const EmailBotService = require('./services/emailBot.js'); -const tablesRouter = require('./routes/tables'); -const errorHandler = require('./middleware/errorHandler'); const PORT = process.env.PORT || 8000; @@ -28,85 +14,18 @@ console.log('Используемый порт:', process.env.PORT || 8000); async function initServices() { try { console.log('Инициализация сервисов...'); - - // Останавливаем предыдущий экземпляр бота - console.log('Перед stopBot'); - await stopBot(); - console.log('После stopBot, перед getBot'); - getBot(); - console.log('После getBot, перед созданием EmailBotService'); - - // Добавляем обработку ошибок при запуске бота - try { - console.log('Пробуем создать экземпляр EmailBotService'); - - // Запуск email-бота - console.log('Создаём экземпляр EmailBotService'); - const emailBot = new EmailBotService(); - await emailBot.start(); - - // Добавляем graceful shutdown - process.once('SIGINT', async () => { - await stopBot(); - process.exit(0); - }); - process.once('SIGTERM', async () => { - await stopBot(); - process.exit(0); - }); - } catch (error) { - if (error.code === 409) { - logger.warn( - 'Another instance of Telegram bot is running. This is normal during development with nodemon' - ); - // Просто логируем ошибку и продолжаем работу - // Бот будет запущен при следующем перезапуске - } else { - logger.error('Error launching Telegram bot:', error); - console.error('Ошибка при запуске Telegram-бота:', error); - } - } - + // Здесь может быть инициализация ботов, email-сервисов и т.д. + // ... console.log('Все сервисы успешно инициализированы'); } catch (error) { console.error('Ошибка при инициализации сервисов:', error); } } -// Настройка сессий -app.use( - session({ - store: new pgSession({ - pool: pool, - tableName: 'session', - }), - secret: process.env.SESSION_SECRET || 'hb3atoken', - resave: false, - saveUninitialized: true, - cookie: { - secure: process.env.NODE_ENV === 'production', - httpOnly: true, - maxAge: 30 * 24 * 60 * 60 * 1000, // 30 дней - }, - }) -); +const server = http.createServer(app); +initWSS(server); -// Маршруты API -app.use('/api/users', usersRouter); -app.use('/api/auth', authRouter); -app.use('/api/identities', identitiesRouter); -app.use('/api/chat', chatRouter); -app.use('/api/tables', tablesRouter); - -// Эндпоинт для проверки состояния сервера -app.get('/api/health', (req, res) => { - res.json({ status: 'ok', timestamp: new Date().toISOString() }); -}); - -// Для отладки -// const host = app.get('host'); -// console.log('host:', host); -app.listen(PORT, async () => { +server.listen(PORT, async () => { try { await initServices(); console.log(`Server is running on port ${PORT}`); @@ -125,6 +44,4 @@ process.on('uncaughtException', (err) => { logger.error('Uncaught Exception:', err); }); -app.use(errorHandler); - module.exports = app; diff --git a/backend/services/userDeleteService.js b/backend/services/userDeleteService.js new file mode 100644 index 0000000..13d04d7 --- /dev/null +++ b/backend/services/userDeleteService.js @@ -0,0 +1,26 @@ +const db = require('../db'); + +async function deleteUserById(userId) { + console.log('[DELETE] Вызван deleteUserById для userId:', userId); + const query = db.getQuery(); + try { + await query('BEGIN'); + console.log('[DELETE] Начинаем удаление user_identities для userId:', userId); + const resIdentities = await query('DELETE FROM user_identities WHERE user_id = $1', [userId]); + console.log('[DELETE] Удалено user_identities:', resIdentities.rowCount); + console.log('[DELETE] Начинаем удаление messages для userId:', userId); + const resMessages = await query('DELETE FROM messages WHERE user_id = $1', [userId]); + console.log('[DELETE] Удалено messages:', resMessages.rowCount); + console.log('[DELETE] Начинаем удаление пользователя из users:', userId); + const result = await query('DELETE FROM users WHERE id = $1 RETURNING *', [userId]); + console.log('[DELETE] Результат удаления пользователя:', result.rowCount, result.rows); + await query('COMMIT'); + return result.rowCount; + } catch (e) { + await query('ROLLBACK'); + console.error('[DELETE] Ошибка при удалении пользователя:', e); + throw e; + } +} + +module.exports = { deleteUserById }; \ No newline at end of file diff --git a/backend/wsHub.js b/backend/wsHub.js new file mode 100644 index 0000000..4fe3623 --- /dev/null +++ b/backend/wsHub.js @@ -0,0 +1,22 @@ +const WebSocket = require('ws'); + +let wss = null; +const wsClients = new Set(); + +function initWSS(server) { + wss = new WebSocket.Server({ server }); + wss.on('connection', (ws) => { + wsClients.add(ws); + ws.on('close', () => wsClients.delete(ws)); + }); +} + +function broadcastContactsUpdate() { + for (const ws of wsClients) { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: 'contacts-updated' })); + } + } +} + +module.exports = { initWSS, broadcastContactsUpdate }; \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 4a3c83f..2f066ab 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "buffer": "^6.0.3", "connect-pg-simple": "^10.0.0", "dompurify": "^3.2.4", + "element-plus": "^2.9.11", "ethers": "6.13.5", "marked": "^15.0.7", "siwe": "^2.1.4", diff --git a/frontend/src/components/ContactDetails.vue b/frontend/src/components/ContactDetails.vue deleted file mode 100644 index dc294ce..0000000 --- a/frontend/src/components/ContactDetails.vue +++ /dev/null @@ -1,323 +0,0 @@ - - - - - \ No newline at end of file diff --git a/frontend/src/components/ContactTable.vue b/frontend/src/components/ContactTable.vue index b9bf29c..89c3682 100644 --- a/frontend/src/components/ContactTable.vue +++ b/frontend/src/components/ContactTable.vue @@ -32,17 +32,18 @@ diff --git a/frontend/src/composables/useChat.js b/frontend/src/composables/useChat.js index 7499832..b848410 100644 --- a/frontend/src/composables/useChat.js +++ b/frontend/src/composables/useChat.js @@ -362,7 +362,7 @@ export function useChat(auth) { } } catch (error) { console.error('[useChat] Ошибка связывания гостевых сообщений:', error); - } + } }; // --- Watchers --- diff --git a/frontend/src/main.js b/frontend/src/main.js index 19b9e0e..c274c8e 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -5,11 +5,15 @@ import { createApp } from 'vue'; import App from './App.vue'; import router from './router'; import axios from 'axios'; +import ElementPlus from 'element-plus'; +import 'element-plus/dist/index.css'; // Настройка axios // В Docker контейнере localhost:8000 не работает, поэтому используем явное значение const apiUrl = - window.location.hostname === 'localhost' ? 'http://localhost:8000' : import.meta.env.VITE_API_URL; + window.location.hostname === 'localhost' + ? 'http://localhost:8000' + : 'http://dapp-backend:8000'; // имя контейнера backend axios.defaults.baseURL = apiUrl; axios.defaults.withCredentials = true; @@ -17,6 +21,7 @@ axios.defaults.withCredentials = true; const app = createApp(App); app.use(router); +app.use(ElementPlus); // Не используем заглушки, так как сервер работает // if (import.meta.env.DEV) { diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 0ebc1d3..3d58510 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -92,6 +92,18 @@ const routes = [ component: () => import('../views/tables/DeleteTableView.vue'), props: true }, + { + path: '/contacts/:id', + name: 'contact-details', + component: () => import('../views/contacts/ContactDetailsView.vue'), + props: true + }, + { + path: '/contacts/:id/delete', + name: 'contact-delete-confirm', + component: () => import('../views/contacts/ContactDeleteConfirm.vue'), + props: true + }, ]; const router = createRouter({ diff --git a/frontend/src/services/contactsService.js b/frontend/src/services/contactsService.js index 06cd67a..4faae53 100644 --- a/frontend/src/services/contactsService.js +++ b/frontend/src/services/contactsService.js @@ -13,7 +13,20 @@ export default { return res.data; }, async deleteContact(id) { - const res = await api.delete(`/api/users/${id}`); - return res.data; + try { + const res = await api.delete(`/api/users/${id}`); + console.log('Ответ на удаление контакта:', res.status, res.data); + return res.data; + } catch (err) { + console.error('Ошибка при удалении контакта:', err.response?.status, err.response?.data, err); + throw err; + } + }, + async getContactById(id) { + const res = await api.get(`/api/users/${id}`); + if (res.data && res.data.id) { + return res.data; + } + return null; } }; \ No newline at end of file diff --git a/frontend/src/views/CrmView.vue b/frontend/src/views/CrmView.vue index 85e7695..42de5d8 100644 --- a/frontend/src/views/CrmView.vue +++ b/frontend/src/views/CrmView.vue @@ -21,7 +21,6 @@ -

Таблицы