diff --git a/backend/app.js b/backend/app.js index 88948f2..64975db 100644 --- a/backend/app.js +++ b/backend/app.js @@ -141,6 +141,18 @@ app.use(async (req, res, next) => { next(); }); +// Middleware для подстановки req.user из сессии +app.use((req, res, next) => { + if (req.session && req.session.userId) { + req.user = { + id: req.session.userId, + isAdmin: req.session.isAdmin, + address: req.session.address, + }; + } + next(); +}); + // Настройка парсеров app.use(express.json()); app.use(express.urlencoded({ extended: true })); diff --git a/backend/db.js b/backend/db.js index fedf9e8..ae44c93 100644 --- a/backend/db.js +++ b/backend/db.js @@ -34,8 +34,12 @@ function getPool() { return pool; } +function query(text, params) { + return pool.query(text, params); +} + function getQuery() { - return pool.query.bind(pool); + return (...args) => pool.query(...args); } let poolChangeCallback = null; @@ -77,8 +81,6 @@ if (process.env.NODE_ENV !== 'migration') { reinitPoolFromDbSettings(); } -const query = (text, params) => pool.query(text, params); - // Функция для сохранения гостевого сообщения в базе данных async function saveGuestMessageToDatabase(message, language, guestId) { try { @@ -97,4 +99,4 @@ async function saveGuestMessageToDatabase(message, language, guestId) { } // Экспортируем функции для работы с базой данных -module.exports = { query: pool.query.bind(pool), getQuery, pool, getPool, setPoolChangeCallback }; +module.exports = { query, getQuery, pool, getPool, setPoolChangeCallback }; diff --git a/backend/db/migrations/035_admin_read_messages.sql b/backend/db/migrations/035_admin_read_messages.sql new file mode 100644 index 0000000..91b8bcd --- /dev/null +++ b/backend/db/migrations/035_admin_read_messages.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS admin_read_messages ( + admin_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + last_read_at TIMESTAMP NOT NULL, + PRIMARY KEY (admin_id, user_id) +); \ No newline at end of file diff --git a/backend/db/migrations/036_admin_read_contacts.sql b/backend/db/migrations/036_admin_read_contacts.sql new file mode 100644 index 0000000..c06db63 --- /dev/null +++ b/backend/db/migrations/036_admin_read_contacts.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS admin_read_contacts ( + admin_id INTEGER NOT NULL, + contact_id INTEGER NOT NULL, + read_at TIMESTAMP NOT NULL DEFAULT NOW(), + PRIMARY KEY (admin_id, contact_id) +); \ No newline at end of file diff --git a/backend/routes/messages.js b/backend/routes/messages.js index 3fa5094..9546982 100644 --- a/backend/routes/messages.js +++ b/backend/routes/messages.js @@ -1,6 +1,7 @@ const express = require('express'); const router = express.Router(); const db = require('../db'); +const { broadcastMessagesUpdate } = require('../wsHub'); // GET /api/messages?userId=123 router.get('/', async (req, res) => { @@ -28,4 +29,71 @@ router.get('/', async (req, res) => { } }); +// POST /api/messages +router.post('/', async (req, res) => { + const { user_id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata } = req.body; + try { + const result = await db.getQuery()( + `INSERT INTO messages (user_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata) + VALUES ($1,$2,$3,$4,$5,$6,NOW(),$7,$8,$9,$10,$11) RETURNING *`, + [user_id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata] + ); + broadcastMessagesUpdate(); + res.json({ success: true, message: result.rows[0] }); + } catch (e) { + res.status(500).json({ error: 'DB error', details: e.message }); + } +}); + +// POST /api/messages/mark-read +router.post('/mark-read', async (req, res) => { + try { + console.log('[DEBUG] /mark-read req.user:', req.user); + console.log('[DEBUG] /mark-read req.body:', req.body); + const adminId = req.user && req.user.id; + const { userId, lastReadAt } = req.body; + if (!adminId) { + console.error('[ERROR] /mark-read: adminId (req.user.id) is missing'); + return res.status(401).json({ error: 'Unauthorized: adminId missing' }); + } + if (!userId || !lastReadAt) { + console.error('[ERROR] /mark-read: userId or lastReadAt missing'); + return res.status(400).json({ error: 'userId and lastReadAt required' }); + } + await db.query(` + INSERT INTO admin_read_messages (admin_id, user_id, last_read_at) + VALUES ($1, $2, $3) + ON CONFLICT (admin_id, user_id) DO UPDATE SET last_read_at = EXCLUDED.last_read_at + `, [adminId, userId, lastReadAt]); + res.json({ success: true }); + } catch (e) { + console.error('[ERROR] /mark-read:', e); + res.status(500).json({ error: e.message }); + } +}); + +// GET /api/messages/read-status +router.get('/read-status', async (req, res) => { + try { + console.log('[DEBUG] /read-status req.user:', req.user); + console.log('[DEBUG] /read-status req.session:', req.session); + console.log('[DEBUG] /read-status req.session.userId:', req.session && req.session.userId); + const adminId = req.user && req.user.id; + if (!adminId) { + console.error('[ERROR] /read-status: adminId (req.user.id) is missing'); + return res.status(401).json({ error: 'Unauthorized: adminId missing' }); + } + const result = await db.query('SELECT user_id, last_read_at FROM admin_read_messages WHERE admin_id = $1', [adminId]); + console.log('[DEBUG] /read-status SQL result:', result.rows); + const map = {}; + for (const row of result.rows) { + map[row.user_id] = row.last_read_at; + } + res.json(map); + } catch (e) { + console.error('[ERROR] /read-status:', e); + res.status(500).json({ error: e.message }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/backend/routes/users.js b/backend/routes/users.js index ea6dc14..8d222d2 100644 --- a/backend/routes/users.js +++ b/backend/routes/users.js @@ -99,6 +99,43 @@ router.get('/', async (req, res) => { }); */ +// Получить просмотренные контакты +router.get('/read-contacts-status', async (req, res) => { + try { + const adminId = req.user && req.user.id; + if (!adminId) { + return res.status(401).json({ error: 'Unauthorized: adminId missing' }); + } + const result = await db.query( + 'SELECT contact_id FROM admin_read_contacts WHERE admin_id = $1', + [adminId] + ); + res.json(result.rows.map(r => r.contact_id)); + } catch (e) { + console.error('[ERROR] /read-contacts-status:', e); + res.status(500).json({ error: e.message }); + } +}); + +// Пометить контакт как просмотренный +router.post('/mark-contact-read', async (req, res) => { + try { + const adminId = req.user && req.user.id; + const { contactId } = req.body; + if (!adminId || !contactId) { + return res.status(400).json({ error: 'adminId and contactId required' }); + } + await db.query( + 'INSERT INTO admin_read_contacts (admin_id, contact_id, read_at) VALUES ($1, $2, NOW()) ON CONFLICT (admin_id, contact_id) DO UPDATE SET read_at = NOW()', + [adminId, contactId] + ); + res.json({ success: true }); + } catch (e) { + console.error('[ERROR] /mark-contact-read:', e); + res.status(500).json({ error: e.message }); + } +}); + // PATCH /api/users/:id — обновить имя и язык router.patch('/:id', async (req, res) => { const userId = req.params.id; @@ -180,4 +217,20 @@ router.get('/:id', async (req, res, next) => { } }); +// POST /api/users +router.post('/', async (req, res) => { + const { first_name, last_name, preferred_language } = req.body; + try { + const result = await db.getQuery()( + `INSERT INTO users (first_name, last_name, preferred_language, created_at) + VALUES ($1, $2, $3, NOW()) RETURNING *`, + [first_name, last_name, JSON.stringify(preferred_language || [])] + ); + broadcastContactsUpdate(); + res.json({ success: true, user: result.rows[0] }); + } catch (e) { + res.status(500).json({ error: 'DB error', details: e.message }); + } +}); + module.exports = router; diff --git a/backend/services/auth-service.js b/backend/services/auth-service.js index bd0ada1..be48960 100644 --- a/backend/services/auth-service.js +++ b/backend/services/auth-service.js @@ -9,6 +9,7 @@ const authTokenService = require('./authTokenService'); const rpcProviderService = require('./rpcProviderService'); const { getLinkedWallet } = require('./wallet-service'); const { checkAdminRole } = require('./admin-role'); +const { broadcastContactsUpdate } = require('../wsHub'); const ERC20_ABI = ['function balanceOf(address owner) view returns (uint256)']; @@ -102,6 +103,8 @@ class AuthService { ); } + broadcastContactsUpdate(); + return { userId, isAdmin }; } catch (error) { logger.error('Error finding or creating user:', error); @@ -743,6 +746,8 @@ class AuthService { delete session.tempUserId; delete session.pendingEmail; + broadcastContactsUpdate(); + return { userId, email: normalizedEmail, diff --git a/backend/services/emailAuth.js b/backend/services/emailAuth.js index ed1cee1..2db3415 100644 --- a/backend/services/emailAuth.js +++ b/backend/services/emailAuth.js @@ -5,6 +5,7 @@ const EmailBotService = require('./emailBot.js'); const db = require('../db'); const authService = require('./auth-service'); const { checkAdminRole } = require('./admin-role'); +const { broadcastContactsUpdate } = require('../wsHub'); class EmailAuth { constructor() { @@ -65,6 +66,9 @@ class EmailAuth { `Generated verification code for Email auth for ${email} and sent to user's email` ); + // После каждого успешного создания пользователя: + broadcastContactsUpdate(); + return { success: true, verificationCode }; } catch (error) { logger.error('Error in email auth initialization:', error); @@ -201,6 +205,9 @@ class EmailAuth { delete session.tempUserId; } + // После каждого успешного создания пользователя: + broadcastContactsUpdate(); + return { verified: true, userId: finalUserId, diff --git a/backend/services/emailBot.js b/backend/services/emailBot.js index 1bcac1b..e1e38e8 100644 --- a/backend/services/emailBot.js +++ b/backend/services/emailBot.js @@ -8,6 +8,7 @@ const { inspect } = require('util'); const logger = require('../utils/logger'); const identityService = require('./identity-service'); const aiAssistant = require('./ai-assistant'); +const { broadcastContactsUpdate } = require('../wsHub'); class EmailBotService { constructor() { @@ -172,6 +173,8 @@ class EmailBotService { ); // 5. Отправить ответ на email await this.sendEmail(fromEmail, 'Re: ' + subject, aiResponse); + // После каждого успешного создания пользователя: + broadcastContactsUpdate(); } catch (processErr) { logger.error('Error processing incoming email:', processErr); } diff --git a/backend/services/identity-service.js b/backend/services/identity-service.js index b40682b..532ddd8 100644 --- a/backend/services/identity-service.js +++ b/backend/services/identity-service.js @@ -544,6 +544,7 @@ class IdentityService { await this.saveIdentity(userId, provider, providerId, true); user = { id: userId, role: 'user' }; isNew = true; + logger.info('[WS] broadcastContactsUpdate after new user created'); broadcastContactsUpdate(); } // Проверяем связь с кошельком diff --git a/backend/services/telegramBot.js b/backend/services/telegramBot.js index ae2c3eb..a2433aa 100644 --- a/backend/services/telegramBot.js +++ b/backend/services/telegramBot.js @@ -7,6 +7,7 @@ const crypto = require('crypto'); const identityService = require('./identity-service'); const aiAssistant = require('./ai-assistant'); const { checkAdminRole } = require('./admin-role'); +const { broadcastContactsUpdate } = require('../wsHub'); let botInstance = null; let telegramSettingsCache = null; @@ -252,6 +253,9 @@ async function getBot() { } catch (error) { logger.warn('Could not delete code message:', error); } + + // После каждого успешного создания пользователя: + broadcastContactsUpdate(); } catch (error) { logger.error('Error in Telegram auth:', error); await ctx.reply('Произошла ошибка при аутентификации. Попробуйте позже.'); diff --git a/backend/wsHub.js b/backend/wsHub.js index 4fe3623..f6daa6c 100644 --- a/backend/wsHub.js +++ b/backend/wsHub.js @@ -19,4 +19,12 @@ function broadcastContactsUpdate() { } } -module.exports = { initWSS, broadcastContactsUpdate }; \ No newline at end of file +function broadcastMessagesUpdate() { + for (const ws of wsClients) { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: 'messages-updated' })); + } + } +} + +module.exports = { initWSS, broadcastContactsUpdate, broadcastMessagesUpdate }; \ No newline at end of file diff --git a/frontend/src/components/BaseLayout.vue b/frontend/src/components/BaseLayout.vue index 48eb41a..f350163 100644 --- a/frontend/src/components/BaseLayout.vue +++ b/frontend/src/components/BaseLayout.vue @@ -176,8 +176,8 @@ onMounted(() => { if (savedSidebarState !== null) { showWalletSidebar.value = savedSidebarState; } else { - showWalletSidebar.value = true; - setToStorage('showWalletSidebar', true); + showWalletSidebar.value = false; // по умолчанию закрыт + setToStorage('showWalletSidebar', false); } }); diff --git a/frontend/src/components/ContactTable.vue b/frontend/src/components/ContactTable.vue index 143dde1..fde9d05 100644 --- a/frontend/src/components/ContactTable.vue +++ b/frontend/src/components/ContactTable.vue @@ -4,6 +4,17 @@
| {{ contact.name || '-' }} | {{ contact.email || '-' }} | {{ contact.telegram || '-' }} | {{ contact.wallet || '-' }} | -{{ formatDate(contact.created_at) }} | +{{ contact.created_at ? new Date(contact.created_at).toLocaleString() : '-' }} | + |