ваше сообщение коммита

This commit is contained in:
2025-05-23 17:44:28 +03:00
parent 9aa842d238
commit 7de23551f7
24 changed files with 2990 additions and 306 deletions

View File

@@ -98,4 +98,19 @@ docker compose down -v
## Примечания
- Загрузка модели qwen2.5:7b может занять некоторое время в зависимости от скорости интернета
- Для использования GPU Ollama требуются установленные драйверы NVIDIA и nvidia-container-toolkit
- Для использования GPU Ollama требуются установленные драйверы NVIDIA и nvidia-container-toolkit
## Важно! Если в контейнерах нет доступа к интернету
1. Откройте Docker Desktop → Settings → Docker Engine.
2. Добавьте строку:
"dns": ["8.8.8.8", "1.1.1.1"]
Пример:
{
...
"dns": ["8.8.8.8", "1.1.1.1"]
}
3. Нажмите "Apply & Restart".
4. Перезапустите приложение:
docker compose down
docker compose up -d

View File

@@ -1,9 +1,9 @@
FROM node:20-alpine
FROM node:20-bullseye
WORKDIR /app
# Устанавливаем зависимости, включая Python для node-gyp
RUN apk add --no-cache python3 make g++
RUN apt-get update && apt-get install -y python3 make g++ cmake openssl libssl-dev
# Копируем package.json и yarn.lock для установки зависимостей
COPY package.json yarn.lock ./

View File

@@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS ipfs_publications (
id SERIAL PRIMARY KEY,
cid TEXT NOT NULL,
url TEXT NOT NULL,
published_at TIMESTAMP DEFAULT NOW()
);

View File

@@ -1,9 +0,0 @@
# API Documentation
## Authentication
### POST /api/auth/refresh-session
Refreshes the user session.
**Request:**

View File

