ваше сообщение коммита
This commit is contained in:
15
README.md
15
README.md
@@ -99,3 +99,18 @@ docker compose down -v
|
||||
|
||||
- Загрузка модели qwen2.5:7b может занять некоторое время в зависимости от скорости интернета
|
||||
- Для использования 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
|
||||
@@ -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 ./
|
||||
|
||||
6
backend/db/migrations/024_create_ipfs_publications.sql
Normal file
6
backend/db/migrations/024_create_ipfs_publications.sql
Normal 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()
|
||||
);
|
||||
@@ -1,9 +0,0 @@
|
||||
# API Documentation
|
||||
|
||||
## Authentication
|
||||
|
||||
### POST /api/auth/refresh-session
|
||||
|
||||
Refreshes the user session.
|
||||
|
||||
**Request:**
|
||||
@@ -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` - количество дублирующихся идентификаторов
|
||||
@@ -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();
|
||||
```
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
// Сохраняем идентификаторы
|
||||
|
||||
@@ -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'}`);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
@@ -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 {
|
||||
|
||||
@@ -32,4 +32,6 @@ module.exports = {
|
||||
|
||||
telegramBot,
|
||||
aiAssistant,
|
||||
|
||||
interfaceService: require('./interfaceService'),
|
||||
};
|
||||
|
||||
21
backend/services/interfaceService.mjs
Normal file
21
backend/services/interfaceService.mjs
Normal 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 };
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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}`);
|
||||
|
||||
2773
backend/yarn.lock
2773
backend/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -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}
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
VITE_APP_ETHEREUM_NETWORK_URL=https://your-ethereum-network-url
|
||||
|
||||
VITE_API_URL=http://localhost:8000
|
||||
@@ -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: {
|
||||
|
||||
115
frontend/src/views/settings/DomainConnectBlock.vue
Normal file
115
frontend/src/views/settings/DomainConnectBlock.vue
Normal 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/<ваш CID></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>
|
||||
@@ -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'));
|
||||
|
||||
1
frontend/src/views/settings/PublishToIPFSBlock.vue
Normal file
1
frontend/src/views/settings/PublishToIPFSBlock.vue
Normal file
@@ -0,0 +1 @@
|
||||
<!-- Компонент удалён как неактуальный -->
|
||||
Reference in New Issue
Block a user