diff --git a/RAG_TASKS.md b/RAG_TASKS.md new file mode 100644 index 0000000..2c7624f --- /dev/null +++ b/RAG_TASKS.md @@ -0,0 +1,129 @@ +# Внедрение RAG-ассистента: поэтапный план + +--- + +## Особенности проекта: разнообразие клиентов, каналов и данных + +- **Клиенты:** + - Различные сегменты: B2B, B2C, VIP, оптовые и розничные покупатели, корпоративные клиенты, частные лица и др. + - Различные сценарии взаимодействия (покупка, поддержка, консультация, возврат и т.д.). + +- **Каналы коммуникации:** + - Веб-чат + - Email + - Telegram/мессенджеры + - Возможна интеграция с другими каналами (WhatsApp, телефон и др.) + +- **Типы данных:** + - Текстовые сообщения + - Аудио, видео, изображения (мультимодальные данные) + - Вложения (документы, сканы, фото товаров и т.д.) + +- **Языки:** + - Русский + - Английский + - Испанский + - Китайский + - Возможность расширения на другие языки + +- **Товары и услуги:** + - Широкий ассортимент товаров (разные категории, бренды, характеристики) + - Различные услуги (консультации, сервис, доставка, гарантия, возврат и др.) + - Возможность кросс-продаж и рекомендаций + +- **Требования к RAG:** + - Гибкая фильтрация знаний по сегменту клиента, языку, категории товара/услуги, каналу обращения + - Поддержка мультиязычности и мультимодальности + - Масштабируемость для добавления новых ассистентов, сегментов, каналов и языков + +--- + +## Этап 1. Проектирование и подготовка инфраструктуры +1. **Проектирование схемы хранения знаний (RAG):** + - Описать структуру таблицы `knowledge_documents` (миграция). + - Определить поля: id, content, language, type (текст/медиа), метаданные, дата, автор и т.д. +2. **Подготовка backend:** + - Создать миграцию и модель для `knowledge_documents`. + - Подготовить базовые CRUD-эндпоинты для работы с базой знаний. + +--- + +## Этап 2. Интеграция векторного поиска (RAG) +1. **Реализация векторного хранилища:** + - Реализовать методы инициализации и поиска (`initVectorStore`, `findSimilarDocuments`) в `ai-assistant.js`. + - Настроить хранение эмбеддингов для документов. +2. **API для поиска знаний:** + - Добавить эндпоинт для поиска релевантных знаний по запросу пользователя. + +--- + +## Этап 3. Интеграция RAG в pipeline ассистента +1. **Модификация логики ответа ассистента:** + - При получении сообщения пользователя — искать релевантные знания и включать их в prompt LLM. + - Обеспечить мультиязычность поиска и генерации ответа. +2. **Логирование и трассировка:** + - Сохранять, какие знания были использованы для ответа. + +--- + +## Этап 4. Интерфейс для админа +1. **UI для управления знаниями:** + - Добавить на фронте раздел для просмотра, добавления, редактирования и удаления знаний. +2. **UI для модерации ответов ассистента:** + - Кнопки "Редактировать", "Отправить", "Добавить в RAG" для сообщений и ответов. + - Возможность быстро добавить сообщение пользователя или ответ ассистента в базу знаний. + +--- + +## Этап 5. Поддержка мультимодальности и мультиязычности +1. **Обработка вложений (аудио, видео, картинки):** + - Решить, как хранить и индексировать такие данные (например, хранить ссылки и метаданные, а не сами файлы). +2. **Мультиязычный поиск и генерация:** + - Проверить корректность работы эмбеддингов и LLM для разных языков. + +--- + +## Этап 6. Тестирование и оптимизация +1. **Покрытие тестами ключевых сценариев (unit, интеграционные).** +2. **Оптимизация скорости поиска и генерации.** +3. **Документация для команды.** + +--- + +## Бизнес-логика управления знаниями и тегами для RAG-ассистента + +### 1. Гибкая система тегов и связей с пользователями +- Пользователь может создавать собственные таблицы тегов (например, "покупатель", "поставщик", "VIP-клиент" и т.д.). +- В таблице тегов должна быть возможность добавлять ссылки (relation) на пользователей из таблицы `users`. +- Для одного тега может быть привязано несколько пользователей (мультисвязь). +- Для одного пользователя может быть несколько тегов. + +### 2. Управление знаниями (FAQ, инструкции, ответы) +- Пользователь может создавать таблицы с вопросами и ответами (например, FAQ для определённой группы клиентов). +- Каждая запись (вопрос-ответ) может быть связана с определённым тегом или группой тегов. +- Возможна фильтрация и поиск знаний по тегам, языку, типу клиента и другим параметрам. + +### 3. Использование тегов и знаний в RAG-ассистенте +- При обработке запроса пользователя RAG-ассистент определяет его теги (по связям в таблице тегов). +- Для генерации ответа ассистент использует только те знания (вопросы/ответы), которые соответствуют тегам пользователя. +- Администратор может добавлять новые теги, связывать их с пользователями, а также создавать и редактировать знания для каждой группы. + +### 4. UI/UX требования +- В интерфейсе создания/редактирования пользовательских таблиц должен быть доступен тип столбца "relation" (связь с users). +- Для ячеек типа "relation" реализовать выпадающий список с поиском по пользователям. +- Для таблиц знаний — возможность выбора одного или нескольких тегов для каждой записи. + +**Пример структуры:** +- Таблица `user_tags`: id, name, [user_id (relation, мультисвязь)] +- Таблица `faq`: id, question, answer, [tag_id (relation, мультисвязь)] + +**Применение:** +- RAG-ассистент использует связи между пользователями, тегами и знаниями для персонализации ответов и поиска релевантной информации. + +### 5. Безопасность и контроль +- Только администратор может создавать и редактировать системные теги и знания. +- Обычные пользователи могут видеть только свои теги и связанные с ними знания. + +--- + +**Этот документ будет дополняться по мере реализации каждого этапа.** \ No newline at end of file diff --git a/backend/db/migrations/028_create_dynamic_tables.sql b/backend/db/migrations/028_create_dynamic_tables.sql new file mode 100644 index 0000000..ed33cbd --- /dev/null +++ b/backend/db/migrations/028_create_dynamic_tables.sql @@ -0,0 +1,36 @@ +-- Миграция для динамических пользовательских таблиц (аналог Notion) + +CREATE TABLE IF NOT EXISTS user_tables ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS user_columns ( + id SERIAL PRIMARY KEY, + table_id INTEGER NOT NULL REFERENCES user_tables(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + type VARCHAR(50) NOT NULL, -- text, number, select, multiselect, date, etc. + options JSONB DEFAULT NULL, -- для select/multiselect + "order" INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS user_rows ( + id SERIAL PRIMARY KEY, + table_id INTEGER NOT NULL REFERENCES user_tables(id) ON DELETE CASCADE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS user_cell_values ( + id SERIAL PRIMARY KEY, + row_id INTEGER NOT NULL REFERENCES user_rows(id) ON DELETE CASCADE, + column_id INTEGER NOT NULL REFERENCES user_columns(id) ON DELETE CASCADE, + value TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(row_id, column_id) +); \ No newline at end of file diff --git a/backend/middleware/errorHandler.js b/backend/middleware/errorHandler.js index 0644c5d..fbc8b00 100644 --- a/backend/middleware/errorHandler.js +++ b/backend/middleware/errorHandler.js @@ -5,13 +5,12 @@ const { ERROR_CODES } = require('../utils/constants'); /** * Middleware для обработки ошибок */ -// eslint-disable-next-line no-unused-vars -const errorHandler = (err, req, res, /* next */) => { +const errorHandler = (err, req, res, next) => { + console.log('errorHandler called, arguments:', arguments); + console.log('typeof res:', typeof res, 'isFunction:', typeof res === 'function'); console.error('errorHandler: err =', err); console.error('errorHandler: typeof err =', typeof err); console.error('errorHandler: stack =', err && err.stack); - console.log('errorHandler called, typeof res:', typeof res, 'res:', res); - console.log('typeof res:', typeof res, 'isFunction:', typeof res === 'function'); // Логируем ошибку logger.error(`Error: ${err.message}`, { stack: err.stack, diff --git a/backend/nodemon.json b/backend/nodemon.json index e493199..29bab23 100644 --- a/backend/nodemon.json +++ b/backend/nodemon.json @@ -5,6 +5,5 @@ "env": { "NODE_ENV": "development" }, - "ext": "js,json,env", - "exec": "node server.js" + "ext": "js,json,env" } diff --git a/backend/routes/tables.js b/backend/routes/tables.js new file mode 100644 index 0000000..e207244 --- /dev/null +++ b/backend/routes/tables.js @@ -0,0 +1,220 @@ +const express = require('express'); +const router = express.Router(); +const db = require('../db'); +const { requireAuth } = require('../middleware/auth'); + +router.use((req, res, next) => { + console.log('Tables router received:', req.method, req.originalUrl); + next(); +}); + +// Получить список всех таблиц (доступно всем) +router.get('/', async (req, res, next) => { + try { + const result = await db.getQuery()('SELECT * FROM user_tables ORDER BY id'); + res.json(result.rows); + } catch (err) { + next(err); + } +}); + +// Создать новую таблицу (доступно всем) +router.post('/', async (req, res, next) => { + try { + const { name, description } = req.body; + const result = await db.getQuery()( + 'INSERT INTO user_tables (name, description) VALUES ($1, $2) RETURNING *', + [name, description || null] + ); + res.json(result.rows[0]); + } catch (err) { + next(err); + } +}); + +// Получить структуру и данные таблицы (доступно всем) +router.get('/:id', async (req, res, next) => { + try { + const tableId = req.params.id; + const columns = (await db.getQuery()('SELECT * FROM user_columns WHERE table_id = $1 ORDER BY "order" ASC, id ASC', [tableId])).rows; + const rows = (await db.getQuery()('SELECT * FROM user_rows WHERE table_id = $1 ORDER BY id', [tableId])).rows; + const cellValues = (await db.getQuery()('SELECT * FROM user_cell_values WHERE row_id IN (SELECT id FROM user_rows WHERE table_id = $1)', [tableId])).rows; + res.json({ columns, rows, cellValues }); + } catch (err) { + next(err); + } +}); + +// Добавить столбец (доступно всем) +router.post('/:id/columns', async (req, res, next) => { + try { + const tableId = req.params.id; + const { name, type, options, order } = req.body; + const result = await db.getQuery()( + 'INSERT INTO user_columns (table_id, name, type, options, "order") VALUES ($1, $2, $3, $4, $5) RETURNING *', + [tableId, name, type, options ? JSON.stringify(options) : null, order || 0] + ); + res.json(result.rows[0]); + } catch (err) { + next(err); + } +}); + +// Добавить строку (доступно всем) +router.post('/:id/rows', async (req, res, next) => { + try { + const tableId = req.params.id; + const result = await db.getQuery()( + 'INSERT INTO user_rows (table_id) VALUES ($1) RETURNING *', + [tableId] + ); + res.json(result.rows[0]); + } catch (err) { + next(err); + } +}); + +// Изменить значение ячейки (доступно всем) +router.patch('/cell/:cellId', async (req, res, next) => { + try { + const cellId = req.params.cellId; + const { value } = req.body; + const result = await db.getQuery()( + 'UPDATE user_cell_values SET value = $1, updated_at = NOW() WHERE id = $2 RETURNING *', + [value, cellId] + ); + res.json(result.rows[0]); + } catch (err) { + next(err); + } +}); + +// Создать/обновить значение ячейки (upsert) (доступно всем) +router.post('/cell', async (req, res, next) => { + try { + const { row_id, column_id, value } = req.body; + const result = await db.getQuery()( + `INSERT INTO user_cell_values (row_id, column_id, value) VALUES ($1, $2, $3) + ON CONFLICT (row_id, column_id) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW() + RETURNING *`, + [row_id, column_id, value] + ); + res.json(result.rows[0]); + } catch (err) { + next(err); + } +}); + +// Удалить строку (доступно всем) +router.delete('/row/:rowId', async (req, res, next) => { + try { + const rowId = req.params.rowId; + await db.getQuery()('DELETE FROM user_rows WHERE id = $1', [rowId]); + res.json({ success: true }); + } catch (err) { + next(err); + } +}); + +// Удалить столбец (доступно всем) +router.delete('/column/:columnId', async (req, res, next) => { + try { + const columnId = req.params.columnId; + await db.getQuery()('DELETE FROM user_columns WHERE id = $1', [columnId]); + res.json({ success: true }); + } catch (err) { + next(err); + } +}); + +// PATCH для обновления столбца (доступно всем) +router.patch('/column/:columnId', async (req, res, next) => { + try { + const columnId = req.params.columnId; + const { name, type, options, order } = req.body; + + // Построение динамического запроса + const updates = []; + const values = []; + let paramIndex = 1; + + if (name !== undefined) { + updates.push(`name = $${paramIndex++}`); + values.push(name); + } + if (type !== undefined) { + updates.push(`type = $${paramIndex++}`); + values.push(type); + } + if (options !== undefined) { + updates.push(`options = $${paramIndex++}`); + values.push(options ? JSON.stringify(options) : null); + } + if (order !== undefined) { + updates.push(`"order" = $${paramIndex++}`); + values.push(order); + } + + if (updates.length === 0) { + return res.status(400).json({ error: 'No fields to update' }); + } + + updates.push(`updated_at = NOW()`); + values.push(columnId); + + const query = `UPDATE user_columns SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING *`; + const result = await db.getQuery()(query, values); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Column not found' }); + } + + res.json(result.rows[0]); + } catch (err) { + next(err); + } +}); + +// PATCH: обновить название/описание таблицы (доступно всем) +router.patch('/:id', async (req, res, next) => { + try { + const tableId = req.params.id; + const { name, description } = req.body; + const result = await db.getQuery()( + `UPDATE user_tables SET + name = COALESCE($1, name), + description = COALESCE($2, description), + updated_at = NOW() + WHERE id = $3 RETURNING *`, + [name, description, tableId] + ); + res.json(result.rows[0]); + } catch (err) { + next(err); + } +}); + +// DELETE: удалить таблицу и каскадно все связанные строки/столбцы/ячейки (доступно всем) +router.delete('/:id', requireAuth, async (req, res, next) => { + try { + const tableId = Number(req.params.id); + console.log('Backend: typeof tableId:', typeof tableId, 'value:', tableId); + // Проверяем, существует ли таблица + const checkResult = await db.getQuery()('SELECT id, name FROM user_tables WHERE id = $1', [tableId]); + console.log('Backend: Table check result:', checkResult.rows); + if (checkResult.rows.length === 0) { + console.log('Backend: Table not found'); + return res.status(404).json({ error: 'Table not found' }); + } + // Удаляем только основную таблицу - каскадное удаление сработает автоматически + console.log('Backend: Executing DELETE query for table_id:', tableId); + const result = await db.getQuery()('DELETE FROM user_tables WHERE id = $1', [tableId]); + console.log('Backend: Delete result - rowCount:', result.rowCount); + res.json({ success: true, deleted: result.rowCount }); + } catch (err) { + console.error('Backend: Error deleting table:', err); + next(err); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 8c02ab6..46b09c8 100644 --- a/backend/server.js +++ b/backend/server.js @@ -15,6 +15,8 @@ const pgSession = require('connect-pg-simple')(session); const authService = require('./services/auth-service'); 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; @@ -94,6 +96,7 @@ 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) => { @@ -122,4 +125,6 @@ process.on('uncaughtException', (err) => { logger.error('Uncaught Exception:', err); }); +app.use(errorHandler); + module.exports = app; diff --git a/docker-compose.yml b/docker-compose.yml index e8c234c..3e0bf22 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3.8' +# version: '3.8' services: postgres: @@ -60,7 +60,8 @@ services: - FRONTEND_URL=http://localhost:5173 ports: - "8000:8000" - command: sh -c "yarn run dev" + # command: sh -c "yarn run dev" # Временно комментируем эту строку + # command: nodemon server.js # Запускаем через nodemon frontend: build: diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 07e0786..71fa492 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -20,7 +20,7 @@ \ No newline at end of file diff --git a/frontend/src/components/cells/CellDate.vue b/frontend/src/components/cells/CellDate.vue new file mode 100644 index 0000000..c8b4fb3 --- /dev/null +++ b/frontend/src/components/cells/CellDate.vue @@ -0,0 +1,16 @@ + + {{ formatted }} + + + \ No newline at end of file diff --git a/frontend/src/components/cells/CellNumber.vue b/frontend/src/components/cells/CellNumber.vue new file mode 100644 index 0000000..4d5a02b --- /dev/null +++ b/frontend/src/components/cells/CellNumber.vue @@ -0,0 +1,9 @@ + + {{ value }} + + + \ No newline at end of file diff --git a/frontend/src/components/cells/CellSelect.vue b/frontend/src/components/cells/CellSelect.vue new file mode 100644 index 0000000..cdf2553 --- /dev/null +++ b/frontend/src/components/cells/CellSelect.vue @@ -0,0 +1,20 @@ + + + {{ v }} + + {{ value }} + + + \ No newline at end of file diff --git a/frontend/src/components/cells/CellText.vue b/frontend/src/components/cells/CellText.vue new file mode 100644 index 0000000..37ebb8b --- /dev/null +++ b/frontend/src/components/cells/CellText.vue @@ -0,0 +1,9 @@ + + {{ value }} + + + \ No newline at end of file diff --git a/frontend/src/components/identity/EmailConnect.vue b/frontend/src/components/identity/EmailConnect.vue index e4023c4..7f95773 100644 --- a/frontend/src/components/identity/EmailConnect.vue +++ b/frontend/src/components/identity/EmailConnect.vue @@ -38,10 +38,10 @@ + + \ No newline at end of file diff --git a/frontend/src/components/tables/DynamicTableEditor.vue b/frontend/src/components/tables/DynamicTableEditor.vue new file mode 100644 index 0000000..3fcd0a4 --- /dev/null +++ b/frontend/src/components/tables/DynamicTableEditor.vue @@ -0,0 +1,276 @@ + + + + + + × + + Загрузка... + + + + + Добавить столбец + Добавить строку + + + + + {{ col.name }} + Действия + + + + + + + + {{ opt }} + + + + + + + + Удалить + + + + + Нет данных. Добавьте столбцы и строки. + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/tables/DynamicTablesModal.vue b/frontend/src/components/tables/DynamicTablesModal.vue new file mode 100644 index 0000000..5d75ff9 --- /dev/null +++ b/frontend/src/components/tables/DynamicTablesModal.vue @@ -0,0 +1,100 @@ + + + + Пользовательские таблицы + × + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/tables/SelectOptionsEditor.vue b/frontend/src/components/tables/SelectOptionsEditor.vue new file mode 100644 index 0000000..9ad3d0a --- /dev/null +++ b/frontend/src/components/tables/SelectOptionsEditor.vue @@ -0,0 +1,68 @@ + + + Опции для select + + + + × + + + Добавить опцию + + + + + + \ No newline at end of file diff --git a/frontend/src/components/tables/TableColumnsDraggable.vue b/frontend/src/components/tables/TableColumnsDraggable.vue new file mode 100644 index 0000000..5ed55da --- /dev/null +++ b/frontend/src/components/tables/TableColumnsDraggable.vue @@ -0,0 +1,104 @@ + + + + + + Текст + Список + + × + Опции + + ← + → + + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/tables/UserTablesList.vue b/frontend/src/components/tables/UserTablesList.vue new file mode 100644 index 0000000..8dad759 --- /dev/null +++ b/frontend/src/components/tables/UserTablesList.vue @@ -0,0 +1,167 @@ + + + + Пользовательские таблицы + Создать таблицу + + Загрузка... + + Нет таблиц. Создайте первую! + + + + + {{ table.name }} + + Открыть + Переименовать + Удалить + + + + + {{ table.description || 'Без описания' }} + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/composables/useAuth.js b/frontend/src/composables/useAuth.js index 78d5cb7..587ee35 100644 --- a/frontend/src/composables/useAuth.js +++ b/frontend/src/composables/useAuth.js @@ -1,474 +1,492 @@ -import { ref, onMounted, onUnmounted } from 'vue'; +import { ref, onMounted, onUnmounted, provide, inject } from 'vue'; import axios from '../api/axios'; -export function useAuth() { - const isAuthenticated = ref(false); - const authType = ref(null); - const userId = ref(null); - const address = ref(null); - const telegramId = ref(null); - const isAdmin = ref(false); - const email = ref(null); - const processedGuestIds = ref([]); - const identities = ref([]); - const tokenBalances = ref([]); +// === SINGLETON STATE === +const isAuthenticated = ref(false); +const authType = ref(null); +const userId = ref(null); +const address = ref(null); +const telegramId = ref(null); +const isAdmin = ref(false); +const email = ref(null); +const processedGuestIds = ref([]); +const identities = ref([]); +const tokenBalances = ref([]); - // Функция для обновления списка идентификаторов - const updateIdentities = async () => { - if (!isAuthenticated.value || !userId.value) return; +// Функция для обновления списка идентификаторов +const updateIdentities = async () => { + if (!isAuthenticated.value || !userId.value) return; - try { - const response = await axios.get('/api/auth/identities'); - if (response.data.success) { - // Фильтруем идентификаторы: убираем гостевые и оставляем только уникальные - const filteredIdentities = response.data.identities - .filter((identity) => identity.provider !== 'guest') - .reduce((acc, identity) => { - // Для каждого типа провайдера оставляем только один идентификатор - const existingIdentity = acc.find((i) => i.provider === identity.provider); - if (!existingIdentity) { - acc.push(identity); - } - return acc; - }, []); + try { + const response = await axios.get('/api/auth/identities'); + if (response.data.success) { + // Фильтруем идентификаторы: убираем гостевые и оставляем только уникальные + const filteredIdentities = response.data.identities + .filter((identity) => identity.provider !== 'guest') + .reduce((acc, identity) => { + // Для каждого типа провайдера оставляем только один идентификатор + const existingIdentity = acc.find((i) => i.provider === identity.provider); + if (!existingIdentity) { + acc.push(identity); + } + return acc; + }, []); - // Сравниваем новый отфильтрованный список с текущим значением - const currentProviders = identities.value.map(id => id.provider).sort(); - const newProviders = filteredIdentities.map(id => id.provider).sort(); - - const identitiesChanged = JSON.stringify(currentProviders) !== JSON.stringify(newProviders); + // Сравниваем новый отфильтрованный список с текущим значением + const currentProviders = identities.value.map(id => id.provider).sort(); + const newProviders = filteredIdentities.map(id => id.provider).sort(); + + const identitiesChanged = JSON.stringify(currentProviders) !== JSON.stringify(newProviders); - // Обновляем реактивное значение - identities.value = filteredIdentities; - console.log('User identities updated:', identities.value); + // Обновляем реактивное значение + identities.value = filteredIdentities; + console.log('User identities updated:', identities.value); - // Если список идентификаторов изменился, принудительно проверяем аутентификацию, - // чтобы обновить authType и другие связанные данные (например, telegramId) - if (identitiesChanged) { - console.log('Identities changed, forcing auth check.'); - await checkAuth(); // Вызываем checkAuth для обновления полного состояния - } + // Если список идентификаторов изменился, принудительно проверяем аутентификацию, + // чтобы обновить authType и другие связанные данные (например, telegramId) + if (identitiesChanged) { + console.log('Identities changed, forcing auth check.'); + await checkAuth(); // Вызываем checkAuth для обновления полного состояния } - } catch (error) { - console.error('Error fetching user identities:', error); } - }; + } catch (error) { + console.error('Error fetching user identities:', error); + } +}; - // Периодическое обновление идентификаторов - let identitiesInterval; +// Периодическое обновление идентификаторов +let identitiesInterval; - const startIdentitiesPolling = () => { - if (identitiesInterval) return; - identitiesInterval = setInterval(updateIdentities, 30000); // Обновляем каждые 30 секунд - }; +const startIdentitiesPolling = () => { + if (identitiesInterval) return; + identitiesInterval = setInterval(updateIdentities, 30000); // Обновляем каждые 30 секунд +}; - const stopIdentitiesPolling = () => { - if (identitiesInterval) { - clearInterval(identitiesInterval); - identitiesInterval = null; +const stopIdentitiesPolling = () => { + if (identitiesInterval) { + clearInterval(identitiesInterval); + identitiesInterval = null; + } +}; + +const checkTokenBalances = async (address) => { + try { + const response = await axios.get(`/api/auth/check-tokens/${address}`); + if (response.data.success) { + tokenBalances.value = response.data.balances; + return response.data.balances; } - }; + return null; + } catch (error) { + console.error('Error checking token balances:', error); + return null; + } +}; - const checkTokenBalances = async (address) => { - try { - const response = await axios.get(`/api/auth/check-tokens/${address}`); - if (response.data.success) { - tokenBalances.value = response.data.balances; - return response.data.balances; - } - return null; - } catch (error) { - console.error('Error checking token balances:', error); - return null; - } - }; +const updateAuth = async ({ + authenticated, + authType: newAuthType, + userId: newUserId, + address: newAddress, + telegramId: newTelegramId, + isAdmin: newIsAdmin, + email: newEmail, +}) => { + const wasAuthenticated = isAuthenticated.value; + const previousUserId = userId.value; - const updateAuth = async ({ + console.log('updateAuth called with:', { authenticated, - authType: newAuthType, - userId: newUserId, - address: newAddress, - telegramId: newTelegramId, - isAdmin: newIsAdmin, - email: newEmail, - }) => { - const wasAuthenticated = isAuthenticated.value; - const previousUserId = userId.value; + newAuthType, + newUserId, + newAddress, + newTelegramId, + newIsAdmin, + newEmail, + }); - console.log('updateAuth called with:', { + // Убедимся, что переменные являются реактивными + isAuthenticated.value = authenticated === true; + authType.value = newAuthType || null; + userId.value = newUserId || null; + address.value = newAddress || null; + telegramId.value = newTelegramId || null; + isAdmin.value = newIsAdmin === true; + email.value = newEmail || null; + + // Кэшируем данные аутентификации + localStorage.setItem( + 'authData', + JSON.stringify({ authenticated, - newAuthType, - newUserId, - newAddress, - newTelegramId, - newIsAdmin, - newEmail, - }); + authType: newAuthType, + userId: newUserId, + address: newAddress, + telegramId: newTelegramId, + isAdmin: newIsAdmin, + email: newEmail, + }) + ); - // Убедимся, что переменные являются реактивными - isAuthenticated.value = authenticated === true; - authType.value = newAuthType || null; - userId.value = newUserId || null; - address.value = newAddress || null; - telegramId.value = newTelegramId || null; - isAdmin.value = newIsAdmin === true; - email.value = newEmail || null; + // Если аутентификация через кошелек, проверяем баланс токенов только при изменении адреса + if (authenticated && newAuthType === 'wallet' && newAddress && newAddress !== address.value) { + await checkTokenBalances(newAddress); + } - // Кэшируем данные аутентификации - localStorage.setItem( - 'authData', - JSON.stringify({ - authenticated, - authType: newAuthType, - userId: newUserId, - address: newAddress, - telegramId: newTelegramId, - isAdmin: newIsAdmin, - email: newEmail, - }) - ); + // Обновляем идентификаторы при любом изменении аутентификации + if (authenticated) { + await updateIdentities(); + startIdentitiesPolling(); + } else { + stopIdentitiesPolling(); + identities.value = []; + } - // Если аутентификация через кошелек, проверяем баланс токенов только при изменении адреса - if (authenticated && newAuthType === 'wallet' && newAddress && newAddress !== address.value) { - await checkTokenBalances(newAddress); - } + console.log('Auth updated:', { + authenticated: isAuthenticated.value, + userId: userId.value, + address: address.value, + telegramId: telegramId.value, + email: email.value, + isAdmin: isAdmin.value, + }); - // Обновляем идентификаторы при любом изменении аутентификации - if (authenticated) { - await updateIdentities(); - startIdentitiesPolling(); - } else { - stopIdentitiesPolling(); - identities.value = []; - } + // Если пользователь только что аутентифицировался или сменил аккаунт, + // пробуем связать сообщения + if (authenticated && (!wasAuthenticated || (previousUserId && previousUserId !== newUserId))) { + console.log('Auth change detected, linking messages'); + linkMessages(); + } +}; - console.log('Auth updated:', { - authenticated: isAuthenticated.value, - userId: userId.value, - address: address.value, - telegramId: telegramId.value, - email: email.value, - isAdmin: isAdmin.value, - }); +// Функция для связывания сообщений после успешной авторизации +const linkMessages = async () => { + try { + if (isAuthenticated.value) { + console.log('Linking messages after authentication'); - // Если пользователь только что аутентифицировался или сменил аккаунт, - // пробуем связать сообщения - if (authenticated && (!wasAuthenticated || (previousUserId && previousUserId !== newUserId))) { - console.log('Auth change detected, linking messages'); - linkMessages(); - } - }; + // Проверка, есть ли гостевой ID для обработки + const localGuestId = localStorage.getItem('guestId'); - // Функция для связывания сообщений после успешной авторизации - const linkMessages = async () => { - try { - if (isAuthenticated.value) { - console.log('Linking messages after authentication'); + // Если гостевого ID нет или он уже был обработан, пропускаем запрос + if (!localGuestId || processedGuestIds.value.includes(localGuestId)) { + console.log('No new guest IDs to process or already processed'); + return { + success: true, + message: 'No new guest IDs to process', + processedIds: processedGuestIds.value, + }; + } - // Проверка, есть ли гостевой ID для обработки - const localGuestId = localStorage.getItem('guestId'); + // Создаем объект с идентификаторами для передачи на сервер + const identifiersData = { + userId: userId.value, + guestId: localGuestId, + }; + + // Добавляем все доступные идентификаторы + if (address.value) identifiersData.address = address.value; + if (email.value) identifiersData.email = email.value; + if (telegramId.value) identifiersData.telegramId = telegramId.value; + + console.log('Sending link-guest-messages request with data:', identifiersData); + + /* Удаляем ненужный вызов + try { + // Отправляем запрос на связывание сообщений + const response = await axios.post('/api/auth/link-guest-messages', identifiersData); + + if (response.data.success) { + console.log('Messages linked successfully:', response.data); + + // Обновляем список обработанных guestIds из ответа сервера + if (response.data.processedIds && Array.isArray(response.data.processedIds)) { + processedGuestIds.value = [...response.data.processedIds]; + console.log('Updated processed guest IDs from server:', processedGuestIds.value); + } + // В качестве запасного варианта также обрабатываем старый формат ответа + else if (response.data.results && Array.isArray(response.data.results)) { + const newProcessedIds = response.data.results + .filter((result) => result.guestId) + .map((result) => result.guestId); + + if (newProcessedIds.length > 0) { + processedGuestIds.value = [ + ...new Set([...processedGuestIds.value, ...newProcessedIds]), + ]; + console.log('Updated processed guest IDs from results:', processedGuestIds.value); + } + } + + // Очищаем гостевые сообщения из localStorage после успешного связывания + localStorage.removeItem('guestMessages'); + localStorage.removeItem('guestId'); - // Если гостевого ID нет или он уже был обработан, пропускаем запрос - if (!localGuestId || processedGuestIds.value.includes(localGuestId)) { - console.log('No new guest IDs to process or already processed'); return { success: true, - message: 'No new guest IDs to process', processedIds: processedGuestIds.value, }; } - - // Создаем объект с идентификаторами для передачи на сервер - const identifiersData = { - userId: userId.value, - guestId: localGuestId, + } catch (error) { + console.error('Error linking messages:', error); + return { + success: false, + error: error.message, }; + } + */ + // Предполагаем, что бэкенд автоматически связывает сообщения + // Очищаем данные гостя локально + console.log('Assuming backend handles message linking. Clearing local guest data.'); + localStorage.removeItem('guestMessages'); + localStorage.removeItem('guestId'); + // Добавляем текущий guestId в обработанные, чтобы не пытаться отправить его снова + if(localGuestId) { + updateProcessedGuestIds([localGuestId]); + } + return { success: true, message: 'Local guest data cleared.' }; - // Добавляем все доступные идентификаторы - if (address.value) identifiersData.address = address.value; - if (email.value) identifiersData.email = email.value; - if (telegramId.value) identifiersData.telegramId = telegramId.value; + } - console.log('Sending link-guest-messages request with data:', identifiersData); + return { success: false, message: 'Not authenticated' }; + } catch (error) { + console.error('Error in linkMessages:', error); + return { success: false, error: error.message }; + } +}; - /* Удаляем ненужный вызов - try { - // Отправляем запрос на связывание сообщений - const response = await axios.post('/api/auth/link-guest-messages', identifiersData); +const checkAuth = async () => { + try { + const response = await axios.get('/api/auth/check'); + console.log('Auth check response:', response.data); - if (response.data.success) { - console.log('Messages linked successfully:', response.data); + const wasAuthenticated = isAuthenticated.value; + const previousUserId = userId.value; + const previousAuthType = authType.value; - // Обновляем список обработанных guestIds из ответа сервера - if (response.data.processedIds && Array.isArray(response.data.processedIds)) { - processedGuestIds.value = [...response.data.processedIds]; - console.log('Updated processed guest IDs from server:', processedGuestIds.value); - } - // В качестве запасного варианта также обрабатываем старый формат ответа - else if (response.data.results && Array.isArray(response.data.results)) { - const newProcessedIds = response.data.results - .filter((result) => result.guestId) - .map((result) => result.guestId); + // Обновляем данные авторизации через updateAuth вместо прямого изменения + await updateAuth({ + authenticated: response.data.authenticated, + authType: response.data.authType, + userId: response.data.userId, + address: response.data.address, + telegramId: response.data.telegramId, + email: response.data.email, + isAdmin: response.data.isAdmin, + }); - if (newProcessedIds.length > 0) { - processedGuestIds.value = [ - ...new Set([...processedGuestIds.value, ...newProcessedIds]), - ]; - console.log('Updated processed guest IDs from results:', processedGuestIds.value); - } - } + // Если пользователь аутентифицирован, обновляем список идентификаторов и связываем сообщения + if (response.data.authenticated) { + // Сначала обновляем идентификаторы, чтобы иметь актуальные данные + await updateIdentities(); - // Очищаем гостевые сообщения из localStorage после успешного связывания - localStorage.removeItem('guestMessages'); - localStorage.removeItem('guestId'); + // Если пользователь только что аутентифицировался или сменил аккаунт, + // связываем гостевые сообщения с его аккаунтом + if (!wasAuthenticated || (previousUserId && previousUserId !== response.data.userId)) { + // Немедленно связываем сообщения + const linkResult = await linkMessages(); + console.log('Link messages result on auth change:', linkResult); - return { - success: true, - processedIds: processedGuestIds.value, - }; - } - } catch (error) { - console.error('Error linking messages:', error); - return { - success: false, - error: error.message, - }; + // Если пользователь только что аутентифицировался через Telegram, + // обновляем историю чата без перезагрузки страницы + if (response.data.authType === 'telegram' && previousAuthType !== 'telegram') { + console.log('Telegram auth detected, loading message history'); + // Отправляем событие для загрузки истории чата + window.dispatchEvent(new CustomEvent('load-chat-history')); } - */ - // Предполагаем, что бэкенд автоматически связывает сообщения - // Очищаем данные гостя локально - console.log('Assuming backend handles message linking. Clearing local guest data.'); - localStorage.removeItem('guestMessages'); - localStorage.removeItem('guestId'); - // Добавляем текущий guestId в обработанные, чтобы не пытаться отправить его снова - if(localGuestId) { - updateProcessedGuestIds([localGuestId]); - } - return { success: true, message: 'Local guest data cleared.' }; - } - return { success: false, message: 'Not authenticated' }; - } catch (error) { - console.error('Error in linkMessages:', error); - return { success: false, error: error.message }; - } - }; - - const checkAuth = async () => { - try { - const response = await axios.get('/api/auth/check'); - console.log('Auth check response:', response.data); - - const wasAuthenticated = isAuthenticated.value; - const previousUserId = userId.value; - const previousAuthType = authType.value; - - // Обновляем данные авторизации через updateAuth вместо прямого изменения - await updateAuth({ - authenticated: response.data.authenticated, - authType: response.data.authType, - userId: response.data.userId, - address: response.data.address, - telegramId: response.data.telegramId, - email: response.data.email, - isAdmin: response.data.isAdmin, - }); - - // Если пользователь аутентифицирован, обновляем список идентификаторов и связываем сообщения - if (response.data.authenticated) { - // Сначала обновляем идентификаторы, чтобы иметь актуальные данные - await updateIdentities(); - - // Если пользователь только что аутентифицировался или сменил аккаунт, - // связываем гостевые сообщения с его аккаунтом - if (!wasAuthenticated || (previousUserId && previousUserId !== response.data.userId)) { - // Немедленно связываем сообщения - const linkResult = await linkMessages(); - console.log('Link messages result on auth change:', linkResult); - - // Если пользователь только что аутентифицировался через Telegram, - // обновляем историю чата без перезагрузки страницы - if (response.data.authType === 'telegram' && previousAuthType !== 'telegram') { - console.log('Telegram auth detected, loading message history'); - // Отправляем событие для загрузки истории чата - window.dispatchEvent(new CustomEvent('load-chat-history')); - } - } - - // Обновляем отображение подключенного состояния в UI - updateConnectionDisplay(true, response.data.authType, response.data); - } else { - // Обновляем отображение отключенного состояния - updateConnectionDisplay(false); - } - - return response.data; - } catch (error) { - console.error('Error checking auth:', error); - // В случае ошибки сбрасываем состояние аутентификации - updateConnectionDisplay(false); - return { authenticated: false }; - } - }; - - const disconnect = async () => { - try { - // Удаляем все идентификаторы перед выходом - await axios.post('/api/auth/logout'); - - // Обновляем состояние в памяти - updateAuth({ - authenticated: false, - authType: null, - userId: null, - address: null, - telegramId: null, - email: null, - isAdmin: false, - }); - + // Обновляем отображение подключенного состояния в UI + updateConnectionDisplay(true, response.data.authType, response.data); + } else { // Обновляем отображение отключенного состояния updateConnectionDisplay(false); + } - // Очищаем списки идентификаторов - identities.value = []; - processedGuestIds.value = []; + return response.data; + } catch (error) { + console.error('Error checking auth:', error); + // В случае ошибки сбрасываем состояние аутентификации + updateConnectionDisplay(false); + return { authenticated: false }; + } +}; - // Очищаем localStorage полностью - localStorage.removeItem('isAuthenticated'); - localStorage.removeItem('userId'); - localStorage.removeItem('address'); - localStorage.removeItem('isAdmin'); - localStorage.removeItem('guestId'); - localStorage.removeItem('guestMessages'); - localStorage.removeItem('telegramId'); - localStorage.removeItem('email'); +const disconnect = async () => { + try { + // Удаляем все идентификаторы перед выходом + await axios.post('/api/auth/logout'); - // Удаляем класс подключенного кошелька + // Обновляем состояние в памяти + updateAuth({ + authenticated: false, + authType: null, + userId: null, + address: null, + telegramId: null, + email: null, + isAdmin: false, + }); + + // Обновляем отображение отключенного состояния + updateConnectionDisplay(false); + + // Очищаем списки идентификаторов + identities.value = []; + processedGuestIds.value = []; + + // Очищаем localStorage полностью + localStorage.removeItem('isAuthenticated'); + localStorage.removeItem('userId'); + localStorage.removeItem('address'); + localStorage.removeItem('isAdmin'); + localStorage.removeItem('guestId'); + localStorage.removeItem('guestMessages'); + localStorage.removeItem('telegramId'); + localStorage.removeItem('email'); + + // Удаляем класс подключенного кошелька + document.body.classList.remove('wallet-connected'); + + console.log('User disconnected successfully and all identifiers cleared'); + + return { success: true }; + } catch (error) { + console.error('Error disconnecting:', error); + return { success: false, error: error.message }; + } +}; + +// Обновляем список обработанных guestIds +const updateProcessedGuestIds = (ids) => { + if (Array.isArray(ids)) { + processedGuestIds.value = [...new Set([...processedGuestIds.value, ...ids])].slice(-20); + } +}; + +// Функция для обновления отображения подключения в UI +const updateConnectionDisplay = (isConnected, authType, authData = {}) => { + try { + console.log('Updating connection display:', { isConnected, authType, authData }); + + if (isConnected) { + document.body.classList.add('wallet-connected'); + + const authDisplayEl = document.getElementById('auth-display'); + if (authDisplayEl) { + let displayText = 'Подключено'; + + if (authType === 'wallet' && authData.address) { + const shortAddress = `${authData.address.substring(0, 6)}...${authData.address.substring(authData.address.length - 4)}`; + displayText = `Кошелек: ${shortAddress}`; + } else if (authType === 'email' && authData.email) { + displayText = `Email: ${authData.email}`; + } else if (authType === 'telegram' && authData.telegramId) { + displayText = `Telegram: ${authData.telegramUsername || authData.telegramId}`; + } + + authDisplayEl.innerHTML = displayText; + authDisplayEl.style.display = 'inline-block'; + } + + // Скрываем кнопки авторизации и показываем кнопку выхода + const authButtonsEl = document.getElementById('auth-buttons'); + const logoutButtonEl = document.getElementById('logout-button'); + + if (authButtonsEl) authButtonsEl.style.display = 'none'; + if (logoutButtonEl) logoutButtonEl.style.display = 'inline-block'; + } else { document.body.classList.remove('wallet-connected'); - console.log('User disconnected successfully and all identifiers cleared'); - - return { success: true }; - } catch (error) { - console.error('Error disconnecting:', error); - return { success: false, error: error.message }; - } - }; - - // Обновляем список обработанных guestIds - const updateProcessedGuestIds = (ids) => { - if (Array.isArray(ids)) { - processedGuestIds.value = [...new Set([...processedGuestIds.value, ...ids])]; - } - }; - - // Функция для обновления отображения подключения в UI - const updateConnectionDisplay = (isConnected, authType, authData = {}) => { - try { - console.log('Updating connection display:', { isConnected, authType, authData }); - - if (isConnected) { - document.body.classList.add('wallet-connected'); - - const authDisplayEl = document.getElementById('auth-display'); - if (authDisplayEl) { - let displayText = 'Подключено'; - - if (authType === 'wallet' && authData.address) { - const shortAddress = `${authData.address.substring(0, 6)}...${authData.address.substring(authData.address.length - 4)}`; - displayText = `Кошелек: ${shortAddress}`; - } else if (authType === 'email' && authData.email) { - displayText = `Email: ${authData.email}`; - } else if (authType === 'telegram' && authData.telegramId) { - displayText = `Telegram: ${authData.telegramUsername || authData.telegramId}`; - } - - authDisplayEl.innerHTML = displayText; - authDisplayEl.style.display = 'inline-block'; - } - - // Скрываем кнопки авторизации и показываем кнопку выхода - const authButtonsEl = document.getElementById('auth-buttons'); - const logoutButtonEl = document.getElementById('logout-button'); - - if (authButtonsEl) authButtonsEl.style.display = 'none'; - if (logoutButtonEl) logoutButtonEl.style.display = 'inline-block'; - } else { - document.body.classList.remove('wallet-connected'); - - // Скрываем отображение аутентификации - const authDisplayEl = document.getElementById('auth-display'); - if (authDisplayEl) { - authDisplayEl.style.display = 'none'; - } - - // Показываем кнопки авторизации и скрываем кнопку выхода - const authButtonsEl = document.getElementById('auth-buttons'); - const logoutButtonEl = document.getElementById('logout-button'); - - if (authButtonsEl) authButtonsEl.style.display = 'flex'; - if (logoutButtonEl) logoutButtonEl.style.display = 'none'; + // Скрываем отображение аутентификации + const authDisplayEl = document.getElementById('auth-display'); + if (authDisplayEl) { + authDisplayEl.style.display = 'none'; } - } catch (error) { - console.error('Error updating connection display:', error); + + // Показываем кнопки авторизации и скрываем кнопку выхода + const authButtonsEl = document.getElementById('auth-buttons'); + const logoutButtonEl = document.getElementById('logout-button'); + + if (authButtonsEl) authButtonsEl.style.display = 'flex'; + if (logoutButtonEl) logoutButtonEl.style.display = 'none'; } - }; + } catch (error) { + console.error('Error updating connection display:', error); + } +}; - onMounted(async () => { - await checkAuth(); +onMounted(async () => { + await checkAuth(); +}); + +// Очищаем интервал при размонтировании компонента +onUnmounted(() => { + stopIdentitiesPolling(); +}); + +/** + * Связывает новый идентификатор с текущим аккаунтом пользователя + * @param {string} type - Тип идентификатора (wallet, email, telegram) + * @param {string} value - Значение идентификатора + * @returns {Promise} - Результат операции + */ +const linkIdentity = async (type, value) => { + const response = await axios.post('/api/link', { + type, + value, }); + return response.data; +}; - // Очищаем интервал при размонтировании компонента - onUnmounted(() => { - stopIdentitiesPolling(); - }); +/** + * Удаляет идентификатор пользователя + * @param {string} provider - Тип идентификатора (wallet, email, telegram) + * @param {string} providerId - Значение идентификатора + * @returns {Promise} - Результат операции + */ +const deleteIdentity = async (provider, providerId) => { + const response = await axios.delete(`/api/${provider}/${encodeURIComponent(providerId)}`); + return response.data; +}; - /** - * Связывает новый идентификатор с текущим аккаунтом пользователя - * @param {string} type - Тип идентификатора (wallet, email, telegram) - * @param {string} value - Значение идентификатора - * @returns {Promise} - Результат операции - */ - const linkIdentity = async (type, value) => { - const response = await axios.post('/api/link', { - type, - value, - }); - return response.data; - }; +// === SINGLETON API === +const authApi = { + isAuthenticated, + authType, + userId, + address, + isAdmin, + telegramId, + email, + identities, + processedGuestIds, + tokenBalances, + updateAuth, + checkAuth, + disconnect, + linkMessages, + updateIdentities, + updateProcessedGuestIds, + updateConnectionDisplay, + linkIdentity, + deleteIdentity, +}; - /** - * Удаляет идентификатор пользователя - * @param {string} provider - Тип идентификатора (wallet, email, telegram) - * @param {string} providerId - Значение идентификатора - * @returns {Promise} - Результат операции - */ - const deleteIdentity = async (provider, providerId) => { - const response = await axios.delete(`/api/${provider}/${encodeURIComponent(providerId)}`); - return response.data; - }; +// === PROVIDE/INJECT HELPERS === +const AUTH_KEY = Symbol('auth'); - return { - isAuthenticated, - authType, - userId, - address, - isAdmin, - telegramId, - email, - identities, - processedGuestIds, - tokenBalances, - updateAuth, - checkAuth, - disconnect, - linkMessages, - updateIdentities, - updateProcessedGuestIds, - updateConnectionDisplay, - linkIdentity, - deleteIdentity, - }; +export function provideAuth() { + provide(AUTH_KEY, authApi); +} + +export function useAuthContext() { + const ctx = inject(AUTH_KEY); + if (!ctx) throw new Error('Auth context not provided!'); + return ctx; +} + +// === useAuth теперь просто возвращает singleton === +export function useAuth() { + return authApi; } diff --git a/frontend/src/composables/useAuthFlow.js b/frontend/src/composables/useAuthFlow.js index c6781cc..6fd5fbc 100644 --- a/frontend/src/composables/useAuthFlow.js +++ b/frontend/src/composables/useAuthFlow.js @@ -1,12 +1,12 @@ import { ref, onUnmounted } from 'vue'; import api from '../api/axios'; -import { useAuth } from './useAuth'; +import { useAuthContext } from './useAuth'; import { useNotifications } from './useNotifications'; export function useAuthFlow(options = {}) { const { onAuthSuccess } = options; // Callback после успешной аутентификации/привязки - const auth = useAuth(); + const auth = useAuthContext(); const { showSuccessMessage, showErrorMessage } = useNotifications(); // Состояния Telegram diff --git a/frontend/src/composables/useTokenBalances.js b/frontend/src/composables/useTokenBalances.js index 021dfca..9af9286 100644 --- a/frontend/src/composables/useTokenBalances.js +++ b/frontend/src/composables/useTokenBalances.js @@ -1,10 +1,10 @@ import { ref, watch, onUnmounted } from 'vue'; import { fetchTokenBalances } from '../services/tokens'; -import { useAuth } from './useAuth'; // Предполагаем, что useAuth предоставляет identities +import { useAuthContext } from './useAuth'; // Предполагаем, что useAuth предоставляет identities import eventBus from '../utils/eventBus'; export function useTokenBalances() { - const auth = useAuth(); // Получаем доступ к состоянию аутентификации + const auth = useAuthContext(); // Получаем доступ к состоянию аутентификации const tokenBalances = ref([]); // теперь массив объектов const isLoadingTokens = ref(false); let balanceUpdateInterval = null; diff --git a/frontend/src/services/tablesService.js b/frontend/src/services/tablesService.js new file mode 100644 index 0000000..62dec75 --- /dev/null +++ b/frontend/src/services/tablesService.js @@ -0,0 +1,57 @@ +import axios from 'axios'; + +const api = '/api/tables'; + +export default { + async getTables() { + const res = await axios.get(`${api}?_t=${Date.now()}`); + return res.data; + }, + async createTable(data) { + const res = await axios.post(api, data); + return res.data; + }, + async getTable(id) { + const res = await axios.get(`${api}/${id}`); + return res.data; + }, + async addColumn(tableId, data) { + const res = await axios.post(`${api}/${tableId}/columns`, data); + return res.data; + }, + async addRow(tableId) { + const res = await axios.post(`${api}/${tableId}/rows`); + return res.data; + }, + async saveCell(data) { + const res = await axios.post(`${api}/cell`, data); + return res.data; + }, + async deleteColumn(columnId) { + const res = await axios.delete(`${api}/column/${columnId}`); + return res.data; + }, + async deleteRow(rowId) { + const res = await axios.delete(`${api}/row/${rowId}`); + return res.data; + }, + async updateColumn(columnId, data) { + const res = await axios.patch(`${api}/column/${columnId}`, data); + return res.data; + }, + async updateTable(id, data) { + const res = await axios.patch(`${api}/${id}`, data); + return res.data; + }, + async deleteTable(id) { + console.log('tablesService.deleteTable called with id:', id); + try { + const res = await axios.delete(`${api}/${id}`); + console.log('Delete response:', res.data); + return res.data; + } catch (error) { + console.error('Error in deleteTable service:', error); + throw error; + } + } +}; \ No newline at end of file diff --git a/frontend/src/views/CrmView.vue b/frontend/src/views/CrmView.vue index 1e77589..85ffef5 100644 --- a/frontend/src/views/CrmView.vue +++ b/frontend/src/views/CrmView.vue @@ -22,13 +22,20 @@ + + Таблицы + + Подробнее + + +
{{ table.description || 'Без описания' }}