@@ -1,137 +0,0 @@
# Архитектура идентификаторов пользователей
## Общая структура
### Таблицы для хранения данных пользователей
Система идентификации пользователей построена на следующих таблицах:
1. **users** - Основная таблица пользователей
- `id SERIAL PRIMARY KEY` - Основной идентификатор пользователя
- `status` - Статус пользователя (active, blocked)
- `role` - Роль пользователя (user, admin)
- `created_at`, `updated_at` - Временные метки
- Поля `username`, `email` и `address` являются устаревшими и должны быть NULL
2. **user_identities** - Таблица идентификаторов пользователей
- `id SERIAL PRIMARY KEY` - Идентификатор записи
- `user_id INTEGER REFERENCES users(id)` - Ссылка на пользователя
- `provider VARCHAR(50)` - Тип идентификатора (email, wallet, telegram, username)
- `provider_id VARCHAR(255)` - Значение идентификатора (должно быть в нижнем регистре для email и wallet)
- Уникальный составной ключ `(provider, provider_id)`
- Ограничение `CHECK (provider IN ('email', 'wallet', 'telegram', 'username'))` - запрещает тип 'guest'
3. **guest_user_mapping** - Таблица связи гостевых идентификаторов с пользователями
- `id SERIAL PRIMARY KEY` - Идентификатор записи
- `user_id INTEGER REFERENCES users(id)` - Ссылка на пользователя
- `guest_id VARCHAR(255)` - Гостевой идентификатор
- `processed BOOLEAN` - Флаг обработки гостевых сообщений
- Уникальный ключ `guest_id`
4. **messages** - Таблица сообщений
- `id SERIAL PRIMARY KEY` - Идентификатор сообщения
- `conversation_id INTEGER REFERENCES conversations(id)` - Ссылка на диалог
- `user_id INTEGER REFERENCES users(id)` - Прямая ссылка на пользователя
- `content TEXT` - Содержание сообщения
- `sender_type`, `role` - Тип отправителя и роль (user/assistant)
5. **guest_messages** - Таблица гостевых сообщений
- `id SERIAL PRIMARY KEY` - Идентификатор гостевого сообщения
- `guest_id VARCHAR(255)` - Идентификатор гостя
- `content TEXT` - Содержание сообщения
- `is_ai BOOLEAN` - Флаг, указывающий на сообщение от AI
## Процесс аутентификации и работа с гостевыми сообщениями
### Гостевой доступ
1. Гость (неаутентифицированный пользователь) начинает взаимодействие с системой
2. Для гостя генерируется уникальный `guest_id`, который сохраняется в localStorage браузера
3. Гостевые сообщения сохраняются в таблице `guest_messages` с привязкой к `guest_id`
4. Локально сообщения также хранятся в localStorage
### Аутентификация пользователя
1. Когда гость аутентифицируется (через email, wallet или telegram):
- Создается запись в таблице `users`
- Создается запись в таблице `user_identities` с соответствующим провайдером
- Гостевой ID сохраняется в таблице `guest_user_mapping` (не в user_identities)
2. После аутентификации система автоматически обрабатывает гостевые сообщения:
- Вызывается метод `linkGuestMessages`
- Создается новый диалог для гостевых сообщений
- Гостевые сообщения переносятся в таблицу `messages` с привязкой к пользователю
- Обработанные гостевые сообщения удаляются из `guest_messages`
- Запись в `guest_user_mapping` помечается как `processed = true`
### Объединение пользователей
Если пользователь аутентифицируется разными способами, система может объединить его данные:
1. Система проверяет связанных пользователей через `user_identities`
2. Если находятся связанные пользователи, вызывается метод `migrateUserData`
3. Данные от вторичных аккаунтов мигрируют к основному:
- Идентификаторы в таблице `user_identities`
- Гостевые связи в таблице `guest_user_mapping`
- Сообщения с прямым указанием `user_id`
- Диалоги
- Настройки
## Ограничения и правила
1. Тип провайдера `guest` запрещен в таблице `user_identities` (проверяется ограничением CHECK)
2. Гостевые идентификаторы хранятся только в таблице `guest_user_mapping`
3. Все идентификаторы email и wallet должны храниться в нижнем регистре
4. Таблица `messages` имеет прямую связь с пользователем через поле `user_id`
5. Сообщения всегда связаны с конкретным пользователем и диалогом
6. В таблице `users` поля `username`, `email` и `address` должны быть NULL
## Обработка ошибок
1. Если возникает ошибка при обработке гостевых сообщений, система:
- Логирует ошибку
- Продолжает попытки обработки при следующих авторизациях
- Не удаляет гостевые сообщения до успешной обработки
2. Если гостевые сообщения уже обработаны, повторная обработка пропускается
## Оптимизации
1. Индексы созданы для всех полей, используемых в запросах:
- `user_identities(user_id)`
- `user_identities(provider, provider_id)`
- `guest_user_mapping(guest_id)`
- `guest_user_mapping(user_id)`
- `messages(user_id)`
- `messages(conversation_id)`
2. Триггеры автоматически поддерживают целостность данных:
- Автоматическое заполнение `user_id` в таблице `messages`
- Очистка неиспользуемых полей в таблице `users`
3. Ограничения предотвращают некорректные данные:
- Запрет на использование провайдера `guest` в таблице `user_identities`
- Уникальность `guest_id` в таблице `guest_user_mapping`
- Ограничение допустимых значений для поля `provider`
## Функции для диагностики
1. **verify_migration_017()** - проверяет состояние гостевых идентификаторов
- `guest_identities_count` - количество гостевых идентификаторов в таблице user_identities
- `guest_mapping_count` - количество записей в таблице guest_user_mapping
- `missing_mappings` - количество гостевых ID, которые отсутствуют в guest_user_mapping
2. **verify_identity_data()** - проверяет общее состояние данных идентификаторов
- `users_with_address` - количество пользователей с заполненным полем address
- `users_with_email` - количество пользователей с заполненным полем email
- `wallet_identities` - количество идентификаторов wallet
- `email_identities` - количество идентификаторов email
- `telegram_identities` - количество идентификаторов telegram
- `duplicate_provider_ids` - количество дублирующихся идентификаторов

View File

@@ -1,75 +0,0 @@
# Руководство по миграциям базы данных
## Общая информация
Система миграций базы данных предназначена для поддержания структуры базы данных в актуальном состоянии и обеспечения возможности обновления между версиями приложения.
## Структура миграций
Миграции размещены в папке `backend/db/migrations/` и именуются по схеме `XXX_descriptive_name.sql`, где XXX - порядковый номер миграции.
### Категории миграций
1. **Основные структурные миграции** (001-013) - создание базовых таблиц и первоначальной структуры
2. **Функциональные миграции** - изменения, связанные с конкретными функциями
3. **Рефакторинг и оптимизация** (019+) - улучшение существующей структуры
## Важные миграции
### 019_identity_system_refactor.sql
Комплексная миграция, объединяющая несколько предыдущих миграций (014-018) для улучшения системы идентификации пользователей:
- Создание таблицы `guest_user_mapping` для связи гостевых идентификаторов с пользователями
- Добавление прямой связи между сообщениями и пользователями через поле `user_id`
- Очистка дублирующихся данных между таблицами `users` и `user_identities`
- Нормализация формата идентификаторов (приведение к нижнему регистру)
- Добавление ограничений и триггеров для поддержания целостности данных
## Применение миграций
При развертывании новой версии приложения миграции применяются автоматически через скрипт `backend/db/run-migrations.js`. Порядок применения определяется порядковым номером в имени файла.
## Создание новых миграций
1. **Именование**: Используйте следующий свободный порядковый номер и описательное имя
2. **Идемпотентность**: Миграции должны быть безопасны для повторного выполнения
3. **Проверки**: Добавляйте проверки существования объектов перед их созданием
4. **Тестирование**: Проверяйте миграцию на тестовой базе данных перед применением
Пример правильной идемпотентной миграции:
```sql
-- Создание таблицы, если она не существует
CREATE TABLE IF NOT EXISTS example_table (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL
);
-- Добавление колонки, если она отсутствует
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'example_table' AND column_name = 'new_column'
) THEN
ALTER TABLE example_table ADD COLUMN new_column INTEGER;
END IF;
END $$;
```
## Архивация устаревших миграций
Устаревшие миграции, объединенные в более новые версии, перемещаются в папку `backend/db/migrations/archive/`. Для архивации используйте скрипт `backend/scripts/cleanup_migrations.sh`.
## Диагностические функции
Для проверки состояния базы данных и корректности миграций созданы следующие диагностические функции SQL:
- `verify_identity_system()` - проверка состояния системы идентификации пользователей
Пример использования:
```sql
SELECT * FROM verify_identity_system();
```

View File

@@ -36,7 +36,7 @@ const requireAuth = async (req, res, next) => {
const address = authHeader.split(' ')[1];
if (address.startsWith('0x')) {
const result = await db.query(
const result = await db.getQuery()(
`
SELECT u.id, u.is_admin
FROM users u
@@ -114,7 +114,7 @@ async function requireAdmin(req, res, next) {
// Проверка через ID пользователя
if (req.session.userId) {
const userResult = await db.query('SELECT role FROM users WHERE id = $1', [
const userResult = await db.getQuery()('SELECT role FROM users WHERE id = $1', [
req.session.userId,
]);
if (userResult.rows.length > 0 && userResult.rows[0].role === USER_ROLES.ADMIN) {
@@ -151,7 +151,7 @@ function requireRole(role) {
// Проверка через ID пользователя
if (req.session.userId) {
const userResult = await db.query('SELECT role FROM users WHERE id = $1', [
const userResult = await db.getQuery()('SELECT role FROM users WHERE id = $1', [
req.session.userId,
]);
if (userResult.rows.length > 0 && userResult.rows[0].role === role) {

View File

@@ -29,6 +29,7 @@
"@langchain/core": "0.3.0",
"@langchain/ollama": "^0.2.0",
"@openzeppelin/contracts": "5.2.0",
"archiver": "^7.0.1",
"axios": "^1.8.4",
"connect-pg-simple": "^10.0.0",
"cookie": "^1.0.2",
@@ -43,6 +44,8 @@
"helmet": "^8.0.0",
"hnswlib-node": "^3.0.0",
"imap": "^0.8.19",
"interface-store": "^6.0.2",
"ipfs-http-client": "^60.0.1",
"langchain": "^0.3.19",
"mailparser": "^3.7.2",
"multer": "^1.4.5-lts.2",

View File

@@ -40,13 +40,13 @@ router.get('/nonce', async (req, res) => {
if (existingNonce.rows.length > 0) {
// Обновляем существующий nonce
await db.query(
await db.getQuery()(
"UPDATE nonces SET nonce = $1, expires_at = NOW() + INTERVAL '15 minutes' WHERE identity_value = $2",
[nonce, address.toLowerCase()]
);
} else {
// Создаем новый nonce
await db.query(
await db.getQuery()(
"INSERT INTO nonces (identity_value, nonce, expires_at) VALUES ($1, $2, NOW() + INTERVAL '15 minutes')",
[address.toLowerCase(), nonce]
);
@@ -82,7 +82,7 @@ router.post('/verify', async (req, res) => {
const normalizedAddress = ethers.getAddress(address).toLowerCase();
// Проверяем nonce
const nonceResult = await db.query('SELECT nonce FROM nonces WHERE identity_value = $1', [
const nonceResult = await db.getQuery()('SELECT nonce FROM nonces WHERE identity_value = $1', [
normalizedAddress,
]);
if (
@@ -131,7 +131,7 @@ router.post('/verify', async (req, res) => {
const adminStatus = await authService.checkAdminTokens(normalizedAddress);
if (adminStatus) {
await db.query('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]);
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]);
isAdmin = true;
}
@@ -211,7 +211,7 @@ router.post('/telegram/verify', async (req, res) => {
// Обновляем роль в БД, если она отличается от той, что была получена из verifyTelegramAuth
const currentRoleInDb = verificationResult.role === 'admin';
if (finalIsAdmin !== currentRoleInDb) {
await db.query('UPDATE users SET role = $1 WHERE id = $2', [finalIsAdmin ? 'admin' : 'user', verificationResult.userId]);
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [finalIsAdmin ? 'admin' : 'user', verificationResult.userId]);
logger.info(`[telegram/verify] User role updated in DB for user ${verificationResult.userId} to ${finalIsAdmin ? 'admin' : 'user'}`);
}
} else {
@@ -385,7 +385,7 @@ router.post('/email/verify-code', async (req, res) => {
// Обновляем роль в БД, если она отличается от текущей
const currentRole = authResult.role === 'admin';
if (finalIsAdmin !== currentRole) {
await db.query('UPDATE users SET role = $1 WHERE id = $2', [finalIsAdmin ? 'admin' : 'user', authResult.userId]);
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [finalIsAdmin ? 'admin' : 'user', authResult.userId]);
logger.info(`[email/verify-code] User role updated in DB for user ${authResult.userId} to ${finalIsAdmin ? 'admin' : 'user'}`);
}
} catch (tokenCheckError) {
@@ -533,7 +533,7 @@ router.get('/check', async (req, res) => {
identities = await identityService.getUserIdentities(req.session.userId);
// Проверяем роль пользователя
const roleResult = await db.query('SELECT role FROM users WHERE id = $1', [
const roleResult = await db.getQuery()('SELECT role FROM users WHERE id = $1', [
req.session.userId,
]);
@@ -739,7 +739,7 @@ router.post('/wallet', async (req, res) => {
// Обновляем роль пользователя в базе данных, если нужно
if (isAdmin) {
await db.query('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]);
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]);
}
// Сохраняем идентификаторы

View File

@@ -7,6 +7,7 @@ const rpcProviderService = require('../services/rpcProviderService');
const authTokenService = require('../services/authTokenService');
const aiProviderSettingsService = require('../services/aiProviderSettingsService');
const aiAssistant = require('../services/ai-assistant');
const dns = require('node:dns').promises;
// Логируем версию ethers для отладки
logger.info(`Ethers version: ${ethers.version || 'unknown'}`);

View File

@@ -43,7 +43,7 @@ class AuthService {
const normalizedAddress = ethers.getAddress(address).toLowerCase();
// Ищем пользователя по адресу в таблице user_identities
const userResult = await db.query(
const userResult = await db.getQuery()(
`
SELECT u.* FROM users u
JOIN user_identities ui ON u.id = ui.user_id
@@ -60,11 +60,11 @@ class AuthService {
// Если статус админа изменился, обновляем роль в базе данных
if (user.role === 'admin' && !isAdmin) {
await db.query('UPDATE users SET role = $1 WHERE id = $2', ['user', user.id]);
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['user', user.id]);
logger.info(`Updated user ${user.id} role to user (admin tokens no longer present)`);
return { userId: user.id, isAdmin: false };
} else if (user.role !== 'admin' && isAdmin) {
await db.query('UPDATE users SET role = $1 WHERE id = $2', ['admin', user.id]);
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['admin', user.id]);
logger.info(`Updated user ${user.id} role to admin (admin tokens found)`);
return { userId: user.id, isAdmin: true };
}
@@ -76,14 +76,14 @@ class AuthService {
}
// Если пользователь не найден, создаем нового
const newUserResult = await db.query('INSERT INTO users (role) VALUES ($1) RETURNING id', [
const newUserResult = await db.getQuery()('INSERT INTO users (role) VALUES ($1) RETURNING id', [
'user',
]);
const userId = newUserResult.rows[0].id;
// Добавляем идентификатор кошелька (всегда в нижнем регистре)
await db.query(
await db.getQuery()(
'INSERT INTO user_identities (user_id, provider, provider_id) VALUES ($1, $2, $3)',
[userId, 'wallet', normalizedAddress]
);
@@ -94,7 +94,7 @@ class AuthService {
// Если у пользователя есть админские токены, обновляем его роль
if (isAdmin) {
await db.query('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]);
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]);
logger.info(
`New user ${userId} with wallet ${normalizedAddress} automatically granted admin role`
);
@@ -301,7 +301,7 @@ class AuthService {
}
// Сохраняем сессию в БД
const result = await db.query(
const result = await db.getQuery()(
`UPDATE session
SET sess = $1
WHERE sid = $2`,
@@ -351,7 +351,7 @@ class AuthService {
async getSession(sessionId) {
try {
const result = await db.query('SELECT * FROM session WHERE sid = $1', [sessionId]);
const result = await db.getQuery()('SELECT * FROM session WHERE sid = $1', [sessionId]);
return result.rows[0];
} catch (error) {
console.error('Error getting session:', error);
@@ -363,7 +363,7 @@ class AuthService {
async getLinkedWallet(userId) {
logger.info(`[getLinkedWallet] Called with userId: ${userId} (Type: ${typeof userId})`);
try {
const result = await db.query(
const result = await db.getQuery()(
`SELECT provider_id as address
FROM user_identities
WHERE user_id = $1 AND provider = 'wallet'`,
@@ -422,7 +422,7 @@ class AuthService {
const email = result.providerId;
// Проверяем, существует ли пользователь с таким email
const userResult = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
const userResult = await db.getQuery()('SELECT * FROM users WHERE id = $1', [userId]);
if (userResult.rows.length === 0) {
return { verified: false };
@@ -486,7 +486,7 @@ class AuthService {
// Если в сессии нет авторизованного пользователя, проверяем существующие идентификаторы
// Проверяем, существует ли уже пользователь с таким Telegram ID
const existingUserResult = await db.query(
const existingUserResult = await db.getQuery()(
`SELECT u.*, ui.provider, ui.provider_id
FROM users u
JOIN user_identities ui ON u.id = ui.user_id
@@ -503,14 +503,14 @@ class AuthService {
);
} else {
// Создаем нового пользователя для нового telegramId
const newUserResult = await db.query('INSERT INTO users (role) VALUES ($1) RETURNING id', [
const newUserResult = await db.getQuery()('INSERT INTO users (role) VALUES ($1) RETURNING id', [
'user',
]);
userId = newUserResult.rows[0].id;
isNewUser = true;
// Добавляем Telegram идентификатор
await db.query(
await db.getQuery()(
'INSERT INTO user_identities (user_id, provider, provider_id) VALUES ($1, $2, $3)',
[userId, 'telegram', telegramId]
);
@@ -522,7 +522,7 @@ class AuthService {
// Если есть гостевой ID в сессии, сохраняем его для нового пользователя
if (session.guestId && isNewUser) {
await db.query(
await db.getQuery()(
'INSERT INTO guest_user_mapping (user_id, guest_id) VALUES ($1, $2) ON CONFLICT (guest_id) DO UPDATE SET user_id = $1',
[userId, session.guestId]
);
@@ -555,7 +555,7 @@ class AuthService {
if (isAdmin) {
try {
// Находим userId по адресу
const userResult = await db.query(
const userResult = await db.getQuery()(
`
SELECT u.id FROM users u
JOIN user_identities ui ON u.id = ui.user_id
@@ -566,7 +566,7 @@ class AuthService {
if (userResult.rows.length > 0) {
const userId = userResult.rows[0].id;
// Обновляем роль пользователя
await db.query('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]);
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]);
logger.info(`Updated user ${userId} role to admin based on token holdings`);
}
} catch (error) {
@@ -576,7 +576,7 @@ class AuthService {
} else {
// Если пользователь не является администратором, сбрасываем роль на "user", если она была "admin"
try {
const userResult = await db.query(
const userResult = await db.getQuery()(
`
SELECT u.id, u.role FROM users u
JOIN user_identities ui ON u.id = ui.user_id
@@ -586,7 +586,7 @@ class AuthService {
if (userResult.rows.length > 0 && userResult.rows[0].role === 'admin') {
const userId = userResult.rows[0].id;
await db.query('UPDATE users SET role = $1 WHERE id = $2', ['user', userId]);
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['user', userId]);
logger.info(`Reset user ${userId} role from admin to user (no tokens found)`);
}
} catch (error) {
@@ -624,7 +624,7 @@ class AuthService {
// Удаляем старые идентификаторы
for (const identity of identitiesToDelete) {
await db.query('DELETE FROM user_identities WHERE id = $1', [identity.id]);
await db.getQuery()('DELETE FROM user_identities WHERE id = $1', [identity.id]);
logger.info(`Deleted old guest identity: ${identity.identity_value}`);
}
}
@@ -640,7 +640,7 @@ class AuthService {
*/
async getUserIdentities(userId) {
try {
const result = await db.query(
const result = await db.getQuery()(
'SELECT * FROM user_identities WHERE user_id = $1 ORDER BY created_at DESC',
[userId]
);
@@ -705,7 +705,7 @@ class AuthService {
);
// Проверяем, существует ли уже такой идентификатор
const existingResult = await db.query(
const existingResult = await db.getQuery()(
`SELECT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2`,
[provider, normalizedProviderId]
);
@@ -729,7 +729,7 @@ class AuthService {
}
// Добавляем новый идентификатор для пользователя
await db.query(
await db.getQuery()(
`INSERT INTO user_identities (user_id, provider, provider_id)
VALUES ($1, $2, $3)`,
[userId, provider, normalizedProviderId]
@@ -742,7 +742,7 @@ class AuthService {
// Обновляем роль пользователя в базе данных, если нужно
if (isAdmin) {
await db.query('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]);
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]);
logger.info(`[AuthService] Updated user ${userId} role to admin based on token holdings`);
}
}
@@ -791,7 +791,7 @@ class AuthService {
logger.info(`[handleEmailVerification] Using temporary user ${userId}`);
} else {
// Создаем нового пользователя
const newUserResult = await db.query('INSERT INTO users (role) VALUES ($1) RETURNING id', [
const newUserResult = await db.getQuery()('INSERT INTO users (role) VALUES ($1) RETURNING id', [
'user',
]);
userId = newUserResult.rows[0].id;
@@ -822,15 +822,15 @@ class AuthService {
logger.info(`[handleEmailVerification] Role determined as: ${userRole}`);
// Опционально: Обновить роль в таблице users
const currentUser = await db.query('SELECT role FROM users WHERE id = $1', [userId]);
const currentUser = await db.getQuery()('SELECT role FROM users WHERE id = $1', [userId]);
if (currentUser.rows.length > 0 && currentUser.rows[0].role !== userRole) {
await db.query('UPDATE users SET role = $1 WHERE id = $2', [userRole, userId]);
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [userRole, userId]);
logger.info(`[handleEmailVerification] Updated user role in DB to ${userRole}`);
}
} else {
logger.info(`[handleEmailVerification] No linked wallet found. Role remains 'user'.`);
// Если кошелька нет, проверяем текущую роль из базы (на случай, если она была admin ранее)
const currentUser = await db.query('SELECT role FROM users WHERE id = $1', [userId]);
const currentUser = await db.getQuery()('SELECT role FROM users WHERE id = $1', [userId]);
if (currentUser.rows.length > 0) {
userRole = currentUser.rows[0].role;
}
@@ -839,7 +839,7 @@ class AuthService {
logger.error(`[handleEmailVerification] Error checking admin role:`, roleCheckError);
// В случае ошибки берем текущую роль из базы или оставляем 'user'
try {
const currentUser = await db.query('SELECT role FROM users WHERE id = $1', [userId]);
const currentUser = await db.getQuery()('SELECT role FROM users WHERE id = $1', [userId]);
if (currentUser.rows.length > 0) {
userRole = currentUser.rows[0].role;
}

View File

@@ -6,9 +6,9 @@ async function getAllAuthTokens() {
}
async function saveAllAuthTokens(authTokens) {
await db.query('DELETE FROM auth_tokens');
await db.getQuery()('DELETE FROM auth_tokens');
for (const token of authTokens) {
await db.query(
await db.getQuery()(
'INSERT INTO auth_tokens (name, address, network, min_balance) VALUES ($1, $2, $3, $4)',
[token.name, token.address, token.network, token.minBalance]
);
@@ -17,7 +17,7 @@ async function saveAllAuthTokens(authTokens) {
async function upsertAuthToken(token) {
const minBalance = token.minBalance == null ? 0 : Number(token.minBalance);
await db.query(
await db.getQuery()(
`INSERT INTO auth_tokens (name, address, network, min_balance)
VALUES ($1, $2, $3, $4)
ON CONFLICT (address, network) DO UPDATE SET name=EXCLUDED.name, min_balance=EXCLUDED.min_balance`,
@@ -26,7 +26,7 @@ async function upsertAuthToken(token) {
}
async function deleteAuthToken(address, network) {
await db.query('DELETE FROM auth_tokens WHERE address = $1 AND network = $2', [address, network]);
await db.getQuery()('DELETE FROM auth_tokens WHERE address = $1 AND network = $2', [address, network]);
}
module.exports = { getAllAuthTokens, saveAllAuthTokens, upsertAuthToken, deleteAuthToken };

View File

@@ -39,7 +39,7 @@ class EmailAuth {
logger.info(`[initEmailAuth] Found existing user ${userId} with email ${email}`);
} else {
// Создаем временного пользователя, если нужно будет создать нового
const userResult = await db.query('INSERT INTO users (role) VALUES ($1) RETURNING id', [
const userResult = await db.getQuery()('INSERT INTO users (role) VALUES ($1) RETURNING id', [
'user',
]);
userId = userResult.rows[0].id;
@@ -148,7 +148,7 @@ class EmailAuth {
finalUserId = session.tempUserId;
logger.info(`[checkEmailVerification] Using temporary user ${finalUserId}`);
} else {
const newUserResult = await db.query(
const newUserResult = await db.getQuery()(
'INSERT INTO users (role) VALUES ($1) RETURNING id',
['user']
);
@@ -172,9 +172,9 @@ class EmailAuth {
logger.info(`[checkEmailVerification] Role for user ${finalUserId} determined as: ${userRole}`);
// Опционально: Обновить роль в таблице users, если она отличается
const currentUser = await db.query('SELECT role FROM users WHERE id = $1', [finalUserId]);
const currentUser = await db.getQuery()('SELECT role FROM users WHERE id = $1', [finalUserId]);
if (currentUser.rows.length > 0 && currentUser.rows[0].role !== userRole) {
await db.query('UPDATE users SET role = $1 WHERE id = $2', [userRole, finalUserId]);
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [userRole, finalUserId]);
logger.info(`[checkEmailVerification] Updated user role in DB to ${userRole}`);
}
} else {

View File

@@ -32,4 +32,6 @@ module.exports = {
telegramBot,
aiAssistant,
interfaceService: require('./interfaceService'),
};

View File

@@ -0,0 +1,21 @@
import path from 'path';
import fs from 'fs';
import { createHelia } from 'helia';
import { unixfs, globSource } from '@helia/unixfs';
import dns from 'node:dns/promises';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const db = require('../db');
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export async function checkDomain(domain) {
try {
const records = await dns.resolveAny(domain);
return { records };
} catch (e) {
return { error: e.message };
}
}

View File

@@ -6,7 +6,7 @@ async function getAllRpcProviders() {
}
async function saveAllRpcProviders(rpcConfigs) {
await db.query('DELETE FROM rpc_providers');
await db.getQuery()('DELETE FROM rpc_providers');
for (const cfg of rpcConfigs) {
await db.query(
'INSERT INTO rpc_providers (network_id, rpc_url, chain_id) VALUES ($1, $2, $3)',
@@ -25,7 +25,7 @@ async function upsertRpcProvider(cfg) {
}
async function deleteRpcProvider(networkId) {
await db.query('DELETE FROM rpc_providers WHERE network_id = $1', [networkId]);
await db.getQuery()('DELETE FROM rpc_providers WHERE network_id = $1', [networkId]);
}
module.exports = { getAllRpcProviders, saveAllRpcProviders, upsertRpcProvider, deleteRpcProvider };

View File

@@ -65,7 +65,7 @@ class SessionService {
guestIdsToProcess.add(session.guestId);
// Записываем связь с пользователем в новую таблицу
await db.query(
await db.getQuery()(
'INSERT INTO guest_user_mapping (user_id, guest_id) VALUES ($1, $2) ON CONFLICT (guest_id) DO UPDATE SET user_id = $1',
[userId, session.guestId]
);
@@ -76,7 +76,7 @@ class SessionService {
guestIdsToProcess.add(session.previousGuestId);
// Записываем связь с пользователем в новую таблицу
await db.query(
await db.getQuery()(
'INSERT INTO guest_user_mapping (user_id, guest_id) VALUES ($1, $2) ON CONFLICT (guest_id) DO UPDATE SET user_id = $1',
[userId, session.previousGuestId]
);
@@ -95,9 +95,10 @@ class SessionService {
session.processedGuestIds.push(guestId);
// Помечаем guestId как обработанный в базе данных
await db.query('UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', [
guestId,
]);
await db.getQuery()(
'UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1',
[guestId]
);
}
// Сохраняем сессию
@@ -182,7 +183,10 @@ class SessionService {
logger.info(`[SessionService] Attempting to retrieve session ${sessionId}`);
const result = await db.query('SELECT sess FROM session WHERE sid = $1', [sessionId]);
const result = await db.getQuery()(
'SELECT sess FROM session WHERE sid = $1',
[sessionId]
);
if (result.rows.length === 0) {
logger.info(`[SessionService] No session found with ID ${sessionId}`);

File diff suppressed because it is too large Load Diff

View File

@@ -44,6 +44,7 @@ services:
volumes:
- ./backend:/app
- backend_node_modules:/app/node_modules
- ./frontend/dist:/app/frontend_dist:ro
environment:
- NODE_ENV=${NODE_ENV:-development}
- PORT=${PORT:-8000}

View File

@@ -1,3 +1 @@
VITE_APP_ETHEREUM_NETWORK_URL=https://your-ethereum-network-url
VITE_API_URL=http://localhost:8000

View File

@@ -1,7 +1,7 @@
<template>
<div class="ai-settings settings-panel">
<h2>Интеграции</h2>
<div class="integration-blocks" v-if="!showProvider && !showEmailSettings && !showTelegramSettings">
<div class="integration-blocks" v-if="!showProvider && !showEmailSettings && !showTelegramSettings && !showDbSettings">
<div class="integration-block">
<h3>OpenAI</h3>
<p>Интеграция с OpenAI (GPT-4, GPT-3.5 и др.).</p>
@@ -32,6 +32,11 @@
<p>Интеграция с Email для отправки писем и уведомлений.</p>
<button class="details-btn" @click="showEmailSettings = true">Подробнее</button>
</div>
<div class="integration-block">
<h3>База данных</h3>
<p>Интеграция с PostgreSQL для хранения данных приложения и управления настройками.</p>
<button class="details-btn" @click="showDbSettings = true">Подробнее</button>
</div>
</div>
<AIProviderSettings
v-if="showProvider"
@@ -46,6 +51,7 @@
/>
<TelegramSettingsView v-if="showTelegramSettings" @cancel="showTelegramSettings = false" />
<EmailSettingsView v-if="showEmailSettings" @cancel="showEmailSettings = false" />
<DatabaseSettingsView v-if="showDbSettings" @cancel="showDbSettings = false" />
</div>
</template>
@@ -54,9 +60,11 @@ import { ref } from 'vue';
import AIProviderSettings from './AIProviderSettings.vue';
import TelegramSettingsView from './TelegramSettingsView.vue';
import EmailSettingsView from './EmailSettingsView.vue';
import DatabaseSettingsView from './DatabaseSettingsView.vue';
const showProvider = ref(null);
const showTelegramSettings = ref(false);
const showEmailSettings = ref(false);
const showDbSettings = ref(false);
const providerLabels = {
openai: {

View File

@@ -0,0 +1,115 @@
<template>
<div class="domain-block">
<h3>Подключение домена</h3>
<p>Укажите свой домен, чтобы привязать его к опубликованному в IPFS фронтенду. Следуйте инструкции ниже для настройки DNS.</p>
<div class="form-group">
<input v-model="domain" class="form-control" placeholder="example.com" />
<button class="btn-primary" :disabled="loading || !domain" @click="checkDomain">
{{ loading ? 'Проверка...' : 'Проверить' }}
</button>
</div>
<div v-if="error" class="error">Ошибка: {{ error }}</div>
<div v-if="success" class="success">DNS-записи найдены: <pre>{{ records }}</pre></div>
<div class="instruction">
<b>Инструкция по подключению:</b>
<ol>
<li>Опубликуйте фронтенд в IPFS и получите CID.</li>
<li>В панели управления доменом создайте DNS-запись типа <b>CNAME</b> или <b>TXT</b> (или используйте IPFS gateway).</li>
<li>Для CNAME: укажите <code>yourdomain.com CNAME gateway.ipfs.io</code> или аналогичный шлюз.</li>
<li>Для TXT: <code>_dnslink.yourdomain.com TXT dnslink=/ipfs/&lt;ваш CID&gt;</code></li>
<li>Дождитесь обновления DNS и проверьте домен через эту форму.</li>
</ol>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import axios from '../../api/axios';
const domain = ref('');
const loading = ref(false);
const error = ref('');
const success = ref(false);
const records = ref('');
async function checkDomain() {
loading.value = true;
error.value = '';
success.value = false;
records.value = '';
try {
const { data } = await axios.post('/api/settings/interface/check-domain', { domain: domain.value });
if (data.success && data.records) {
records.value = JSON.stringify(data.records, null, 2);
success.value = true;
} else {
error.value = data.error || 'DNS-записи не найдены';
}
} catch (e) {
error.value = e.response?.data?.error || e.message || 'Ошибка запроса';
} finally {
loading.value = false;
}
}
</script>
<style scoped>
.domain-block {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
padding: 2rem;
margin-bottom: 2rem;
max-width: 500px;
}
.form-group {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.form-control {
flex: 1;
padding: 0.5rem 1rem;
border-radius: 6px;
border: 1px solid #ccc;
font-size: 1rem;
}
.btn-primary {
background: var(--color-primary);
color: #fff;
border: none;
border-radius: 6px;
padding: 0.5rem 1.5rem;
cursor: pointer;
font-size: 1rem;
transition: background 0.2s;
}
.btn-primary:disabled {
background: #ccc;
cursor: not-allowed;
}
.success {
margin-top: 1rem;
color: var(--color-success, #2e7d32);
}
.error {
margin-top: 1rem;
color: var(--color-danger, #c62828);
}
.instruction {
margin-top: 1.5rem;
font-size: 0.95em;
color: #555;
background: #f8f8f8;
border-radius: 8px;
padding: 1rem;
}
pre {
background: #f4f4f4;
border-radius: 6px;
padding: 0.5rem;
font-size: 0.95em;
margin: 0.5rem 0 0 0;
}
</style>

View File

@@ -18,14 +18,15 @@
</div>
</div>
<!-- Другие настройки интерфейса можно добавить сюда -->
<DomainConnectBlock />
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
import { ref } from 'vue';
import { getFromStorage, setToStorage } from '../../utils/storage'; // Путь к utils может отличаться
import DomainConnectBlock from './DomainConnectBlock.vue';
// TODO: Импортировать API для сохранения, если нужно
const selectedLanguage = ref(getFromStorage('userLanguage', 'ru'));

View File

@@ -0,0 +1 @@
<!-- Компонент удалён как неактуальный -->