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

This commit is contained in:
2025-05-22 16:24:39 +03:00
parent a91658eb31
commit 9aa842d238
41 changed files with 1621 additions and 507 deletions

View File

@@ -34,8 +34,9 @@ nano frontend/.env
4. Выполните миграции изнутри контейнера backend: 4. Выполните миграции изнутри контейнера backend:
``` ```
docker exec dapp-backend yarn migrate docker exec -e NODE_ENV=migration dapp-backend yarn migrate
```
```
Скрипт автоматически: Скрипт автоматически:
- Проверит наличие Docker и Docker Compose - Проверит наличие Docker и Docker Compose

View File

@@ -2,12 +2,12 @@ const express = require('express');
const helmet = require('helmet'); const helmet = require('helmet');
const cors = require('cors'); const cors = require('cors');
const session = require('express-session'); const session = require('express-session');
const { sessionMiddleware } = require('./config/session'); const sessionConfig = require('./config/session');
const logger = require('./utils/logger'); const logger = require('./utils/logger');
// const csurf = require('csurf'); // Закомментировано, так как не используется // const csurf = require('csurf'); // Закомментировано, так как не используется
const { errorHandler } = require('./middleware/errorHandler'); const errorHandler = require('./middleware/errorHandler');
// const { version } = require('./package.json'); // Закомментировано, так как не используется // const { version } = require('./package.json'); // Закомментировано, так как не используется
const pool = require('./db'); // Добавляем импорт pool const db = require('./db'); // Добавляем импорт db
const aiAssistant = require('./services/ai-assistant'); // Добавляем импорт aiAssistant const aiAssistant = require('./services/ai-assistant'); // Добавляем импорт aiAssistant
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
@@ -48,6 +48,9 @@ const ensureDirectoriesExist = () => {
// Вызываем функцию проверки директорий при запуске сервера // Вызываем функцию проверки директорий при запуске сервера
ensureDirectoriesExist(); ensureDirectoriesExist();
// Регистрируем коллбек для пересоздания session middleware при смене пула
db.setPoolChangeCallback(sessionConfig.reloadSessionMiddleware);
// Импорт маршрутов // Импорт маршрутов
const authRoutes = require('./routes/auth'); const authRoutes = require('./routes/auth');
const usersRoutes = require('./routes/users'); const usersRoutes = require('./routes/users');
@@ -79,8 +82,8 @@ app.use(
}) })
); );
// Настройка сессии (ИСПОЛЬЗУЕМ ИМПОРТИРОВАННОЕ MIDDLEWARE) // Настройка сессии (используем геттер, чтобы всегда был актуальный middleware)
app.use(sessionMiddleware); app.use((req, res, next) => sessionConfig.sessionMiddleware(req, res, next));
// Добавим middleware для проверки сессии // Добавим middleware для проверки сессии
app.use(async (req, res, next) => { app.use(async (req, res, next) => {
@@ -89,7 +92,7 @@ app.use(async (req, res, next) => {
// Проверяем сессию в базе данных // Проверяем сессию в базе данных
if (req.sessionID) { if (req.sessionID) {
const result = await pool.query('SELECT sess FROM session WHERE sid = $1', [req.sessionID]); const result = await db.getQuery()('SELECT sess FROM session WHERE sid = $1', [req.sessionID]);
console.log('Session from DB:', result.rows[0]?.sess); console.log('Session from DB:', result.rows[0]?.sess);
} }
@@ -104,7 +107,7 @@ app.use(async (req, res, next) => {
const token = authHeader.split(' ')[1]; const token = authHeader.split(' ')[1];
try { try {
// Находим пользователя по токену // Находим пользователя по токену
const { rows } = await pool.query( const { rows } = await db.getQuery(
` `
SELECT u.id, SELECT u.id,
(u.role = 'admin') as is_admin, (u.role = 'admin') as is_admin,
@@ -152,7 +155,7 @@ app.use((req, res, next) => {
// Маршруты API // Маршруты API
app.use('/api/auth', authRoutes); app.use('/api/auth', authRoutes);
app.use('/api/users', usersRoutes); app.use('/api/users', usersRoutes);
app.use('/api/identities', identitiesRoutes); app.use('/api', identitiesRoutes);
app.use('/api/chat', chatRoutes); app.use('/api/chat', chatRoutes);
app.use('/api/admin', adminRoutes); app.use('/api/admin', adminRoutes);
app.use('/api/tokens', tokensRouter); app.use('/api/tokens', tokensRouter);
@@ -181,6 +184,8 @@ console.log('OPENAI_API_KEY:', redactedValue);
console.log('EMAIL_USER:', process.env.EMAIL_USER); console.log('EMAIL_USER:', process.env.EMAIL_USER);
console.log('EMAIL_PASSWORD:', redactedValue); console.log('EMAIL_PASSWORD:', redactedValue);
console.log('typeof errorHandler:', typeof errorHandler, errorHandler.name);
// Добавляем обработчик ошибок последним // Добавляем обработчик ошибок последним
app.use(errorHandler); app.use(errorHandler);
@@ -188,7 +193,7 @@ app.use(errorHandler);
app.get('/api/health', async (req, res) => { app.get('/api/health', async (req, res) => {
try { try {
// Проверяем подключение к БД // Проверяем подключение к БД
await pool.query('SELECT NOW()'); await db.getQuery('SELECT NOW()');
// Проверяем AI сервис // Проверяем AI сервис
const aiStatus = await aiAssistant.checkHealth(); const aiStatus = await aiAssistant.checkHealth();
@@ -212,7 +217,7 @@ app.get('/api/health', async (req, res) => {
setInterval( setInterval(
async () => { async () => {
try { try {
await pool.query('DELETE FROM session WHERE expire < NOW()'); await db.getQuery('DELETE FROM session WHERE expire < NOW()');
} catch (error) { } catch (error) {
console.error('Error cleaning old sessions:', error); console.error('Error cleaning old sessions:', error);
} }

View File

@@ -1,10 +1,19 @@
const session = require('express-session'); const session = require('express-session');
const pgSession = require('connect-pg-simple')(session); const pgSession = require('connect-pg-simple')(session);
const { pool } = require('../db'); const db = require('../db');
const sessionConfig = { let onPoolChangeCallback = null;
function setPoolChangeCallback(cb) {
onPoolChangeCallback = cb;
}
let sessionMiddleware = createSessionMiddleware();
function createSessionMiddleware() {
return session({
store: new pgSession({ store: new pgSession({
pool, pool: db.getPool(),
tableName: 'session', tableName: 'session',
}), }),
secret: process.env.SESSION_SECRET || 'hb3atoken', secret: process.env.SESSION_SECRET || 'hb3atoken',
@@ -18,8 +27,20 @@ const sessionConfig = {
sameSite: 'lax', sameSite: 'lax',
path: '/', path: '/',
}, },
}; });
}
function reloadSessionMiddleware() {
sessionMiddleware = createSessionMiddleware();
if (onPoolChangeCallback) {
onPoolChangeCallback();
}
}
module.exports = { module.exports = {
sessionMiddleware: session(sessionConfig), get sessionMiddleware() {
return sessionMiddleware;
},
reloadSessionMiddleware,
setPoolChangeCallback,
}; };

View File

@@ -9,10 +9,14 @@ console.log('DB_PORT:', process.env.DB_PORT);
console.log('DB_NAME:', process.env.DB_NAME); console.log('DB_NAME:', process.env.DB_NAME);
console.log('DB_USER:', process.env.DB_USER); console.log('DB_USER:', process.env.DB_USER);
// Создаем пул соединений с базой данных // Первичное подключение по дефолтным значениям
const pool = new Pool({ let pool = new Pool({
connectionString: process.env.DATABASE_URL, host: process.env.DB_HOST || 'postgres',
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false, port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME || 'dapp_db',
user: process.env.DB_USER || 'dapp_user',
password: process.env.DB_PASSWORD,
ssl: false,
}); });
// Проверяем подключение к базе данных // Проверяем подключение к базе данных
@@ -21,36 +25,59 @@ pool.query('SELECT NOW()')
console.log('Успешное подключение к базе данных:', res.rows[0]); console.log('Успешное подключение к базе данных:', res.rows[0]);
}) })
.catch(err => { .catch(err => {
console.error('Failed to connect to the database using DATABASE_URL:', err); console.error('Ошибка подключения к базе данных:', err);
console.log('Attempting alternative database connection...');
// Пробуем альтернативное подключение
const altPool = new Pool({
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME || 'dapp_db',
user: process.env.DB_USER || 'dapp_user',
password: process.env.DB_PASSWORD,
}); });
altPool.query('SELECT NOW()') console.log('Пул создан:', pool.options || pool);
.then(altRes => {
console.log('Альтернативное подключение успешно:', altRes.rows[0]);
// Заменяем основной пул на альтернативный
module.exports.pool = altPool;
module.exports.query = (text, params) => altPool.query(text, params);
})
.catch(altErr => {
console.error('Альтернативное подключение тоже не удалось:', altErr);
console.log('Переключение на временное хранилище данных в памяти...');
module.exports = createInMemoryStorage();
});
});
// Функция для выполнения SQL-запросов function getPool() {
const query = (text, params) => { return pool;
return pool.query(text, params); }
};
function getQuery() {
return pool.query.bind(pool);
}
let poolChangeCallback = null;
function setPoolChangeCallback(cb) {
poolChangeCallback = cb;
}
// Функция для пересоздания пула из db_settings
async function reinitPoolFromDbSettings() {
try {
const res = await pool.query('SELECT * FROM db_settings ORDER BY id LIMIT 1');
if (!res.rows.length) throw new Error('DB settings not found');
const settings = res.rows[0];
// Закрываем старый пул
await pool.end();
// Создаём новый пул
pool = new Pool({
host: settings.db_host,
port: parseInt(settings.db_port),
database: settings.db_name,
user: settings.db_user,
password: settings.db_password,
ssl: false,
});
// Пересоздаём session middleware
if (poolChangeCallback) {
poolChangeCallback();
}
console.log('Пул пересоздан с новыми параметрами:', settings);
} catch (err) {
console.error('Ошибка пересоздания пула:', err);
throw err;
}
}
// При старте приложения — сразу пробуем инициализировать из db_settings
if (process.env.NODE_ENV !== 'migration') {
reinitPoolFromDbSettings();
}
const query = (text, params) => pool.query(text, params);
// Функция для сохранения гостевого сообщения в базе данных // Функция для сохранения гостевого сообщения в базе данных
async function saveGuestMessageToDatabase(message, language, guestId) { async function saveGuestMessageToDatabase(message, language, guestId) {
@@ -71,70 +98,9 @@ async function saveGuestMessageToDatabase(message, language, guestId) {
// Экспортируем функции для работы с базой данных // Экспортируем функции для работы с базой данных
module.exports = { module.exports = {
query, getPool,
pool, getQuery,
reinitPoolFromDbSettings,
saveGuestMessageToDatabase, saveGuestMessageToDatabase,
setPoolChangeCallback,
}; };
// Функция для создания временного хранилища данных в памяти
function createInMemoryStorage() {
console.log('Используется временное хранилище данных в памяти');
const users = [];
let userId = 1;
// Эмуляция функции query для работы с пользователями
const inMemoryQuery = async (text, params) => {
console.log('SQL query (in-memory):', text, 'Params:', params);
// Эмуляция запроса SELECT * FROM users WHERE address = $1
if (text.includes('SELECT * FROM users WHERE address = $1')) {
const address = params[0];
const user = users.find((u) => u.address === address);
return { rows: user ? [user] : [] };
}
// Эмуляция запроса SELECT * FROM users WHERE email = $1
if (text.includes('SELECT * FROM users WHERE email = $1')) {
const email = params[0];
const user = users.find((u) => u.email === email);
return { rows: user ? [user] : [] };
}
// Эмуляция запроса INSERT INTO users
if (text.includes('INSERT INTO users')) {
let newUser;
if (text.includes('address')) {
newUser = { id: userId++, address: params[0], created_at: new Date(), is_admin: false };
} else if (text.includes('email')) {
newUser = { id: userId++, email: params[0], created_at: new Date(), is_admin: false };
}
if (newUser) {
users.push(newUser);
return { rows: [newUser] };
}
}
return { rows: [] };
};
return {
query: inMemoryQuery,
pool: {
query: (text, params, callback) => {
if (callback) {
try {
const result = inMemoryQuery(text, params);
callback(null, result);
} catch (err) {
callback(err);
}
} else {
return inMemoryQuery(text, params);
}
},
},
};
}

View File

@@ -1,21 +0,0 @@
const { Pool } = require('pg');
const logger = require('../utils/logger');
const pool = new Pool({
user: process.env.DB_USER || 'dapp_user',
host: process.env.DB_HOST || 'localhost',
database: process.env.DB_NAME || 'dapp_db',
password: process.env.DB_PASSWORD,
port: process.env.DB_PORT || 5432,
});
// Проверка подключения
pool.query('SELECT NOW()', (err, res) => {
if (err) {
logger.error('Error connecting to database:', err);
} else {
logger.info('Успешное подключение к базе данных:', res.rows[0]);
}
});
module.exports = { pool };

View File

@@ -0,0 +1,17 @@
CREATE TABLE IF NOT EXISTS email_settings (
id SERIAL PRIMARY KEY,
smtp_host VARCHAR(255) NOT NULL,
smtp_port INTEGER NOT NULL,
smtp_user VARCHAR(255) NOT NULL,
smtp_password VARCHAR(255) NOT NULL,
imap_host VARCHAR(255),
imap_port INTEGER,
from_email VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Для простоты предполагаем, что настройки всегда одни (id=1)
INSERT INTO email_settings (smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email)
VALUES ('smtp.example.com', 465, 'user@example.com', 'password', 'imap.example.com', 993, 'noreply@example.com')
ON CONFLICT DO NOTHING;

View File

@@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS telegram_settings (
id SERIAL PRIMARY KEY,
bot_token VARCHAR(255) NOT NULL,
bot_username VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Для простоты предполагаем, что настройки всегда одни (id=1)
INSERT INTO telegram_settings (bot_token, bot_username)
VALUES ('your-telegram-bot-token', 'your_bot_username')
ON CONFLICT DO NOTHING;

View File

@@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS db_settings (
id SERIAL PRIMARY KEY,
db_host VARCHAR(255) NOT NULL,
db_port INTEGER NOT NULL,
db_name VARCHAR(255) NOT NULL,
db_user VARCHAR(255) NOT NULL,
db_password VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Для простоты предполагаем, что настройки всегда одни (id=1)
INSERT INTO db_settings (db_host, db_port, db_name, db_user, db_password)
VALUES ('localhost', 5432, 'dapp_db', 'dapp_user', 'dapp_password')
ON CONFLICT DO NOTHING;

View File

@@ -0,0 +1,14 @@
CREATE TABLE IF NOT EXISTS ai_providers_settings (
id SERIAL PRIMARY KEY,
provider VARCHAR(32) NOT NULL UNIQUE, -- openai, anthropic, google, ollama
api_key VARCHAR(255),
base_url VARCHAR(255),
selected_model VARCHAR(128),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Пример заполнения для Ollama (без ключа)
INSERT INTO ai_providers_settings (provider, base_url, selected_model)
VALUES ('ollama', 'http://localhost:11434', 'qwen2.5')
ON CONFLICT (provider) DO NOTHING;

View File

@@ -7,6 +7,11 @@ const { ERROR_CODES } = require('../utils/constants');
*/ */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const errorHandler = (err, req, res, /* next */) => { const errorHandler = (err, req, res, /* next */) => {
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}`, { logger.error(`Error: ${err.message}`, {
stack: err.stack, stack: err.stack,
@@ -65,7 +70,6 @@ function createError(message, status) {
return error; return error;
} }
module.exports = { module.exports = errorHandler;
errorHandler, // Если нужен createError для других файлов:
createError, // module.exports.createError = createError;
};

View File

@@ -23,6 +23,8 @@
"fix-duplicates": "node scripts/fix-duplicate-identities.js" "fix-duplicates": "node scripts/fix-duplicate-identities.js"
}, },
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.51.0",
"@google/genai": "^1.0.1",
"@langchain/community": "^0.3.34", "@langchain/community": "^0.3.34",
"@langchain/core": "0.3.0", "@langchain/core": "0.3.0",
"@langchain/ollama": "^0.2.0", "@langchain/ollama": "^0.2.0",
@@ -47,6 +49,7 @@
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"node-telegram-bot-api": "^0.66.0", "node-telegram-bot-api": "^0.66.0",
"nodemailer": "^6.10.0", "nodemailer": "^6.10.0",
"openai": "^4.102.0",
"pg": "^8.10.0", "pg": "^8.10.0",
"semver": "^7.7.1", "semver": "^7.7.1",
"session-file-store": "^1.5.0", "session-file-store": "^1.5.0",

View File

@@ -6,49 +6,49 @@ const authService = require('../services/auth-service');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
// Роли // Роли
router.get('/roles', requireAdmin, async (req, res) => { router.get('/roles', requireAdmin, async (req, res, next) => {
try { try {
const roles = await authService.getAllRoles(); const roles = await authService.getAllRoles();
res.json({ success: true, roles }); res.json({ success: true, roles });
} catch (error) { } catch (error) {
logger.error('Error getting roles:', error); logger.error('Error getting roles:', error);
res.status(500).json({ error: 'Internal server error' }); next(error);
} }
}); });
router.post('/roles', requireAdmin, async (req, res) => { router.post('/roles', requireAdmin, async (req, res, next) => {
try { try {
const { name, permissions } = req.body; const { name, permissions } = req.body;
const role = await authService.createRole(name, permissions); const role = await authService.createRole(name, permissions);
res.json({ success: true, role }); res.json({ success: true, role });
} catch (error) { } catch (error) {
logger.error('Error creating role:', error); logger.error('Error creating role:', error);
res.status(500).json({ error: 'Internal server error' }); next(error);
} }
}); });
// Админ функции // Админ функции
router.get('/users', requireAdmin, async (req, res) => { router.get('/users', requireAdmin, async (req, res, next) => {
try { try {
const users = await authService.getAllUsers(); const users = await authService.getAllUsers();
res.json({ success: true, users }); res.json({ success: true, users });
} catch (error) { } catch (error) {
logger.error('Error getting users:', error); logger.error('Error getting users:', error);
res.status(500).json({ error: 'Internal server error' }); next(error);
} }
}); });
// Маршрут для получения статистики (защищен middleware requireAdmin) // Маршрут для получения статистики (защищен middleware requireAdmin)
router.get('/stats', requireAdmin, async (req, res) => { router.get('/stats', requireAdmin, async (req, res, next) => {
try { try {
// Получаем количество пользователей // Получаем количество пользователей
const usersCount = await db.query('SELECT COUNT(*) FROM users'); const usersCount = await db.getQuery()('SELECT COUNT(*) FROM users');
// Получаем количество досок // Получаем количество досок
const boardsCount = await db.query('SELECT COUNT(*) FROM kanban_boards'); const boardsCount = await db.getQuery()('SELECT COUNT(*) FROM kanban_boards');
// Получаем количество задач // Получаем количество задач
const tasksCount = await db.query('SELECT COUNT(*) FROM kanban_tasks'); const tasksCount = await db.getQuery()('SELECT COUNT(*) FROM kanban_tasks');
res.json({ res.json({
userCount: parseInt(usersCount.rows[0].count), userCount: parseInt(usersCount.rows[0].count),
@@ -57,18 +57,18 @@ router.get('/stats', requireAdmin, async (req, res) => {
}); });
} catch (error) { } catch (error) {
console.error('Ошибка при получении статистики:', error); console.error('Ошибка при получении статистики:', error);
res.status(500).json({ error: 'Internal server error' }); next(error);
} }
}); });
// Маршрут для получения логов // Маршрут для получения логов
router.get('/logs', requireAdmin, async (req, res) => { router.get('/logs', requireAdmin, async (req, res, next) => {
try { try {
const result = await db.query('SELECT * FROM logs ORDER BY created_at DESC LIMIT 100'); const result = await db.getQuery()('SELECT * FROM logs ORDER BY created_at DESC LIMIT 100');
res.json(result.rows); res.json(result.rows);
} catch (error) { } catch (error) {
console.error('Ошибка при получении логов:', error); console.error('Ошибка при получении логов:', error);
res.status(500).json({ error: 'Internal server error' }); next(error);
} }
}); });

View File

@@ -34,7 +34,7 @@ router.get('/nonce', async (req, res) => {
const nonce = crypto.randomBytes(16).toString('hex'); const nonce = crypto.randomBytes(16).toString('hex');
// Проверяем, существует ли уже nonce для этого адреса // Проверяем, существует ли уже nonce для этого адреса
const existingNonce = await db.query('SELECT id FROM nonces WHERE identity_value = $1', [ const existingNonce = await db.getQuery()('SELECT id FROM nonces WHERE identity_value = $1', [
address.toLowerCase(), address.toLowerCase(),
]); ]);

View File

@@ -17,7 +17,7 @@ async function processGuestMessages(userId, guestId) {
logger.info(`Processing guest messages for user ${userId} with guest ID ${guestId}`); logger.info(`Processing guest messages for user ${userId} with guest ID ${guestId}`);
// Проверяем, обрабатывались ли уже эти сообщения // Проверяем, обрабатывались ли уже эти сообщения
const mappingCheck = await db.query( const mappingCheck = await db.getQuery()(
'SELECT processed FROM guest_user_mapping WHERE guest_id = $1', 'SELECT processed FROM guest_user_mapping WHERE guest_id = $1',
[guestId] [guestId]
); );
@@ -30,7 +30,7 @@ async function processGuestMessages(userId, guestId) {
// Проверяем наличие mapping записи и создаем если нет // Проверяем наличие mapping записи и создаем если нет
if (mappingCheck.rows.length === 0) { if (mappingCheck.rows.length === 0) {
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', 'INSERT INTO guest_user_mapping (user_id, guest_id) VALUES ($1, $2) ON CONFLICT (guest_id) DO UPDATE SET user_id = $1',
[userId, guestId] [userId, guestId]
); );
@@ -38,7 +38,7 @@ async function processGuestMessages(userId, guestId) {
} }
// Получаем все гостевые сообщения со всеми новыми полями // Получаем все гостевые сообщения со всеми новыми полями
const guestMessagesResult = await db.query( const guestMessagesResult = await db.getQuery()(
`SELECT `SELECT
id, guest_id, content, language, is_ai, created_at, id, guest_id, content, language, is_ai, created_at,
attachment_filename, attachment_mimetype, attachment_size, attachment_data attachment_filename, attachment_mimetype, attachment_size, attachment_data
@@ -48,9 +48,9 @@ async function processGuestMessages(userId, guestId) {
if (guestMessagesResult.rows.length === 0) { if (guestMessagesResult.rows.length === 0) {
logger.info(`No guest messages found for guest ID ${guestId}`); logger.info(`No guest messages found for guest ID ${guestId}`);
const checkResult = await db.query('SELECT 1 FROM guest_user_mapping WHERE guest_id = $1', [guestId]); const checkResult = await db.getQuery()('SELECT 1 FROM guest_user_mapping WHERE guest_id = $1', [guestId]);
if (checkResult.rows.length > 0) { if (checkResult.rows.length > 0) {
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]);
logger.info(`Marked guest mapping as processed (no messages found) for guest ID ${guestId}`); logger.info(`Marked guest mapping as processed (no messages found) for guest ID ${guestId}`);
} else { } else {
logger.warn(`Attempted to mark non-existent guest mapping as processed for guest ID ${guestId}`); logger.warn(`Attempted to mark non-existent guest mapping as processed for guest ID ${guestId}`);
@@ -67,7 +67,7 @@ async function processGuestMessages(userId, guestId) {
? (firstMessage.content.length > 30 ? `${firstMessage.content.substring(0, 30)}...` : firstMessage.content) ? (firstMessage.content.length > 30 ? `${firstMessage.content.substring(0, 30)}...` : firstMessage.content)
: (firstMessage.attachment_filename ? `Файл: ${firstMessage.attachment_filename}` : 'Новый диалог'); : (firstMessage.attachment_filename ? `Файл: ${firstMessage.attachment_filename}` : 'Новый диалог');
const newConversationResult = await db.query( const newConversationResult = await db.getQuery()(
'INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING *', 'INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING *',
[userId, title] [userId, title]
); );
@@ -84,7 +84,7 @@ async function processGuestMessages(userId, guestId) {
try { try {
// Сохраняем сообщение пользователя в таблицу messages, включая данные файла // Сохраняем сообщение пользователя в таблицу messages, включая данные файла
const userMessageResult = await db.query( const userMessageResult = await db.getQuery()(
`INSERT INTO messages `INSERT INTO messages
(conversation_id, content, sender_type, role, channel, created_at, user_id, (conversation_id, content, sender_type, role, channel, created_at, user_id,
attachment_filename, attachment_mimetype, attachment_size, attachment_data) attachment_filename, attachment_mimetype, attachment_size, attachment_data)
@@ -118,7 +118,7 @@ async function processGuestMessages(userId, guestId) {
if (aiResponseContent) { if (aiResponseContent) {
// Сохраняем ответ от ИИ (у него нет вложений) // Сохраняем ответ от ИИ (у него нет вложений)
const aiMessageResult = await db.query( const aiMessageResult = await db.getQuery()(
`INSERT INTO messages `INSERT INTO messages
(conversation_id, content, sender_type, role, channel, created_at, user_id) (conversation_id, content, sender_type, role, channel, created_at, user_id)
VALUES VALUES
@@ -144,20 +144,20 @@ async function processGuestMessages(userId, guestId) {
// Удаляем только успешно обработанные гостевые сообщения // Удаляем только успешно обработанные гостевые сообщения
if (savedMessageIds.length > 0) { if (savedMessageIds.length > 0) {
await db.query('DELETE FROM guest_messages WHERE id = ANY($1::int[])', [savedMessageIds]); await db.getQuery()('DELETE FROM guest_messages WHERE id = ANY($1::int[])', [savedMessageIds]);
logger.info( logger.info(
`Deleted ${savedMessageIds.length} processed guest messages for guest ID ${guestId}` `Deleted ${savedMessageIds.length} processed guest messages for guest ID ${guestId}`
); );
// Помечаем гостевой ID как обработанный // Помечаем гостевой ID как обработанный
await db.query('UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', [ await db.getQuery()('UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', [
guestId, guestId,
]); ]);
logger.info(`Marked guest mapping as processed for guest ID ${guestId}`); logger.info(`Marked guest mapping as processed for guest ID ${guestId}`);
} else { } else {
logger.warn(`No guest messages were successfully processed, skipping deletion for guest ID ${guestId}`); logger.warn(`No guest messages were successfully processed, skipping deletion for guest ID ${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]);
logger.info(`Marked guest mapping as processed (no successful messages) for guest ID ${guestId}`); logger.info(`Marked guest mapping as processed (no successful messages) for guest ID ${guestId}`);
} }
@@ -221,7 +221,7 @@ router.post('/guest-message', upload.array('attachments'), async (req, res) => {
}); });
// Сохраняем сообщение пользователя с текстом или файлом // Сохраняем сообщение пользователя с текстом или файлом
const result = await db.query( const result = await db.getQuery()(
`INSERT INTO guest_messages `INSERT INTO guest_messages
(guest_id, content, language, is_ai, (guest_id, content, language, is_ai,
attachment_filename, attachment_mimetype, attachment_size, attachment_data) attachment_filename, attachment_mimetype, attachment_size, attachment_data)
@@ -293,7 +293,7 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
try { try {
// Найти или создать диалог // Найти или создать диалог
if (conversationId) { if (conversationId) {
const convResult = await db.query( const convResult = await db.getQuery()(
'SELECT * FROM conversations WHERE id = $1 AND user_id = $2', 'SELECT * FROM conversations WHERE id = $1 AND user_id = $2',
[conversationId, userId] [conversationId, userId]
); );
@@ -308,7 +308,7 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
? (message.length > 50 ? `${message.substring(0, 50)}...` : message) ? (message.length > 50 ? `${message.substring(0, 50)}...` : message)
: (file ? `Файл: ${file.originalname}` : 'Новый диалог'); : (file ? `Файл: ${file.originalname}` : 'Новый диалог');
const newConvResult = await db.query( const newConvResult = await db.getQuery()(
'INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING *', 'INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING *',
[userId, title] [userId, title]
); );
@@ -325,7 +325,7 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
const attachmentData = file ? file.buffer : null; const attachmentData = file ? file.buffer : null;
// Сохраняем сообщение пользователя // Сохраняем сообщение пользователя
const userMessageResult = await db.query( const userMessageResult = await db.getQuery()(
`INSERT INTO messages `INSERT INTO messages
(conversation_id, user_id, content, sender_type, role, channel, (conversation_id, user_id, content, sender_type, role, channel,
attachment_filename, attachment_mimetype, attachment_size, attachment_data) attachment_filename, attachment_mimetype, attachment_size, attachment_data)
@@ -354,7 +354,7 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
logger.info('AI response received' + (aiResponseContent ? '' : ' (empty)'), { conversationId }); logger.info('AI response received' + (aiResponseContent ? '' : ' (empty)'), { conversationId });
if (aiResponseContent) { if (aiResponseContent) {
const aiMessageResult = await db.query( const aiMessageResult = await db.getQuery()(
`INSERT INTO messages `INSERT INTO messages
(conversation_id, user_id, content, sender_type, role, channel) (conversation_id, user_id, content, sender_type, role, channel)
VALUES ($1, $2, $3, 'assistant', 'assistant', 'web') VALUES ($1, $2, $3, 'assistant', 'assistant', 'web')
@@ -443,7 +443,7 @@ router.get('/history', requireAuth, async (req, res) => {
countQuery += ' AND conversation_id = $2'; countQuery += ' AND conversation_id = $2';
countParams.push(conversationId); countParams.push(conversationId);
} }
const countResult = await db.query(countQuery, countParams); const countResult = await db.getQuery()(countQuery, countParams);
const totalCount = parseInt(countResult.rows[0].count, 10); const totalCount = parseInt(countResult.rows[0].count, 10);
return res.json({ success: true, count: totalCount }); return res.json({ success: true, count: totalCount });
} }
@@ -481,7 +481,7 @@ router.get('/history', requireAuth, async (req, res) => {
logger.debug('Executing history query:', { query, params }); logger.debug('Executing history query:', { query, params });
const result = await db.query(query, params); const result = await db.getQuery()(query, params);
// Обрабатываем результаты для фронтенда // Обрабатываем результаты для фронтенда
const messages = result.rows.map(msg => { const messages = result.rows.map(msg => {
@@ -522,7 +522,7 @@ router.get('/history', requireAuth, async (req, res) => {
totalCountQuery += ' AND conversation_id = $2'; totalCountQuery += ' AND conversation_id = $2';
totalCountParams.push(conversationId); totalCountParams.push(conversationId);
} }
const totalCountResult = await db.query(totalCountQuery, totalCountParams); const totalCountResult = await db.getQuery()(totalCountQuery, totalCountParams);
const totalMessages = parseInt(totalCountResult.rows[0].count, 10); const totalMessages = parseInt(totalCountResult.rows[0].count, 10);
logger.info(`Returning message history for user ${userId}`, { count: messages.length, offset, limit, total: totalMessages }); logger.info(`Returning message history for user ${userId}`, { count: messages.length, offset, limit, total: totalMessages });

View File

@@ -11,7 +11,7 @@ const fs = require('fs');
* @desc Создать новое DLE (Digital Legal Entity) * @desc Создать новое DLE (Digital Legal Entity)
* @access Private (только для авторизованных пользователей с ролью admin) * @access Private (только для авторизованных пользователей с ролью admin)
*/ */
router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res) => { router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res, next) => {
try { try {
const dleParams = req.body; const dleParams = req.body;
logger.info('Получен запрос на создание DLE:', dleParams); logger.info('Получен запрос на создание DLE:', dleParams);
@@ -44,11 +44,7 @@ router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res) => {
}); });
} catch (error) { } catch (error) {
logger.error('Ошибка при создании DLE:', error); logger.error('Ошибка при создании DLE:', error);
res.status(500).json({ next(error);
success: false,
message: error.message || 'Произошла ошибка при создании DLE',
error: process.env.NODE_ENV === 'development' ? error.stack : undefined
});
} }
}); });
@@ -57,7 +53,7 @@ router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res) => {
* @desc Получить список всех DLE * @desc Получить список всех DLE
* @access Private (только для авторизованных пользователей) * @access Private (только для авторизованных пользователей)
*/ */
router.get('/', auth.requireAuth, async (req, res) => { router.get('/', auth.requireAuth, async (req, res, next) => {
try { try {
const dles = await dleService.getAllDLEs(); const dles = await dleService.getAllDLEs();
res.json({ res.json({
@@ -66,11 +62,7 @@ router.get('/', auth.requireAuth, async (req, res) => {
}); });
} catch (error) { } catch (error) {
logger.error('Ошибка при получении списка DLE:', error); logger.error('Ошибка при получении списка DLE:', error);
res.status(500).json({ next(error);
success: false,
message: error.message || 'Произошла ошибка при получении списка DLE',
error: process.env.NODE_ENV === 'development' ? error.stack : undefined
});
} }
}); });
@@ -99,7 +91,7 @@ router.get('/settings', auth.requireAuth, (req, res) => {
* @desc Удалить DLE по адресу токена * @desc Удалить DLE по адресу токена
* @access Private (только для авторизованных пользователей с ролью admin) * @access Private (только для авторизованных пользователей с ролью admin)
*/ */
router.delete('/:tokenAddress', auth.requireAuth, auth.requireAdmin, async (req, res) => { router.delete('/:tokenAddress', auth.requireAuth, auth.requireAdmin, async (req, res, next) => {
try { try {
const { tokenAddress } = req.params; const { tokenAddress } = req.params;
logger.info(`Получен запрос на удаление DLE с адресом токена: ${tokenAddress}`); logger.info(`Получен запрос на удаление DLE с адресом токена: ${tokenAddress}`);
@@ -142,11 +134,7 @@ router.delete('/:tokenAddress', auth.requireAuth, auth.requireAdmin, async (req,
}); });
} catch (error) { } catch (error) {
logger.error('Ошибка при удалении DLE:', error); logger.error('Ошибка при удалении DLE:', error);
res.status(500).json({ next(error);
success: false,
message: error.message || 'Произошла ошибка при удалении DLE',
error: process.env.NODE_ENV === 'development' ? error.stack : undefined
});
} }
}); });
@@ -155,7 +143,7 @@ router.delete('/:tokenAddress', auth.requireAuth, auth.requireAdmin, async (req,
* @desc Удалить пустое DLE по имени файла * @desc Удалить пустое DLE по имени файла
* @access Private (только для авторизованных пользователей с ролью admin) * @access Private (только для авторизованных пользователей с ролью admin)
*/ */
router.delete('/empty/:fileName', auth.requireAuth, auth.requireAdmin, async (req, res) => { router.delete('/empty/:fileName', auth.requireAuth, auth.requireAdmin, async (req, res, next) => {
try { try {
const { fileName } = req.params; const { fileName } = req.params;
logger.info(`Получен запрос на удаление пустого DLE с именем файла: ${fileName}`); logger.info(`Получен запрос на удаление пустого DLE с именем файла: ${fileName}`);
@@ -180,11 +168,7 @@ router.delete('/empty/:fileName', auth.requireAuth, auth.requireAdmin, async (re
}); });
} catch (error) { } catch (error) {
logger.error('Ошибка при удалении пустого DLE:', error); logger.error('Ошибка при удалении пустого DLE:', error);
res.status(500).json({ next(error);
success: false,
message: error.message || 'Произошла ошибка при удалении пустого DLE',
error: process.env.NODE_ENV === 'development' ? error.stack : undefined
});
} }
}); });

View File

@@ -6,19 +6,19 @@ const logger = require('../utils/logger');
const db = require('../db'); const db = require('../db');
// Получение всех идентификаторов пользователя // Получение всех идентификаторов пользователя
router.get('/', requireAuth, async (req, res) => { router.get('/', requireAuth, async (req, res, next) => {
try { try {
const userId = req.session.userId; const userId = req.session.userId;
const identities = await authService.getUserIdentities(userId); const identities = await authService.getUserIdentities(userId);
res.json({ success: true, identities }); res.json({ success: true, identities });
} catch (error) { } catch (error) {
logger.error('Error getting identities:', error); logger.error('Error getting identities:', error);
res.status(500).json({ error: 'Internal server error' }); next(error);
} }
}); });
// Связывание нового идентификатора // Связывание нового идентификатора
router.post('/link', requireAuth, async (req, res) => { router.post('/link', requireAuth, async (req, res, next) => {
try { try {
const { type, value } = req.body; const { type, value } = req.body;
const userId = req.session.userId; const userId = req.session.userId;
@@ -28,7 +28,7 @@ router.post('/link', requireAuth, async (req, res) => {
const normalizedWallet = value.toLowerCase(); const normalizedWallet = value.toLowerCase();
// Проверяем, существует ли уже такой кошелек // Проверяем, существует ли уже такой кошелек
const existingCheck = await db.query( const existingCheck = await db.getQuery()(
`SELECT user_id FROM user_identities `SELECT user_id FROM user_identities
WHERE provider = 'wallet' AND provider_id = $1`, WHERE provider = 'wallet' AND provider_id = $1`,
[normalizedWallet] [normalizedWallet]
@@ -73,12 +73,12 @@ router.post('/link', requireAuth, async (req, res) => {
}); });
} }
res.status(500).json({ error: error.message || 'Internal server error' }); next(error);
} }
}); });
// Получение балансов токенов // Получение балансов токенов
router.get('/token-balances', requireAuth, async (req, res) => { router.get('/token-balances', requireAuth, async (req, res, next) => {
try { try {
const userId = req.session.userId; const userId = req.session.userId;
if (!userId) { if (!userId) {
@@ -103,12 +103,12 @@ router.get('/token-balances', requireAuth, async (req, res) => {
}); });
} catch (error) { } catch (error) {
logger.error('Error getting token balances:', error); logger.error('Error getting token balances:', error);
res.status(500).json({ error: 'Internal server error' }); next(error);
} }
}); });
// Удаление идентификатора пользователя // Удаление идентификатора пользователя
router.delete('/:provider/:providerId', requireAuth, async (req, res) => { router.delete('/:provider/:providerId', requireAuth, async (req, res, next) => {
try { try {
const userId = req.session.userId; const userId = req.session.userId;
const { provider, providerId } = req.params; const { provider, providerId } = req.params;
@@ -120,7 +120,135 @@ router.delete('/:provider/:providerId', requireAuth, async (req, res) => {
} }
} catch (error) { } catch (error) {
logger.error('Error deleting identity:', error); logger.error('Error deleting identity:', error);
res.status(500).json({ error: 'Internal server error' }); next(error);
}
});
// Получение email-настроек
router.get('/email-settings', requireAuth, async (req, res, next) => {
try {
const { rows } = await db.getQuery()('SELECT * FROM email_settings ORDER BY id LIMIT 1');
if (!rows.length) return res.status(404).json({ success: false, error: 'Not found' });
const settings = rows[0];
delete settings.smtp_password; // не возвращаем пароль
res.json({ success: true, settings });
} catch (error) {
logger.error('Error getting email settings:', error, error && error.stack);
next(error);
}
});
// Обновление email-настроек
router.put('/email-settings', requireAuth, async (req, res, next) => {
try {
const { smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email } = req.body;
if (!smtp_host || !smtp_port || !smtp_user || !from_email) {
return res.status(400).json({ success: false, error: 'Missing required fields' });
}
const { rows } = await db.getQuery()('SELECT id FROM email_settings ORDER BY id LIMIT 1');
if (rows.length) {
// Обновляем существующую запись
await db.getQuery()(
`UPDATE email_settings SET smtp_host=$1, smtp_port=$2, smtp_user=$3, smtp_password=COALESCE($4, smtp_password), imap_host=$5, imap_port=$6, from_email=$7, updated_at=NOW() WHERE id=$8`,
[smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email, rows[0].id]
);
} else {
// Вставляем новую
await db.getQuery()(
`INSERT INTO email_settings (smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email) VALUES ($1,$2,$3,$4,$5,$6,$7)`,
[smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email]
);
}
res.json({ success: true });
} catch (error) {
logger.error('Error updating email settings:', error);
next(error);
}
});
// Получение telegram-настроек
router.get('/telegram-settings', requireAuth, async (req, res, next) => {
try {
const { rows } = await db.getQuery()('SELECT * FROM telegram_settings ORDER BY id LIMIT 1');
if (!rows.length) return res.status(404).json({ success: false, error: 'Not found' });
const settings = rows[0];
delete settings.bot_token; // не возвращаем токен
res.json({ success: true, settings });
} catch (error) {
logger.error('Error getting telegram settings:', error, error && error.stack);
next(error);
}
});
// Обновление telegram-настроек
router.put('/telegram-settings', requireAuth, async (req, res, next) => {
try {
const { bot_token, bot_username } = req.body;
if (!bot_token || !bot_username) {
return res.status(400).json({ success: false, error: 'Missing required fields' });
}
const { rows } = await db.getQuery()('SELECT id FROM telegram_settings ORDER BY id LIMIT 1');
if (rows.length) {
// Обновляем существующую запись
await db.getQuery()(
`UPDATE telegram_settings SET bot_token=$1, bot_username=$2, updated_at=NOW() WHERE id=$3`,
[bot_token, bot_username, rows[0].id]
);
} else {
// Вставляем новую
await db.getQuery()(
`INSERT INTO telegram_settings (bot_token, bot_username) VALUES ($1,$2)` ,
[bot_token, bot_username]
);
}
res.json({ success: true });
} catch (error) {
logger.error('Error updating telegram settings:', error);
next(error);
}
});
// Получение db-настроек
router.get('/db-settings', requireAuth, async (req, res, next) => {
try {
const { rows } = await db.getQuery()('SELECT * FROM db_settings ORDER BY id LIMIT 1');
if (!rows.length) return res.status(404).json({ success: false, error: 'Not found' });
const settings = rows[0];
delete settings.db_password; // не возвращаем пароль
res.json({ success: true, settings });
} catch (error) {
logger.error('Error getting db settings:', error, error && error.stack);
next(error);
}
});
// Обновление db-настроек
router.put('/db-settings', requireAuth, async (req, res, next) => {
try {
const { db_host, db_port, db_name, db_user, db_password } = req.body;
if (!db_host || !db_port || !db_name || !db_user) {
return res.status(400).json({ success: false, error: 'Missing required fields' });
}
const { rows } = await db.getQuery()('SELECT id FROM db_settings ORDER BY id LIMIT 1');
if (rows.length) {
// Обновляем существующую запись
await db.getQuery()(
`UPDATE db_settings SET db_host=$1, db_port=$2, db_name=$3, db_user=$4, db_password=COALESCE($5, db_password), updated_at=NOW() WHERE id=$6`,
[db_host, db_port, db_name, db_user, db_password, rows[0].id]
);
} else {
// Вставляем новую
await db.getQuery()(
`INSERT INTO db_settings (db_host, db_port, db_name, db_user, db_password) VALUES ($1,$2,$3,$4,$5)` ,
[db_host, db_port, db_name, db_user, db_password]
);
}
// Пересоздаём пул соединений с новыми настройками
await db.reinitPoolFromDbSettings();
res.json({ success: true });
} catch (error) {
logger.error('Error updating db settings:', error);
next(error);
} }
}); });

View File

@@ -1,6 +1,6 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const { pool } = require('../db'); // Убедитесь, что путь к вашему db-коннектору правильный const db = require('../db');
const logger = require('../utils/logger'); // Если используете логгер const logger = require('../utils/logger'); // Если используете логгер
/** /**
@@ -98,7 +98,7 @@ router.get('/codes', async (req, res) => {
if (parent_code) { if (parent_code) {
try { try {
const parentResult = await pool.query('SELECT code_level FROM isic_rev4_codes WHERE code = $1', [parent_code]); const parentResult = await db.getQuery()('SELECT code_level FROM isic_rev4_codes WHERE code = $1', [parent_code]);
if (parentResult.rows.length > 0) { if (parentResult.rows.length > 0) {
const parentLevel = parentResult.rows[0].code_level; const parentLevel = parentResult.rows[0].code_level;
if (parentLevel >= 1 && parentLevel < 6) { if (parentLevel >= 1 && parentLevel < 6) {
@@ -146,7 +146,7 @@ router.get('/codes', async (req, res) => {
} }
if (parent_code) { if (parent_code) {
// Предполагаем, что parent_code уже добавлен в countQueryParams // Предполагаем, что parent_code уже добавлен в countQueryParams
const parentLevelResult = await pool.query('SELECT code_level FROM isic_rev4_codes WHERE code = $1', [parent_code]); // Нужно будет передать parent_code в countQueryParams const parentLevelResult = await db.getQuery()('SELECT code_level FROM isic_rev4_codes WHERE code = $1', [parent_code]); // Нужно будет передать parent_code в countQueryParams
if (parentLevelResult.rows.length > 0) { if (parentLevelResult.rows.length > 0) {
const parentLevel = parentLevelResult.rows[0].code_level; const parentLevel = parentLevelResult.rows[0].code_level;
if (parentLevel >=1 && parentLevel < 6) { if (parentLevel >=1 && parentLevel < 6) {
@@ -174,7 +174,7 @@ router.get('/codes', async (req, res) => {
const queryWhereConditions = []; const queryWhereConditions = [];
if (level) queryWhereConditions.push(`c.code_level = $${currentQueryParamIndex++}`); if (level) queryWhereConditions.push(`c.code_level = $${currentQueryParamIndex++}`);
if (parent_code) { if (parent_code) {
const parentLevelResult = await pool.query('SELECT code_level FROM isic_rev4_codes WHERE code = $1', [parent_code]); // Это дублирование, лучше получить parentLevel один раз const parentLevelResult = await db.getQuery()('SELECT code_level FROM isic_rev4_codes WHERE code = $1', [parent_code]); // Это дублирование, лучше получить parentLevel один раз
if (parentLevelResult.rows.length > 0) { if (parentLevelResult.rows.length > 0) {
const parentLevel = parentLevelResult.rows[0].code_level; const parentLevel = parentLevelResult.rows[0].code_level;
if (parentLevel >=1 && parentLevel < 6) { if (parentLevel >=1 && parentLevel < 6) {
@@ -193,12 +193,12 @@ router.get('/codes', async (req, res) => {
try { try {
logger.debug('Executing count query:', finalCountQuery, 'Params:', countQueryParams); logger.debug('Executing count query:', finalCountQuery, 'Params:', countQueryParams);
const totalItemsResult = await pool.query(finalCountQuery, countQueryParams); const totalItemsResult = await db.getQuery()(finalCountQuery, countQueryParams);
const totalItems = parseInt(totalItemsResult.rows[0].total, 10); const totalItems = parseInt(totalItemsResult.rows[0].total, 10);
// Параметры для основного запроса - это все, что в queryParams (включая limit и offset) // Параметры для основного запроса - это все, что в queryParams (включая limit и offset)
logger.debug('Executing data query:', finalQuery, 'Params:', queryParams); logger.debug('Executing data query:', finalQuery, 'Params:', queryParams);
const result = await pool.query(finalQuery, queryParams); const result = await db.getQuery()(finalQuery, queryParams);
res.json({ res.json({
totalItems, totalItems,
@@ -253,13 +253,13 @@ router.get('/tree', async (req, res) => {
try { try {
let items; let items;
if (!root_code) { // Если нет root_code, возвращаем секции (уровень 1) if (!root_code) { // Если нет root_code, возвращаем секции (уровень 1)
const result = await pool.query( const result = await db.getQuery()(
"SELECT code, description, code_level FROM isic_rev4_codes WHERE code_level = 1 ORDER BY sort_order, code" "SELECT code, description, code_level FROM isic_rev4_codes WHERE code_level = 1 ORDER BY sort_order, code"
); );
items = result.rows.map(row => ({ ...row, children: [] })); // Добавляем пустой массив children items = result.rows.map(row => ({ ...row, children: [] })); // Добавляем пустой массив children
} else { } else {
// Получаем сам root_code // Получаем сам root_code
const rootResult = await pool.query( const rootResult = await db.getQuery()(
"SELECT code, description, code_level FROM isic_rev4_codes WHERE code = $1", "SELECT code, description, code_level FROM isic_rev4_codes WHERE code = $1",
[root_code] [root_code]
); );
@@ -281,7 +281,7 @@ router.get('/tree', async (req, res) => {
if (childrenQuery) { if (childrenQuery) {
const childrenResult = await pool.query(childrenQuery, childrenParams); const childrenResult = await db.getQuery()(childrenQuery, childrenParams);
rootNode.children = childrenResult.rows.map(row => ({ ...row, children: [] })); rootNode.children = childrenResult.rows.map(row => ({ ...row, children: [] }));
} }
items = [rootNode]; items = [rootNode];

View File

@@ -5,23 +5,25 @@ const logger = require('../utils/logger');
const { ethers } = require('ethers'); const { ethers } = require('ethers');
const rpcProviderService = require('../services/rpcProviderService'); const rpcProviderService = require('../services/rpcProviderService');
const authTokenService = require('../services/authTokenService'); const authTokenService = require('../services/authTokenService');
const aiProviderSettingsService = require('../services/aiProviderSettingsService');
const aiAssistant = require('../services/ai-assistant');
// Логируем версию ethers для отладки // Логируем версию ethers для отладки
logger.info(`Ethers version: ${ethers.version || 'unknown'}`); logger.info(`Ethers version: ${ethers.version || 'unknown'}`);
// Получение RPC настроек // Получение RPC настроек
router.get('/rpc', requireAdmin, async (req, res) => { router.get('/rpc', requireAdmin, async (req, res, next) => {
try { try {
const rpcConfigs = await rpcProviderService.getAllRpcProviders(); const rpcConfigs = await rpcProviderService.getAllRpcProviders();
res.json({ success: true, data: rpcConfigs }); res.json({ success: true, data: rpcConfigs });
} catch (error) { } catch (error) {
logger.error('Ошибка при получении RPC настроек:', error); logger.error('Ошибка при получении RPC настроек:', error);
res.status(500).json({ success: false, error: 'Ошибка сервера при получении настроек RPC' }); next(error);
} }
}); });
// Добавление/обновление одного или нескольких RPC // Добавление/обновление одного или нескольких RPC
router.post('/rpc', requireAdmin, async (req, res) => { router.post('/rpc', requireAdmin, async (req, res, next) => {
try { try {
// Если пришёл массив rpcConfigs — bulk-режим // Если пришёл массив rpcConfigs — bulk-режим
if (Array.isArray(req.body.rpcConfigs)) { if (Array.isArray(req.body.rpcConfigs)) {
@@ -41,35 +43,35 @@ router.post('/rpc', requireAdmin, async (req, res) => {
res.json({ success: true, message: 'RPC провайдер сохранён' }); res.json({ success: true, message: 'RPC провайдер сохранён' });
} catch (error) { } catch (error) {
logger.error('Ошибка при сохранении RPC:', error); logger.error('Ошибка при сохранении RPC:', error);
res.status(500).json({ success: false, error: 'Ошибка сервера при сохранении RPC' }); next(error);
} }
}); });
// Удаление одного RPC // Удаление одного RPC
router.delete('/rpc/:networkId', requireAdmin, async (req, res) => { router.delete('/rpc/:networkId', requireAdmin, async (req, res, next) => {
try { try {
const { networkId } = req.params; const { networkId } = req.params;
await rpcProviderService.deleteRpcProvider(networkId); await rpcProviderService.deleteRpcProvider(networkId);
res.json({ success: true, message: 'RPC провайдер удалён' }); res.json({ success: true, message: 'RPC провайдер удалён' });
} catch (error) { } catch (error) {
logger.error('Ошибка при удалении RPC:', error); logger.error('Ошибка при удалении RPC:', error);
res.status(500).json({ success: false, error: 'Ошибка сервера при удалении RPC' }); next(error);
} }
}); });
// Получение токенов для аутентификации // Получение токенов для аутентификации
router.get('/auth-tokens', requireAdmin, async (req, res) => { router.get('/auth-tokens', requireAdmin, async (req, res, next) => {
try { try {
const authTokens = await authTokenService.getAllAuthTokens(); const authTokens = await authTokenService.getAllAuthTokens();
res.json({ success: true, data: authTokens }); res.json({ success: true, data: authTokens });
} catch (error) { } catch (error) {
logger.error('Ошибка при получении токенов аутентификации:', error); logger.error('Ошибка при получении токенов аутентификации:', error);
res.status(500).json({ success: false, error: 'Ошибка сервера при получении токенов аутентификации' }); next(error);
} }
}); });
// Сохранение токенов для аутентификации // Сохранение токенов для аутентификации
router.post('/auth-tokens', requireAdmin, async (req, res) => { router.post('/auth-tokens', requireAdmin, async (req, res, next) => {
try { try {
const { authTokens } = req.body; const { authTokens } = req.body;
if (!Array.isArray(authTokens)) { if (!Array.isArray(authTokens)) {
@@ -79,12 +81,12 @@ router.post('/auth-tokens', requireAdmin, async (req, res) => {
res.json({ success: true, message: 'Токены аутентификации успешно сохранены' }); res.json({ success: true, message: 'Токены аутентификации успешно сохранены' });
} catch (error) { } catch (error) {
logger.error('Ошибка при сохранении токенов аутентификации:', error); logger.error('Ошибка при сохранении токенов аутентификации:', error);
res.status(500).json({ success: false, error: 'Ошибка сервера при сохранении токенов аутентификации' }); next(error);
} }
}); });
// Добавление/обновление одного токена // Добавление/обновление одного токена
router.post('/auth-token', requireAdmin, async (req, res) => { router.post('/auth-token', requireAdmin, async (req, res, next) => {
try { try {
const { name, address, network, minBalance } = req.body; const { name, address, network, minBalance } = req.body;
if (!name || !address || !network) { if (!name || !address || !network) {
@@ -94,24 +96,24 @@ router.post('/auth-token', requireAdmin, async (req, res) => {
res.json({ success: true, message: 'Токен аутентификации сохранён' }); res.json({ success: true, message: 'Токен аутентификации сохранён' });
} catch (error) { } catch (error) {
logger.error('Ошибка при сохранении токена аутентификации:', error); logger.error('Ошибка при сохранении токена аутентификации:', error);
res.status(500).json({ success: false, error: 'Ошибка сервера при сохранении токена' }); next(error);
} }
}); });
// Удаление одного токена // Удаление одного токена
router.delete('/auth-token/:address/:network', requireAdmin, async (req, res) => { router.delete('/auth-token/:address/:network', requireAdmin, async (req, res, next) => {
try { try {
const { address, network } = req.params; const { address, network } = req.params;
await authTokenService.deleteAuthToken(address, network); await authTokenService.deleteAuthToken(address, network);
res.json({ success: true, message: 'Токен аутентификации удалён' }); res.json({ success: true, message: 'Токен аутентификации удалён' });
} catch (error) { } catch (error) {
logger.error('Ошибка при удалении токена аутентификации:', error); logger.error('Ошибка при удалении токена аутентификации:', error);
res.status(500).json({ success: false, error: 'Ошибка сервера при удалении токена' }); next(error);
} }
}); });
// Тестирование RPC соединения // Тестирование RPC соединения
router.post('/rpc-test', requireAdmin, async (req, res) => { router.post('/rpc-test', requireAdmin, async (req, res, next) => {
try { try {
const { rpcUrl, networkId } = req.body; const { rpcUrl, networkId } = req.body;
@@ -164,4 +166,76 @@ router.post('/rpc-test', requireAdmin, async (req, res) => {
} }
}); });
// Получить настройки AI-провайдера
router.get('/ai-settings/:provider', requireAdmin, async (req, res, next) => {
try {
const { provider } = req.params;
const settings = await aiProviderSettingsService.getProviderSettings(provider);
res.json({ success: true, settings });
} catch (error) {
logger.error('Ошибка при получении AI-настроек:', error);
next(error);
}
});
// Сохранить/обновить настройки AI-провайдера
router.put('/ai-settings/:provider', requireAdmin, async (req, res, next) => {
try {
const { provider } = req.params;
const { api_key, base_url, selected_model } = req.body;
const updated = await aiProviderSettingsService.upsertProviderSettings({ provider, api_key, base_url, selected_model });
res.json({ success: true, settings: updated });
} catch (error) {
logger.error('Ошибка при сохранении AI-настроек:', error);
next(error);
}
});
// Удалить настройки AI-провайдера
router.delete('/ai-settings/:provider', requireAdmin, async (req, res, next) => {
try {
const { provider } = req.params;
await aiProviderSettingsService.deleteProviderSettings(provider);
res.json({ success: true });
} catch (error) {
logger.error('Ошибка при удалении AI-настроек:', error);
next(error);
}
});
// Получить список моделей для провайдера
router.get('/ai-settings/:provider/models', requireAdmin, async (req, res, next) => {
try {
const { provider } = req.params;
const settings = await aiProviderSettingsService.getProviderSettings(provider);
let models = [];
if (provider === 'ollama') {
models = await aiAssistant.getAvailableModels();
} else {
models = await aiProviderSettingsService.getProviderModels(provider, settings || {});
}
res.json({ success: true, models });
} catch (error) {
logger.error('Ошибка при получении моделей AI:', error);
res.status(500).json({ success: false, error: error.message });
}
});
// Проверить валидность ключа (verify)
router.post('/ai-settings/:provider/verify', requireAdmin, async (req, res, next) => {
try {
const { provider } = req.params;
const { api_key, base_url } = req.body;
const result = await aiProviderSettingsService.verifyProviderKey(provider, { api_key, base_url });
if (result.success) {
res.json({ success: true });
} else {
res.status(400).json({ success: false, error: result.error });
}
} catch (error) {
logger.error('Ошибка при проверке AI-ключа:', error);
res.status(500).json({ success: false, error: error.message });
}
});
module.exports = router; module.exports = router;

View File

@@ -4,7 +4,7 @@ const logger = require('../utils/logger');
const authService = require('../services/auth-service'); const authService = require('../services/auth-service');
// Получение балансов токенов пользователя по токенам из базы // Получение балансов токенов пользователя по токенам из базы
router.get('/balances', async (req, res) => { router.get('/balances', async (req, res, next) => {
try { try {
const address = req.query.address; const address = req.query.address;
if (!address) { if (!address) {
@@ -15,7 +15,7 @@ router.get('/balances', async (req, res) => {
res.json({ success: true, data: balances }); res.json({ success: true, data: balances });
} catch (error) { } catch (error) {
logger.error('Error fetching token balances:', error); logger.error('Error fetching token balances:', error);
res.status(500).json({ success: false, error: 'Failed to fetch token balances' }); next(error);
} }
}); });

View File

@@ -20,53 +20,42 @@ router.get('/:address', (req, res) => {
}); });
// Маршрут для обновления языка пользователя // Маршрут для обновления языка пользователя
router.post('/update-language', requireAuth, async (req, res) => { router.post('/update-language', requireAuth, async (req, res, next) => {
try { try {
const { language } = req.body; const { language } = req.body;
const userId = req.session.userId; const userId = req.session.userId;
// Проверка валидности языка
const validLanguages = ['ru', 'en']; const validLanguages = ['ru', 'en'];
if (!validLanguages.includes(language)) { if (!validLanguages.includes(language)) {
return res.status(400).json({ error: 'Неподдерживаемый язык' }); return res.status(400).json({ error: 'Неподдерживаемый язык' });
} }
await db.getQuery()('UPDATE users SET preferred_language = $1 WHERE id = $2', [language, userId]);
// Обновление языка в базе данных
await db.query('UPDATE users SET preferred_language = $1 WHERE id = $2', [language, userId]);
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {
logger.error('Error updating language:', error); logger.error('Error updating language:', error);
res.status(500).json({ error: 'Внутренняя ошибка сервера' }); next(error);
} }
}); });
// Маршрут для обновления имени и фамилии пользователя // Маршрут для обновления имени и фамилии пользователя
router.post('/update-profile', requireAuth, async (req, res) => { router.post('/update-profile', requireAuth, async (req, res, next) => {
try { try {
const { firstName, lastName } = req.body; const { firstName, lastName } = req.body;
const userId = req.session.userId; const userId = req.session.userId;
// Проверка валидности данных
if (firstName && firstName.length > 255) { if (firstName && firstName.length > 255) {
return res.status(400).json({ error: 'Имя слишком длинное (максимум 255 символов)' }); return res.status(400).json({ error: 'Имя слишком длинное (максимум 255 символов)' });
} }
if (lastName && lastName.length > 255) { if (lastName && lastName.length > 255) {
return res.status(400).json({ error: 'Фамилия слишком длинная (максимум 255 символов)' }); return res.status(400).json({ error: 'Фамилия слишком длинная (максимум 255 символов)' });
} }
await db.getQuery()('UPDATE users SET first_name = $1, last_name = $2 WHERE id = $3', [
// Обновление имени и фамилии в базе данных
await db.query('UPDATE users SET first_name = $1, last_name = $2 WHERE id = $3', [
firstName || null, firstName || null,
lastName || null, lastName || null,
userId, userId,
]); ]);
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {
logger.error('Error updating user profile:', error); logger.error('Error updating user profile:', error);
res.status(500).json({ error: 'Внутренняя ошибка сервера' }); next(error);
} }
}); });

View File

@@ -1,7 +1,8 @@
const fs = require('fs').promises; const fs = require('fs').promises;
const path = require('path'); const path = require('path');
require('dotenv').config(); require('dotenv').config();
const { pool } = require('../db'); const { getPool } = require('../db');
const pool = getPool();
const logger = require('../utils/logger'); const logger = require('../utils/logger');
async function runMigrations() { async function runMigrations() {

View File

@@ -0,0 +1,105 @@
const db = require('../db');
const OpenAI = require('openai');
const Anthropic = require('@anthropic-ai/sdk');
const { GoogleGenAI } = require('@google/genai');
const TABLE = 'ai_providers_settings';
async function getProviderSettings(provider) {
const { rows } = await db.getQuery()(
`SELECT * FROM ${TABLE} WHERE provider = $1 LIMIT 1`,
[provider]
);
return rows[0] || null;
}
async function upsertProviderSettings({ provider, api_key, base_url, selected_model }) {
const { rows } = await db.getQuery()(
`INSERT INTO ${TABLE} (provider, api_key, base_url, selected_model, updated_at)
VALUES ($1, $2, $3, $4, NOW())
ON CONFLICT (provider) DO UPDATE SET
api_key = EXCLUDED.api_key,
base_url = EXCLUDED.base_url,
selected_model = EXCLUDED.selected_model,
updated_at = NOW()
RETURNING *`,
[provider, api_key, base_url, selected_model]
);
return rows[0];
}
async function deleteProviderSettings(provider) {
await db.getQuery()(
`DELETE FROM ${TABLE} WHERE provider = $1`,
[provider]
);
}
async function getProviderModels(provider, { api_key, base_url } = {}) {
try {
if (provider === 'openai') {
const client = new OpenAI({ apiKey: api_key, baseURL: base_url });
const res = await client.models.list();
return res.data ? res.data.map(m => ({ id: m.id, ...m })) : [];
}
if (provider === 'anthropic') {
const client = new Anthropic({ apiKey: api_key, baseURL: base_url });
const res = await client.models.list();
return res.data ? res.data.map(m => ({ id: m.id, ...m })) : [];
}
if (provider === 'google') {
const ai = new GoogleGenAI({ apiKey: api_key, baseUrl: base_url });
const pager = await ai.models.list();
const models = [];
for await (const model of pager) {
models.push(model);
}
return models;
}
if (provider === 'ollama') {
// Для Ollama — через ai-assistant.js
return [];
}
return [];
} catch (error) {
return [];
}
}
async function verifyProviderKey(provider, { api_key, base_url } = {}) {
try {
if (provider === 'openai') {
const client = new OpenAI({ apiKey: api_key, baseURL: base_url });
await client.models.list();
return { success: true };
}
if (provider === 'anthropic') {
const client = new Anthropic({ apiKey: api_key, baseURL: base_url });
await client.models.list();
return { success: true };
}
if (provider === 'google') {
const ai = new GoogleGenAI({ apiKey: api_key, baseUrl: base_url });
const pager = await ai.models.list();
for await (const _ of pager) {
break;
}
return { success: true };
}
if (provider === 'ollama') {
// Для Ollama — всегда true (локальный)
return { success: true };
}
return { success: false, error: 'Unknown provider' };
} catch (error) {
return { success: false, error: error.message };
}
}
module.exports = {
getProviderSettings,
upsertProviderSettings,
deleteProviderSettings,
getProviderModels,
verifyProviderKey,
};

View File

@@ -1,7 +1,7 @@
const db = require('../db'); const db = require('../db');
async function getAllAuthTokens() { async function getAllAuthTokens() {
const { rows } = await db.query('SELECT * FROM auth_tokens ORDER BY id'); const { rows } = await db.getQuery()('SELECT * FROM auth_tokens ORDER BY id');
return rows; return rows;
} }

View File

@@ -1,13 +1,13 @@
const { pool } = require('../db'); const { pool } = require('../db');
const verificationService = require('./verification-service'); const verificationService = require('./verification-service');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const emailBot = require('./emailBot'); const EmailBotService = require('./emailBot');
const db = require('../db'); const db = require('../db');
const authService = require('./auth-service'); const authService = require('./auth-service');
class EmailAuth { class EmailAuth {
constructor() { constructor() {
this.emailBot = emailBot; this.emailBot = new EmailBotService();
} }
async initEmailAuth(session, email) { async initEmailAuth(session, email) {
@@ -17,7 +17,7 @@ class EmailAuth {
} }
// Проверяем, существует ли пользователь с таким email // Проверяем, существует ли пользователь с таким email
const existingEmailUser = await db.query( const existingEmailUser = await db.getQuery()(
`SELECT u.id FROM users u `SELECT u.id FROM users u
JOIN user_identities i ON u.id = i.user_id JOIN user_identities i ON u.id = i.user_id
WHERE i.provider = 'email' AND i.provider_id = $1`, WHERE i.provider = 'email' AND i.provider_id = $1`,

View File

@@ -1,4 +1,4 @@
const { pool } = require('../db'); const db = require('../db');
const nodemailer = require('nodemailer'); const nodemailer = require('nodemailer');
const Imap = require('imap'); const Imap = require('imap');
const simpleParser = require('mailparser').simpleParser; const simpleParser = require('mailparser').simpleParser;
@@ -6,29 +6,37 @@ const { processMessage } = require('./ai-assistant');
const { inspect } = require('util'); const { inspect } = require('util');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
// Конфигурация для отправки писем class EmailBotService {
const transporter = nodemailer.createTransport({ async getSettingsFromDb() {
host: process.env.EMAIL_SMTP_HOST || 'smtp.hostland.ru', const { rows } = await db.getQuery()('SELECT * FROM email_settings ORDER BY id LIMIT 1');
port: process.env.EMAIL_SMTP_PORT || 465, if (!rows.length) throw new Error('Email settings not found in DB');
return rows[0];
}
async getTransporter() {
const settings = await this.getSettingsFromDb();
return nodemailer.createTransport({
host: settings.smtp_host,
port: settings.smtp_port,
secure: true, secure: true,
auth: { auth: {
user: process.env.EMAIL_USER, user: settings.smtp_user,
pass: process.env.EMAIL_PASSWORD, pass: settings.smtp_password,
}, },
pool: true, pool: true,
maxConnections: 3, maxConnections: 3,
maxMessages: 5, maxMessages: 5,
tls: { tls: { rejectUnauthorized: false },
rejectUnauthorized: false, });
}, }
});
// Конфигурация для получения писем async getImapConfig() {
const imapConfig = { const settings = await this.getSettingsFromDb();
user: process.env.EMAIL_USER, return {
password: process.env.EMAIL_PASSWORD, user: settings.smtp_user,
host: process.env.EMAIL_IMAP_HOST, password: settings.smtp_password,
port: process.env.EMAIL_IMAP_PORT, host: settings.imap_host,
port: settings.imap_port,
tls: true, tls: true,
tlsOptions: { rejectUnauthorized: false }, tlsOptions: { rejectUnauthorized: false },
keepalive: { keepalive: {
@@ -36,29 +44,7 @@ const imapConfig = {
idleInterval: 300000, idleInterval: 300000,
forceNoop: true, forceNoop: true,
}, },
}; };
class EmailBotService {
constructor() {
this.transporter = transporter;
this.imap = new Imap(imapConfig);
this.initialize();
}
initialize() {
this.imap.once('error', (err) => {
logger.error(`IMAP connection error: ${err.message}`);
setTimeout(() => {
try {
if (this.imap.state !== 'connected') {
this.imap = new Imap(imapConfig);
this.initialize();
}
} catch (e) {
logger.error(`Error reconnecting IMAP: ${e.message}`);
}
}, 60000);
});
} }
// Метод для инициализации email верификации // Метод для инициализации email верификации
@@ -77,24 +63,16 @@ class EmailBotService {
// Отправка кода верификации // Отправка кода верификации
async sendVerificationCode(email, code) { async sendVerificationCode(email, code) {
try { try {
const settings = await this.getSettingsFromDb();
const transporter = await this.getTransporter();
const mailOptions = { const mailOptions = {
from: process.env.EMAIL_USER, from: settings.from_email,
to: email, to: email,
subject: 'Код подтверждения', subject: 'Код подтверждения',
text: `Ваш код подтверждения: ${code}\n\nКод действителен в течение 15 минут.`, text: `Ваш код подтверждения: ${code}\n\nКод действителен в течение 15 минут.`,
html: ` html: `<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;"><h2 style="color: #333;">Код подтверждения</h2><p style="font-size: 16px; color: #666;">Ваш код подтверждения:</p><div style="background-color: #f5f5f5; padding: 15px; border-radius: 5px; text-align: center; margin: 20px 0;"><span style="font-size: 24px; font-weight: bold; color: #333;">${code}</span></div><p style="font-size: 14px; color: #999;">Код действителен в течение 15 минут.</p></div>`,
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #333;">Код подтверждения</h2>
<p style="font-size: 16px; color: #666;">Ваш код подтверждения:</p>
<div style="background-color: #f5f5f5; padding: 15px; border-radius: 5px; text-align: center; margin: 20px 0;">
<span style="font-size: 24px; font-weight: bold; color: #333;">${code}</span>
</div>
<p style="font-size: 14px; color: #999;">Код действителен в течение 15 минут.</p>
</div>
`,
}; };
await transporter.sendMail(mailOptions);
await this.transporter.sendMail(mailOptions);
logger.info(`Verification code sent to ${email}`); logger.info(`Verification code sent to ${email}`);
} catch (error) { } catch (error) {
logger.error('Error sending verification code:', error); logger.error('Error sending verification code:', error);
@@ -187,14 +165,15 @@ class EmailBotService {
// Метод для отправки email // Метод для отправки email
async sendEmail(to, subject, text) { async sendEmail(to, subject, text) {
try { try {
const settings = await this.getSettingsFromDb();
const transporter = await this.getTransporter();
const mailOptions = { const mailOptions = {
from: process.env.EMAIL_USER, from: settings.from_email,
to, to,
subject, subject,
text, text,
}; };
await transporter.sendMail(mailOptions);
await this.transporter.sendMail(mailOptions);
logger.info(`Email sent to ${to}`); logger.info(`Email sent to ${to}`);
return true; return true;
} catch (error) { } catch (error) {
@@ -204,5 +183,4 @@ class EmailBotService {
} }
} }
// Экспортируем singleton instance module.exports = EmailBotService;
module.exports = new EmailBotService();

View File

@@ -62,7 +62,7 @@ class IdentityService {
); );
try { try {
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', 'INSERT INTO guest_user_mapping (user_id, guest_id) VALUES ($1, $2) ON CONFLICT (guest_id) DO UPDATE SET user_id = $1',
[userId, normalizedProviderId] [userId, normalizedProviderId]
); );
@@ -91,7 +91,7 @@ class IdentityService {
); );
// Проверяем, существует ли уже такой идентификатор // Проверяем, существует ли уже такой идентификатор
const existingResult = await db.query( const existingResult = await db.getQuery()(
`SELECT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2`, `SELECT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2`,
[normalizedProvider, normalizedProviderId] [normalizedProvider, normalizedProviderId]
); );
@@ -116,7 +116,7 @@ class IdentityService {
} }
} else { } else {
// Создаем новую запись // Создаем новую запись
await db.query( await db.getQuery()(
`INSERT INTO user_identities (user_id, provider, provider_id) `INSERT INTO user_identities (user_id, provider, provider_id)
VALUES ($1, $2, $3)`, VALUES ($1, $2, $3)`,
[userId, normalizedProvider, normalizedProviderId] [userId, normalizedProvider, normalizedProviderId]
@@ -148,7 +148,7 @@ class IdentityService {
return []; return [];
} }
const result = await db.query( const result = await db.getQuery()(
`SELECT provider, provider_id FROM user_identities WHERE user_id = $1`, `SELECT provider, provider_id FROM user_identities WHERE user_id = $1`,
[userId] [userId]
); );
@@ -174,7 +174,7 @@ class IdentityService {
return []; return [];
} }
const result = await db.query( const result = await db.getQuery()(
`SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2`, `SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2`,
[userId, provider] [userId, provider]
); );
@@ -211,7 +211,7 @@ class IdentityService {
const { provider: normalizedProvider, providerId: normalizedProviderId } = const { provider: normalizedProvider, providerId: normalizedProviderId } =
this.normalizeIdentity(provider, providerId); this.normalizeIdentity(provider, providerId);
const result = await db.query( const result = await db.getQuery()(
`SELECT u.id, u.role FROM users u `SELECT u.id, u.role FROM users u
JOIN user_identities ui ON u.id = ui.user_id JOIN user_identities ui ON u.id = ui.user_id
WHERE ui.provider = $1 AND ui.provider_id = $2`, WHERE ui.provider = $1 AND ui.provider_id = $2`,
@@ -255,7 +255,7 @@ class IdentityService {
// Нормализуем провайдера // Нормализуем провайдера
const normalizedProvider = provider.toLowerCase(); const normalizedProvider = provider.toLowerCase();
const result = await db.query( const result = await db.getQuery()(
`SELECT provider, provider_id, created_at, updated_at `SELECT provider, provider_id, created_at, updated_at
FROM user_identities FROM user_identities
WHERE user_id = $1 AND provider = $2 WHERE user_id = $1 AND provider = $2
@@ -320,7 +320,7 @@ class IdentityService {
// Сохраняем гостевые идентификаторы в guest_user_mapping // Сохраняем гостевые идентификаторы в guest_user_mapping
if (session.guestId) { if (session.guestId) {
try { try {
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', '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] [userId, session.guestId]
); );
@@ -333,7 +333,7 @@ class IdentityService {
if (session.previousGuestId && session.previousGuestId !== session.guestId) { if (session.previousGuestId && session.previousGuestId !== session.guestId) {
try { try {
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', '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] [userId, session.previousGuestId]
); );
@@ -479,7 +479,7 @@ class IdentityService {
for (const [provider, providerId] of Object.entries(identities)) { for (const [provider, providerId] of Object.entries(identities)) {
if (!providerId) continue; if (!providerId) continue;
const result = await db.query( const result = await db.getQuery()(
`SELECT DISTINCT user_id `SELECT DISTINCT user_id
FROM user_identities FROM user_identities
WHERE provider = $1 AND provider_id = $2`, WHERE provider = $1 AND provider_id = $2`,
@@ -510,7 +510,7 @@ class IdentityService {
return { success: false, error: 'Missing required parameters' }; return { success: false, error: 'Missing required parameters' };
} }
const { provider: normalizedProvider, providerId: normalizedProviderId } = this.normalizeIdentity(provider, providerId); const { provider: normalizedProvider, providerId: normalizedProviderId } = this.normalizeIdentity(provider, providerId);
const result = await db.query( const result = await db.getQuery()(
`DELETE FROM user_identities WHERE user_id = $1 AND provider = $2 AND provider_id = $3`, `DELETE FROM user_identities WHERE user_id = $1 AND provider = $2 AND provider_id = $3`,
[userId, normalizedProvider, normalizedProviderId] [userId, normalizedProvider, normalizedProviderId]
); );

View File

@@ -1,7 +1,7 @@
const db = require('../db'); const db = require('../db');
async function getAllRpcProviders() { async function getAllRpcProviders() {
const { rows } = await db.query('SELECT * FROM rpc_providers ORDER BY id'); const { rows } = await db.getQuery()('SELECT * FROM rpc_providers ORDER BY id');
return rows; return rows;
} }

View File

@@ -51,7 +51,7 @@ class SessionService {
} }
// Получаем все гостевые ID для текущего пользователя из новой таблицы // Получаем все гостевые ID для текущего пользователя из новой таблицы
const guestIdsResult = await db.query( const guestIdsResult = await db.getQuery()(
'SELECT guest_id FROM guest_user_mapping WHERE user_id = $1', 'SELECT guest_id FROM guest_user_mapping WHERE user_id = $1',
[userId] [userId]
); );

View File

@@ -6,11 +6,21 @@ const verificationService = require('./verification-service');
const crypto = require('crypto'); const crypto = require('crypto');
let botInstance = null; let botInstance = null;
let telegramSettingsCache = null;
async function getTelegramSettings() {
if (telegramSettingsCache) return telegramSettingsCache;
const { rows } = await db.getQuery()('SELECT * FROM telegram_settings ORDER BY id LIMIT 1');
if (!rows.length) throw new Error('Telegram settings not found in DB');
telegramSettingsCache = rows[0];
return telegramSettingsCache;
}
// Создание и настройка бота // Создание и настройка бота
async function getBot() { async function getBot() {
if (!botInstance) { if (!botInstance) {
botInstance = new Telegraf(process.env.TELEGRAM_BOT_TOKEN); const settings = await getTelegramSettings();
botInstance = new Telegraf(settings.bot_token);
// Обработка команды /start // Обработка команды /start
botInstance.command('start', (ctx) => { botInstance.command('start', (ctx) => {
@@ -23,7 +33,7 @@ async function getBot() {
try { try {
// Получаем код верификации для всех активных кодов с провайдером telegram // Получаем код верификации для всех активных кодов с провайдером telegram
const codeResult = await db.query( const codeResult = await db.getQuery()(
`SELECT * FROM verification_codes `SELECT * FROM verification_codes
WHERE code = $1 WHERE code = $1
AND provider = 'telegram' AND provider = 'telegram'
@@ -44,14 +54,14 @@ async function getBot() {
let userRole = 'user'; // Роль по умолчанию let userRole = 'user'; // Роль по умолчанию
// Отмечаем код как использованный // Отмечаем код как использованный
await db.query('UPDATE verification_codes SET used = true WHERE id = $1', [ await db.getQuery()('UPDATE verification_codes SET used = true WHERE id = $1', [
verification.id, verification.id,
]); ]);
logger.info('Starting Telegram auth process for code:', code); logger.info('Starting Telegram auth process for code:', code);
// Проверяем, существует ли уже пользователь с таким Telegram ID // Проверяем, существует ли уже пользователь с таким Telegram ID
const existingTelegramUser = await db.query( const existingTelegramUser = await db.getQuery()(
`SELECT ui.user_id `SELECT ui.user_id
FROM user_identities ui FROM user_identities ui
WHERE ui.provider = 'telegram' AND ui.provider_id = $1`, WHERE ui.provider = 'telegram' AND ui.provider_id = $1`,
@@ -68,7 +78,7 @@ async function getBot() {
// Используем userId из кода верификации // Используем userId из кода верификации
userId = linkedUserId; userId = linkedUserId;
// Связываем Telegram с этим пользователем // Связываем Telegram с этим пользователем
await db.query( await db.getQuery()(
`INSERT INTO user_identities `INSERT INTO user_identities
(user_id, provider, provider_id, created_at) (user_id, provider, provider_id, created_at)
VALUES ($1, $2, $3, NOW())`, VALUES ($1, $2, $3, NOW())`,
@@ -81,7 +91,7 @@ async function getBot() {
// Проверяем, есть ли пользователь, связанный с гостевым идентификатором // Проверяем, есть ли пользователь, связанный с гостевым идентификатором
let existingUserWithGuestId = null; let existingUserWithGuestId = null;
if (providerId) { if (providerId) {
const guestUserResult = await db.query( const guestUserResult = await db.getQuery()(
`SELECT user_id FROM guest_user_mapping WHERE guest_id = $1`, `SELECT user_id FROM guest_user_mapping WHERE guest_id = $1`,
[providerId] [providerId]
); );
@@ -96,7 +106,7 @@ async function getBot() {
if (existingUserWithGuestId) { if (existingUserWithGuestId) {
// Используем существующего пользователя и добавляем ему Telegram идентификатор // Используем существующего пользователя и добавляем ему Telegram идентификатор
userId = existingUserWithGuestId; userId = existingUserWithGuestId;
await db.query( await db.getQuery()(
`INSERT INTO user_identities `INSERT INTO user_identities
(user_id, provider, provider_id, created_at) (user_id, provider, provider_id, created_at)
VALUES ($1, $2, $3, NOW())`, VALUES ($1, $2, $3, NOW())`,
@@ -105,14 +115,14 @@ async function getBot() {
logger.info(`Linked Telegram account ${ctx.from.id} to existing user ${userId}`); logger.info(`Linked Telegram account ${ctx.from.id} to existing user ${userId}`);
} else { } else {
// Создаем нового пользователя, если не нашли существующего // Создаем нового пользователя, если не нашли существующего
const userResult = await db.query( const userResult = await db.getQuery()(
'INSERT INTO users (created_at, role) VALUES (NOW(), $1) RETURNING id', 'INSERT INTO users (created_at, role) VALUES (NOW(), $1) RETURNING id',
['user'] ['user']
); );
userId = userResult.rows[0].id; userId = userResult.rows[0].id;
// Связываем Telegram с новым пользователем // Связываем Telegram с новым пользователем
await db.query( await db.getQuery()(
`INSERT INTO user_identities `INSERT INTO user_identities
(user_id, provider, provider_id, created_at) (user_id, provider, provider_id, created_at)
VALUES ($1, $2, $3, NOW())`, VALUES ($1, $2, $3, NOW())`,
@@ -121,7 +131,7 @@ async function getBot() {
// Если был гостевой ID, связываем его с новым пользователем // Если был гостевой ID, связываем его с новым пользователем
if (providerId) { if (providerId) {
await db.query( await db.getQuery()(
`INSERT INTO guest_user_mapping `INSERT INTO guest_user_mapping
(user_id, guest_id) (user_id, guest_id)
VALUES ($1, $2) VALUES ($1, $2)
@@ -147,15 +157,15 @@ async function getBot() {
logger.info(`[TelegramBot] Role for user ${userId} determined as: ${userRole}`); logger.info(`[TelegramBot] Role for user ${userId} determined as: ${userRole}`);
// Опционально: Обновить роль в таблице users // Опционально: Обновить роль в таблице 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) { 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(`[TelegramBot] Updated user role in DB to ${userRole}`); logger.info(`[TelegramBot] Updated user role in DB to ${userRole}`);
} }
} else { } else {
logger.info(`[TelegramBot] No linked wallet found for user ${userId}. Checking current DB role.`); logger.info(`[TelegramBot] No linked wallet found for user ${userId}. Checking current DB role.`);
// Если кошелька нет, берем текущую роль из базы // Если кошелька нет, берем текущую роль из базы
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) { if (currentUser.rows.length > 0) {
userRole = currentUser.rows[0].role; userRole = currentUser.rows[0].role;
} }
@@ -164,7 +174,7 @@ async function getBot() {
logger.error(`[TelegramBot] Error checking admin role for user ${userId}:`, roleCheckError); logger.error(`[TelegramBot] Error checking admin role for user ${userId}:`, roleCheckError);
// В случае ошибки берем роль из базы или оставляем 'user' // В случае ошибки берем роль из базы или оставляем 'user'
try { 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; } if (currentUser.rows.length > 0) { userRole = currentUser.rows[0].role; }
} catch (dbError) { /* ignore */ } } catch (dbError) { /* ignore */ }
} }
@@ -181,7 +191,7 @@ async function getBot() {
try { try {
// Ищем сессию, где есть userId и она не истекла (проверка expires_at) // Ищем сессию, где есть userId и она не истекла (проверка expires_at)
// Сортируем по expires_at DESC чтобы взять самую "свежую", если их несколько // Сортируем по expires_at DESC чтобы взять самую "свежую", если их несколько
const sessionResult = await db.query( const sessionResult = await db.getQuery()(
`SELECT sid FROM session `SELECT sid FROM session
WHERE sess ->> 'userId' = $1 WHERE sess ->> 'userId' = $1
AND expire > NOW() AND expire > NOW()
@@ -195,7 +205,7 @@ async function getBot() {
logger.info(`[telegramBot] Found active session ID ${activeSessionId} for user ${userId}`); logger.info(`[telegramBot] Found active session ID ${activeSessionId} for user ${userId}`);
// Обновляем найденную сессию в базе данных, добавляя/перезаписывая данные Telegram // Обновляем найденную сессию в базе данных, добавляя/перезаписывая данные Telegram
const updateResult = await db.query( const updateResult = await db.getQuery()(
`UPDATE session `UPDATE session
SET sess = (sess::jsonb || $1::jsonb)::json SET sess = (sess::jsonb || $1::jsonb)::json
WHERE sid = $2`, WHERE sid = $2`,
@@ -275,7 +285,7 @@ async function initTelegramAuth(session) {
const guestId = session.guestId || tempId; const guestId = session.guestId || tempId;
// Связываем гостевой ID с текущим пользователем // Связываем гостевой ID с текущим пользователем
await db.query( await db.getQuery()(
`INSERT INTO guest_user_mapping (user_id, guest_id) `INSERT INTO guest_user_mapping (user_id, guest_id)
VALUES ($1, $2) VALUES ($1, $2)
ON CONFLICT (guest_id) DO UPDATE SET user_id = $1`, ON CONFLICT (guest_id) DO UPDATE SET user_id = $1`,
@@ -298,9 +308,10 @@ async function initTelegramAuth(session) {
`[initTelegramAuth] Created verification code for guestId: ${session.guestId || tempId}${session.authenticated ? `, userId: ${session.userId}` : ''}` `[initTelegramAuth] Created verification code for guestId: ${session.guestId || tempId}${session.authenticated ? `, userId: ${session.userId}` : ''}`
); );
const settings = await getTelegramSettings();
return { return {
verificationCode: code, verificationCode: code,
botLink: `https://t.me/${process.env.TELEGRAM_BOT_USERNAME}`, botLink: `https://t.me/${settings.bot_username}`,
}; };
} catch (error) { } catch (error) {
logger.error('Error initializing Telegram auth:', error); logger.error('Error initializing Telegram auth:', error);
@@ -308,8 +319,13 @@ async function initTelegramAuth(session) {
} }
} }
function clearSettingsCache() {
telegramSettingsCache = null;
}
module.exports = { module.exports = {
getBot, getBot,
stopBot, stopBot,
initTelegramAuth, initTelegramAuth,
clearSettingsCache,
}; };

View File

@@ -29,14 +29,14 @@ class VerificationService {
// Если userId не указан, добавляем запись без ссылки на пользователя // Если userId не указан, добавляем запись без ссылки на пользователя
if (userId === null || userId === undefined) { if (userId === null || userId === undefined) {
await db.query( await db.getQuery()(
`INSERT INTO verification_codes `INSERT INTO verification_codes
(code, provider, provider_id, expires_at) (code, provider, provider_id, expires_at)
VALUES ($1, $2, $3, $4)`, VALUES ($1, $2, $3, $4)`,
[code, provider, providerId, expiresAt] [code, provider, providerId, expiresAt]
); );
} else { } else {
await db.query( await db.getQuery()(
`INSERT INTO verification_codes `INSERT INTO verification_codes
(code, provider, provider_id, user_id, expires_at) (code, provider, provider_id, user_id, expires_at)
VALUES ($1, $2, $3, $4, $5)`, VALUES ($1, $2, $3, $4, $5)`,
@@ -67,7 +67,7 @@ class VerificationService {
logger.info(`Normalized code: ${normalizedCode}`); logger.info(`Normalized code: ${normalizedCode}`);
// Проверим, есть ли такой код в базе (для отладки) // Проверим, есть ли такой код в базе (для отладки)
const checkResult = await db.query( const checkResult = await db.getQuery()(
`SELECT code FROM verification_codes `SELECT code FROM verification_codes
WHERE provider = $1 WHERE provider = $1
AND provider_id = $2 AND provider_id = $2
@@ -84,7 +84,7 @@ class VerificationService {
logger.warn(`No active codes found for ${provider}:${providerId}`); logger.warn(`No active codes found for ${provider}:${providerId}`);
} }
const result = await db.query( const result = await db.getQuery()(
`SELECT * FROM verification_codes `SELECT * FROM verification_codes
WHERE code = $1 WHERE code = $1
AND provider = $2 AND provider = $2
@@ -104,7 +104,10 @@ class VerificationService {
const verification = result.rows[0]; const verification = result.rows[0];
// Отмечаем код как использованный // Отмечаем код как использованный
await db.query('UPDATE verification_codes SET used = true WHERE id = $1', [verification.id]); await db.getQuery()(
'UPDATE verification_codes SET used = true WHERE id = $1',
[verification.id]
);
logger.info(`Code verified successfully for ${provider}:${providerId}`); logger.info(`Code verified successfully for ${provider}:${providerId}`);
return { return {
@@ -126,7 +129,7 @@ class VerificationService {
// Очистка истекших кодов // Очистка истекших кодов
async cleanupExpiredCodes() { async cleanupExpiredCodes() {
try { try {
const result = await db.query( const result = await db.getQuery()(
'DELETE FROM verification_codes WHERE expires_at <= NOW() RETURNING id' 'DELETE FROM verification_codes WHERE expires_at <= NOW() RETURNING id'
); );
logger.info(`Cleaned up ${result.rowCount} expired verification codes`); logger.info(`Cleaned up ${result.rowCount} expired verification codes`);

View File

@@ -21,7 +21,7 @@ function generateVerificationCode(length = 6) {
// Проверка существования идентификатора пользователя // Проверка существования идентификатора пользователя
async function checkUserIdentity(userId, provider, providerId) { async function checkUserIdentity(userId, provider, providerId) {
const result = await db.query( const result = await db.getQuery()(
'SELECT * FROM user_identities WHERE user_id = $1 AND provider = $2 AND provider_id = $3', 'SELECT * FROM user_identities WHERE user_id = $1 AND provider = $2 AND provider_id = $3',
[userId, provider, providerId] [userId, provider, providerId]
); );
@@ -31,7 +31,7 @@ async function checkUserIdentity(userId, provider, providerId) {
// Добавление новой идентификации // Добавление новой идентификации
async function addUserIdentity(userId, provider, providerId) { async function addUserIdentity(userId, provider, providerId) {
try { try {
await db.query( await db.getQuery()(
'INSERT INTO user_identities (user_id, provider, provider_id) VALUES ($1, $2, $3)', 'INSERT INTO user_identities (user_id, provider, provider_id) VALUES ($1, $2, $3)',
[userId, provider, providerId] [userId, provider, providerId]
); );

View File

@@ -12,6 +12,11 @@
resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz#42cc67c5baa407ac25059fcd7d405cc5ecdb0c33" resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz#42cc67c5baa407ac25059fcd7d405cc5ecdb0c33"
integrity sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg== integrity sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==
"@anthropic-ai/sdk@^0.51.0":
version "0.51.0"
resolved "https://registry.yarnpkg.com/@anthropic-ai/sdk/-/sdk-0.51.0.tgz#2cd022c47e0eb6f4d645d8e8ed9ee0f3c5745ea8"
integrity sha512-fAFC/uHhyzfw7rs65EPVV+scXDytGNm5BjttxHf6rP/YGvaBRKEvp2lwyuMigTwMI95neeG4bzrZigz7KCikjw==
"@colors/colors@1.5.0": "@colors/colors@1.5.0":
version "1.5.0" version "1.5.0"
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
@@ -354,6 +359,16 @@
resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d"
integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==
"@google/genai@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@google/genai/-/genai-1.0.1.tgz#ccef337f733f27fdc2e5bf741af51889e2847082"
integrity sha512-qf8sq9vpuKUeBKukAn43z2eC1I/Jw63b9wo6O+1x3EIroF3oDouJOtW1AzwvfO+9gzCPfLjuCUONhMKiBC8vkQ==
dependencies:
google-auth-library "^9.14.2"
ws "^8.18.0"
zod "^3.22.4"
zod-to-json-schema "^3.22.4"
"@humanfs/core@^0.19.1": "@humanfs/core@^0.19.1":
version "0.19.1" version "0.19.1"
resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77"
@@ -1223,6 +1238,11 @@ agent-base@6:
dependencies: dependencies:
debug "4" debug "4"
agent-base@^7.1.2:
version "7.1.3"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.3.tgz#29435eb821bc4194633a5b89e5bc4703bafc25a1"
integrity sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==
agentkeepalive@^4.2.1: agentkeepalive@^4.2.1:
version "4.6.0" version "4.6.0"
resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz#35f73e94b3f40bf65f105219c623ad19c136ea6a" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz#35f73e94b3f40bf65f105219c623ad19c136ea6a"
@@ -1504,7 +1524,7 @@ base-x@^3.0.2:
dependencies: dependencies:
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
base64-js@^1.5.1: base64-js@^1.3.0, base64-js@^1.5.1:
version "1.5.1" version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
@@ -1516,6 +1536,11 @@ bcrypt-pbkdf@^1.0.0:
dependencies: dependencies:
tweetnacl "^0.14.3" tweetnacl "^0.14.3"
bignumber.js@^9.0.0:
version "9.3.0"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.3.0.tgz#bdba7e2a4c1a2eba08290e8dcad4f36393c92acd"
integrity sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==
binary-extensions@^2.0.0, binary-extensions@^2.2.0: binary-extensions@^2.0.0, binary-extensions@^2.2.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
@@ -1671,6 +1696,11 @@ buffer-alloc@^1.2.0:
buffer-alloc-unsafe "^1.1.0" buffer-alloc-unsafe "^1.1.0"
buffer-fill "^1.0.0" buffer-fill "^1.0.0"
buffer-equal-constant-time@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==
buffer-fill@^1.0.0: buffer-fill@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
@@ -2309,6 +2339,13 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0" jsbn "~0.1.0"
safer-buffer "^2.1.0" safer-buffer "^2.1.0"
ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
dependencies:
safe-buffer "^5.0.1"
ee-first@1.1.1: ee-first@1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -2842,7 +2879,7 @@ express@^4.21.2:
utils-merge "1.0.1" utils-merge "1.0.1"
vary "~1.1.2" vary "~1.1.2"
extend@~3.0.2: extend@^3.0.2, extend@~3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
@@ -3117,6 +3154,26 @@ functions-have-names@^1.2.3:
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
gaxios@^6.0.0, gaxios@^6.1.1:
version "6.7.1"
resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-6.7.1.tgz#ebd9f7093ede3ba502685e73390248bb5b7f71fb"
integrity sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==
dependencies:
extend "^3.0.2"
https-proxy-agent "^7.0.1"
is-stream "^2.0.0"
node-fetch "^2.6.9"
uuid "^9.0.1"
gcp-metadata@^6.1.0:
version "6.1.1"
resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-6.1.1.tgz#f65aa69f546bc56e116061d137d3f5f90bdec494"
integrity sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==
dependencies:
gaxios "^6.1.1"
google-logging-utils "^0.0.2"
json-bigint "^1.0.0"
get-caller-file@^2.0.5: get-caller-file@^2.0.5:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
@@ -3295,6 +3352,23 @@ globby@^10.0.1:
merge2 "^1.2.3" merge2 "^1.2.3"
slash "^3.0.0" slash "^3.0.0"
google-auth-library@^9.14.2:
version "9.15.1"
resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-9.15.1.tgz#0c5d84ed1890b2375f1cd74f03ac7b806b392928"
integrity sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==
dependencies:
base64-js "^1.3.0"
ecdsa-sig-formatter "^1.0.11"
gaxios "^6.1.1"
gcp-metadata "^6.1.0"
gtoken "^7.0.0"
jws "^4.0.0"
google-logging-utils@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/google-logging-utils/-/google-logging-utils-0.0.2.tgz#5fd837e06fa334da450433b9e3e1870c1594466a"
integrity sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==
gopd@^1.0.1, gopd@^1.2.0: gopd@^1.0.1, gopd@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
@@ -3305,6 +3379,14 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0:
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
gtoken@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-7.1.0.tgz#d61b4ebd10132222817f7222b1e6064bd463fc26"
integrity sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==
dependencies:
gaxios "^6.0.0"
jws "^4.0.0"
handlebars@^4.0.1: handlebars@^4.0.1:
version "4.7.8" version "4.7.8"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.8.tgz#41c42c18b1be2365439188c77c6afae71c0cd9e9" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.8.tgz#41c42c18b1be2365439188c77c6afae71c0cd9e9"
@@ -3550,6 +3632,14 @@ https-proxy-agent@^5.0.0:
agent-base "6" agent-base "6"
debug "4" debug "4"
https-proxy-agent@^7.0.1:
version "7.0.6"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9"
integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==
dependencies:
agent-base "^7.1.2"
debug "4"
humanize-ms@^1.2.1: humanize-ms@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
@@ -3951,6 +4041,13 @@ jsbn@~0.1.0:
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==
json-bigint@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1"
integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==
dependencies:
bignumber.js "^9.0.0"
json-buffer@3.0.1: json-buffer@3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
@@ -4027,6 +4124,23 @@ jsprim@^2.0.2:
json-schema "0.4.0" json-schema "0.4.0"
verror "1.10.0" verror "1.10.0"
jwa@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804"
integrity sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==
dependencies:
buffer-equal-constant-time "^1.0.1"
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
jws@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4"
integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==
dependencies:
jwa "^2.0.0"
safe-buffer "^5.0.1"
keccak@^3.0.0, keccak@^3.0.2: keccak@^3.0.0, keccak@^3.0.2:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.4.tgz#edc09b89e633c0549da444432ecf062ffadee86d" resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.4.tgz#edc09b89e633c0549da444432ecf062ffadee86d"
@@ -4520,7 +4634,7 @@ node-emoji@^1.10.0:
dependencies: dependencies:
lodash "^4.17.21" lodash "^4.17.21"
node-fetch@^2.6.7, node-fetch@^2.7.0: node-fetch@^2.6.7, node-fetch@^2.6.9, node-fetch@^2.7.0:
version "2.7.0" version "2.7.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
@@ -4663,6 +4777,19 @@ one-time@^1.0.0:
dependencies: dependencies:
fn.name "1.x.x" fn.name "1.x.x"
openai@^4.102.0:
version "4.102.0"
resolved "https://registry.yarnpkg.com/openai/-/openai-4.102.0.tgz#fcf09d2ad2b33eb399fe79bb43df1872bf2a15c8"
integrity sha512-CWk15CMhPSHNZnjz+6rwVYV551xaC8CwOd7/zxImrC1btEo37dX/Ii5tBKWfqqxqyzpJ6p3Y4bICzzKhW03WhQ==
dependencies:
"@types/node" "^18.11.18"
"@types/node-fetch" "^2.6.4"
abort-controller "^3.0.0"
agentkeepalive "^4.2.1"
form-data-encoder "1.7.2"
formdata-node "^4.3.2"
node-fetch "^2.6.7"
openai@^4.93.0: openai@^4.93.0:
version "4.96.0" version "4.96.0"
resolved "https://registry.yarnpkg.com/openai/-/openai-4.96.0.tgz#d1a821e99949ac2c55709f4e28e18bb1d9fd8ef9" resolved "https://registry.yarnpkg.com/openai/-/openai-4.96.0.tgz#d1a821e99949ac2c55709f4e28e18bb1d9fd8ef9"
@@ -6355,6 +6482,11 @@ uuid@^10.0.0:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
uuid@^9.0.1:
version "9.0.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
v8-compile-cache-lib@^3.0.1: v8-compile-cache-lib@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
@@ -6595,7 +6727,7 @@ write-file-atomic@3.0.3:
signal-exit "^3.0.2" signal-exit "^3.0.2"
typedarray-to-buffer "^3.1.5" typedarray-to-buffer "^3.1.5"
ws@8.17.1, ws@8.18.1, ws@^7.4.6, ws@^8.18.1: ws@8.17.1, ws@8.18.1, ws@^7.4.6, ws@^8.18.0, ws@^8.18.1:
version "8.18.1" version "8.18.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb" resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb"
integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w== integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==
@@ -6653,7 +6785,7 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zod-to-json-schema@^3.22.3, zod-to-json-schema@^3.22.5, zod-to-json-schema@^3.24.1: zod-to-json-schema@^3.22.3, zod-to-json-schema@^3.22.4, zod-to-json-schema@^3.22.5, zod-to-json-schema@^3.24.1:
version "3.24.5" version "3.24.5"
resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz#d1095440b147fb7c2093812a53c54df8d5df50a3" resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz#d1095440b147fb7c2093812a53c54df8d5df50a3"
integrity sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g== integrity sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==

View File

@@ -142,7 +142,7 @@ const props = defineProps({
isLoadingTokens: Boolean isLoadingTokens: Boolean
}); });
const emit = defineEmits(['update:modelValue', 'wallet-auth', 'disconnect-wallet', 'telegram-auth', 'email-auth']); const emit = defineEmits(['update:modelValue', 'wallet-auth', 'disconnect-wallet', 'telegram-auth', 'email-auth', 'cancel-email-auth']);
const { deleteIdentity } = useAuth(); const { deleteIdentity } = useAuth();

View File

@@ -36,24 +36,24 @@
</template> </template>
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import axios from '@/api/axios'; import axios from '@/api/axios';
import { useAuth } from '@/composables/useAuth'; import { useAuth } from '@/composables/useAuth';
const emit = defineEmits(['close', 'success']); const emit = defineEmits(['close', 'success']);
const { linkIdentity } = useAuth(); const { linkIdentity } = useAuth();
const email = ref(''); const email = ref('');
const code = ref(''); const code = ref('');
const error = ref(''); const error = ref('');
const isLoading = ref(false); const isLoading = ref(false);
const showVerification = ref(false); const showVerification = ref(false);
const isValidEmail = computed(() => { const isValidEmail = computed(() => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value); return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value);
}); });
const requestCode = async () => { const requestCode = async () => {
try { try {
isLoading.value = true; isLoading.value = true;
error.value = ''; error.value = '';
@@ -70,9 +70,9 @@ const requestCode = async () => {
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
}; };
const verifyCode = async () => { const verifyCode = async () => {
try { try {
isLoading.value = true; isLoading.value = true;
error.value = ''; error.value = '';
@@ -90,14 +90,14 @@ const verifyCode = async () => {
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
}; };
const resetForm = () => { const resetForm = () => {
email.value = ''; email.value = '';
code.value = ''; code.value = '';
error.value = ''; error.value = '';
showVerification.value = false; showVerification.value = false;
}; };
</script> </script>
<style scoped> <style scoped>

View File

@@ -427,48 +427,16 @@ export function useAuth() {
/** /**
* Связывает новый идентификатор с текущим аккаунтом пользователя * Связывает новый идентификатор с текущим аккаунтом пользователя
* @param {string} provider - Тип идентификатора (wallet, email, telegram) * @param {string} type - Тип идентификатора (wallet, email, telegram)
* @param {string} providerId - Значение идентификатора * @param {string} value - Значение идентификатора
* @returns {Promise<Object>} - Результат операции * @returns {Promise<Object>} - Результат операции
*/ */
const linkIdentity = async (provider, providerId) => { const linkIdentity = async (type, value) => {
try { const response = await axios.post('/api/link', {
if (!isAuthenticated.value) { type,
console.error('Невозможно связать идентификатор: пользователь не аутентифицирован'); value,
return { success: false, error: 'Пользователь не аутентифицирован' };
}
const response = await axios.post('/api/auth/identities/link', {
type: provider,
value: providerId,
}); });
if (response.data.success) {
// Обновляем локальные данные при необходимости
if (provider === 'wallet') {
address.value = providerId;
isAdmin.value = response.data.isAdmin || false;
} else if (provider === 'telegram') {
telegramId.value = providerId;
} else if (provider === 'email') {
email.value = providerId;
}
// Обновляем список идентификаторов
await updateIdentities();
console.log(`Идентификатор ${provider} успешно связан с аккаунтом`);
return { success: true };
}
return response.data; return response.data;
} catch (error) {
console.error('Ошибка при связывании идентификатора:', error);
return {
success: false,
error: error.response?.data?.error || error.message,
};
}
}; };
/** /**
@@ -478,16 +446,8 @@ export function useAuth() {
* @returns {Promise<Object>} - Результат операции * @returns {Promise<Object>} - Результат операции
*/ */
const deleteIdentity = async (provider, providerId) => { const deleteIdentity = async (provider, providerId) => {
try { const response = await axios.delete(`/api/${provider}/${encodeURIComponent(providerId)}`);
const response = await axios.delete(`/api/identities/${provider}/${encodeURIComponent(providerId)}`); return response.data;
if (response.data.success) {
await updateIdentities();
return { success: true };
}
return { success: false, error: response.data.error };
} catch (error) {
return { success: false, error: error.response?.data?.error || error.message };
}
}; };
return { return {

View File

@@ -0,0 +1,203 @@
<template>
<div class="ai-provider-settings settings-panel">
<h2>{{ label }}</h2>
<p class="desc">{{ description }}</p>
<form @submit.prevent="onSave">
<div v-if="showApiKey">
<label>API Key:</label>
<input type="password" v-model="apiKey" :placeholder="apiKeyPlaceholder" />
<button type="button" class="verify-btn" @click="onVerify" :disabled="verifying">Verify</button>
<span v-if="verifyStatus === true" class="ok"></span>
<span v-if="verifyStatus === false" class="error">Ошибка: {{ verifyError }}</span>
</div>
<div v-if="showBaseUrl">
<label>Base URL:</label>
<input type="text" v-model="baseUrl" :placeholder="baseUrlPlaceholder" />
</div>
<div v-if="models.length">
<label>Модель:</label>
<select v-model="selectedModel">
<option v-for="model in models" :key="model.id || model" :value="model.id || model">
{{ model.id || model }}
</option>
</select>
</div>
<div class="actions">
<button type="submit" :disabled="saving">Сохранить</button>
<button type="button" @click="onDelete" v-if="hasSettings">Удалить ключ</button>
<button type="button" @click="$emit('cancel')">Закрыть</button>
</div>
<div v-if="saveStatus === true" class="ok">Сохранено!</div>
<div v-if="saveStatus === false" class="error">Ошибка: {{ saveError }}</div>
</form>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import axios from 'axios';
const props = defineProps({
provider: { type: String, required: true },
label: { type: String, required: true },
description: { type: String, default: '' },
showApiKey: { type: Boolean, default: true },
showBaseUrl: { type: Boolean, default: true },
apiKeyPlaceholder: { type: String, default: '' },
baseUrlPlaceholder: { type: String, default: '' },
});
const apiKey = ref('');
const baseUrl = ref('');
const selectedModel = ref('');
const models = ref([]);
const hasSettings = ref(false);
const verifying = ref(false);
const verifyStatus = ref(null);
const verifyError = ref('');
const saving = ref(false);
const saveStatus = ref(null);
const saveError = ref('');
async function loadSettings() {
try {
const { data } = await axios.get(`/api/settings/ai-settings/${props.provider}`);
if (data.settings) {
apiKey.value = data.settings.api_key || '';
baseUrl.value = data.settings.base_url || '';
selectedModel.value = data.settings.selected_model || '';
hasSettings.value = true;
if (apiKey.value || props.provider === 'ollama') {
await loadModels();
}
} else {
hasSettings.value = false;
}
} catch (e) {
hasSettings.value = false;
}
}
async function loadModels() {
try {
const { data } = await axios.get(`/api/settings/ai-settings/${props.provider}/models`);
models.value = data.models || [];
if (!selectedModel.value && models.value.length) {
selectedModel.value = models.value[0].id || models.value[0];
}
} catch (e) {
models.value = [];
}
}
async function onVerify() {
verifying.value = true;
verifyStatus.value = null;
verifyError.value = '';
try {
const { data } = await axios.post(`/api/settings/ai-settings/${props.provider}/verify`, {
api_key: apiKey.value,
base_url: baseUrl.value,
});
verifyStatus.value = data.success;
if (data.success) {
await loadModels();
}
} catch (e) {
verifyStatus.value = false;
verifyError.value = e.response?.data?.error || e.message;
} finally {
verifying.value = false;
}
}
async function onSave() {
saving.value = true;
saveStatus.value = null;
saveError.value = '';
try {
await axios.put(`/api/settings/ai-settings/${props.provider}`, {
api_key: apiKey.value,
base_url: baseUrl.value,
selected_model: selectedModel.value,
});
saveStatus.value = true;
hasSettings.value = true;
} catch (e) {
saveStatus.value = false;
saveError.value = e.response?.data?.error || e.message;
} finally {
saving.value = false;
}
}
async function onDelete() {
await axios.delete(`/api/settings/ai-settings/${props.provider}`);
apiKey.value = '';
baseUrl.value = '';
selectedModel.value = '';
models.value = [];
hasSettings.value = false;
}
onMounted(loadSettings);
watch([apiKey, baseUrl], () => {
verifyStatus.value = null;
verifyError.value = '';
saveStatus.value = null;
saveError.value = '';
});
</script>
<style scoped>
.ai-provider-settings.settings-panel {
padding: var(--block-padding);
background-color: var(--color-light);
border-radius: var(--radius-md);
margin-top: var(--spacing-lg);
max-width: 500px;
}
.desc {
color: #666;
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.3rem;
font-weight: 500;
}
input[type="password"], input[type="text"], select {
width: 100%;
padding: 0.5rem 1rem;
border-radius: 6px;
border: 1px solid #ccc;
font-size: 1rem;
margin-bottom: 1rem;
}
.verify-btn {
margin-left: 0.5rem;
background: var(--color-primary);
color: #fff;
border: none;
border-radius: 6px;
padding: 0.4rem 1.2rem;
cursor: pointer;
font-size: 1rem;
transition: background 0.2s;
}
.verify-btn:hover {
background: var(--color-primary-dark);
}
.actions {
display: flex;
gap: 1rem;
margin-top: 1.5rem;
}
.ok {
color: #2cae4f;
margin-left: 1rem;
}
.error {
color: #d32f2f;
margin-left: 1rem;
}
</style>

View File

@@ -1,26 +1,97 @@
<template> <template>
<div class="ai-settings settings-panel"> <div class="ai-settings settings-panel">
<h2>Интеграции</h2> <h2>Интеграции</h2>
<div class="integration-blocks"> <div class="integration-blocks" v-if="!showProvider && !showEmailSettings && !showTelegramSettings">
<div class="integration-block">
<h3>OpenAI</h3>
<p>Интеграция с OpenAI (GPT-4, GPT-3.5 и др.).</p>
<button class="details-btn" @click="showProvider = 'openai'">Подробнее</button>
</div>
<div class="integration-block">
<h3>Anthropic</h3>
<p>Интеграция с Anthropic Claude (Claude 3 и др.).</p>
<button class="details-btn" @click="showProvider = 'anthropic'">Подробнее</button>
</div>
<div class="integration-block">
<h3>Google Gemini</h3>
<p>Интеграция с Google Gemini (Gemini 1.5, 1.0 и др.).</p>
<button class="details-btn" @click="showProvider = 'google'">Подробнее</button>
</div>
<div class="integration-block">
<h3>Ollama</h3>
<p>Локальные open-source модели через Ollama.</p>
<button class="details-btn" @click="showProvider = 'ollama'">Подробнее</button>
</div>
<div class="integration-block"> <div class="integration-block">
<h3>Telegram</h3> <h3>Telegram</h3>
<p>Интеграция с Telegram-ботом для уведомлений и авторизации.</p> <p>Интеграция с Telegram-ботом для уведомлений и авторизации.</p>
<button class="details-btn" @click="goToTelegram">Подробнее</button> <button class="details-btn" @click="showTelegramSettings = true">Подробнее</button>
</div> </div>
<div class="integration-block"> <div class="integration-block">
<h3>Email</h3> <h3>Email</h3>
<p>Интеграция с Email для отправки писем и уведомлений.</p> <p>Интеграция с Email для отправки писем и уведомлений.</p>
<button class="details-btn" @click="goToEmail">Подробнее</button> <button class="details-btn" @click="showEmailSettings = true">Подробнее</button>
</div> </div>
</div> </div>
<AIProviderSettings
v-if="showProvider"
:provider="showProvider"
:label="providerLabels[showProvider].label"
:description="providerLabels[showProvider].description"
:apiKeyPlaceholder="providerLabels[showProvider].apiKeyPlaceholder"
:baseUrlPlaceholder="providerLabels[showProvider].baseUrlPlaceholder"
:showApiKey="providerLabels[showProvider].showApiKey"
:showBaseUrl="providerLabels[showProvider].showBaseUrl"
@cancel="showProvider = null"
/>
<TelegramSettingsView v-if="showTelegramSettings" @cancel="showTelegramSettings = false" />
<EmailSettingsView v-if="showEmailSettings" @cancel="showEmailSettings = false" />
</div> </div>
</template> </template>
<script setup> <script setup>
import { useRouter } from 'vue-router'; import { ref } from 'vue';
const router = useRouter(); import AIProviderSettings from './AIProviderSettings.vue';
const goToTelegram = () => router.push({ name: 'settings-telegram' }); import TelegramSettingsView from './TelegramSettingsView.vue';
const goToEmail = () => router.push({ name: 'settings-email' }); import EmailSettingsView from './EmailSettingsView.vue';
const showProvider = ref(null);
const showTelegramSettings = ref(false);
const showEmailSettings = ref(false);
const providerLabels = {
openai: {
label: 'OpenAI API Key',
description: 'Введите OpenAI API Key и (опционально) Base URL для кастомных endpoint.',
apiKeyPlaceholder: 'sk-...',
baseUrlPlaceholder: 'https://api.openai.com/v1',
showApiKey: true,
showBaseUrl: true,
},
anthropic: {
label: 'Anthropic API Key',
description: 'Введите Anthropic API Key и (опционально) Base URL.',
apiKeyPlaceholder: '...',
baseUrlPlaceholder: 'https://api.anthropic.com/v1',
showApiKey: true,
showBaseUrl: true,
},
google: {
label: 'Google Gemini API Key',
description: 'Введите Google Gemini API Key и (опционально) Base URL.',
apiKeyPlaceholder: '...',
baseUrlPlaceholder: 'https://generativelanguage.googleapis.com/v1beta',
showApiKey: true,
showBaseUrl: true,
},
ollama: {
label: 'Ollama (локальные модели)',
description: 'Настройка Ollama для локальных open-source моделей. Ключ не требуется.',
apiKeyPlaceholder: '',
baseUrlPlaceholder: 'http://localhost:11434',
showApiKey: false,
showBaseUrl: true,
},
};
</script> </script>
<style scoped> <style scoped>

View File

@@ -0,0 +1,171 @@
<template>
<div class="db-settings settings-panel">
<h2>Настройки базы данных</h2>
<form v-if="editMode" @submit.prevent="saveDbSettings" class="settings-form">
<div class="form-group">
<label for="dbHost">Host</label>
<input id="dbHost" v-model="form.dbHost" type="text" required />
</div>
<div class="form-group">
<label for="dbPort">Port</label>
<input id="dbPort" v-model.number="form.dbPort" type="number" required />
</div>
<div class="form-group">
<label for="dbName">Database</label>
<input id="dbName" v-model="form.dbName" type="text" required />
</div>
<div class="form-group">
<label for="dbUser">User</label>
<input id="dbUser" v-model="form.dbUser" type="text" required />
</div>
<div class="form-group">
<label for="dbPassword">Password</label>
<input id="dbPassword" v-model="form.dbPassword" type="password" :placeholder="form.dbPassword ? 'Изменить пароль' : 'Введите пароль'" />
</div>
<button type="submit" class="save-btn">Сохранить</button>
<button type="button" class="cancel-btn" @click="cancelEdit">Отмена</button>
</form>
<div v-else class="settings-view">
<div class="view-row"><span>Host:</span> <b>{{ form.dbHost }}</b></div>
<div class="view-row"><span>Port:</span> <b>{{ form.dbPort }}</b></div>
<div class="view-row"><span>Database:</span> <b>{{ form.dbName }}</b></div>
<div class="view-row"><span>User:</span> <b>{{ form.dbUser }}</b></div>
<div class="view-row"><span>Password:</span> <b></b></div>
<button type="button" class="edit-btn" @click="editMode = true">Изменить</button>
<button type="button" class="cancel-btn" @click="$emit('cancel')">Закрыть</button>
</div>
</div>
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue';
import api from '@/api/axios';
const form = reactive({
dbHost: '',
dbPort: 5432,
dbName: '',
dbUser: '',
dbPassword: ''
});
const original = reactive({});
const editMode = ref(false);
const loadDbSettings = async () => {
try {
const res = await api.get('/api/db-settings');
if (res.data.success) {
const s = res.data.settings;
form.dbHost = s.db_host;
form.dbPort = s.db_port;
form.dbName = s.db_name;
form.dbUser = s.db_user;
form.dbPassword = '';
Object.assign(original, JSON.parse(JSON.stringify(form)));
}
} catch (e) {
// обработка ошибки
}
};
onMounted(async () => {
await loadDbSettings();
editMode.value = false;
});
const saveDbSettings = async () => {
try {
await api.put('/api/db-settings', {
db_host: form.dbHost,
db_port: form.dbPort,
db_name: form.dbName,
db_user: form.dbUser,
db_password: form.dbPassword || undefined
});
alert('Настройки базы данных сохранены');
form.dbPassword = '';
Object.assign(original, JSON.parse(JSON.stringify(form)));
editMode.value = false;
} catch (e) {
alert('Ошибка сохранения настроек базы данных');
}
};
const cancelEdit = () => {
Object.assign(form, JSON.parse(JSON.stringify(original)));
form.dbPassword = '';
editMode.value = false;
};
</script>
<style scoped>
.settings-panel {
padding: var(--block-padding);
background-color: var(--color-light);
border-radius: var(--radius-md);
margin-top: var(--spacing-lg);
max-width: 500px;
}
.settings-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.save-btn {
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;
}
.save-btn:hover {
background: var(--color-primary-dark);
}
.cancel-btn {
background: #eee;
color: #333;
border: none;
border-radius: 6px;
padding: 0.5rem 1.5rem;
cursor: pointer;
font-size: 1rem;
margin-left: 1rem;
}
.settings-view {
display: flex;
flex-direction: column;
gap: 1.2rem;
}
.view-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 1rem;
background: #f8f8f8;
border-radius: 4px;
padding: 0.5rem 1rem;
}
.edit-btn {
background: var(--color-primary);
color: #fff;
border: none;
border-radius: 6px;
padding: 0.5rem 1.5rem;
cursor: pointer;
font-size: 1rem;
align-self: flex-end;
margin-top: 1.5rem;
transition: background 0.2s;
}
.edit-btn:hover {
background: var(--color-primary-dark);
}
</style>

View File

@@ -1,40 +1,115 @@
<template> <template>
<div class="email-settings settings-panel"> <div class="email-settings settings-panel">
<h2>Настройки Email</h2> <h2>Настройки Email</h2>
<form @submit.prevent="saveEmailSettings" class="settings-form"> <form v-if="editMode" @submit.prevent="saveEmailSettings" class="settings-form">
<div class="form-group"> <div class="form-group">
<label for="smtpHost">SMTP Host</label> <label for="smtpHost">SMTP Host</label>
<input id="smtpHost" v-model="form.smtpHost" type="text" required /> <input id="smtpHost" v-model="form.smtpHost" type="text" required />
</div> </div>
<div class="form-group">
<label for="smtpPort">SMTP Port</label>
<input id="smtpPort" v-model.number="form.smtpPort" type="number" required />
</div>
<div class="form-group"> <div class="form-group">
<label for="smtpUser">SMTP User</label> <label for="smtpUser">SMTP User</label>
<input id="smtpUser" v-model="form.smtpUser" type="text" required /> <input id="smtpUser" v-model="form.smtpUser" type="text" required />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="smtpPassword">SMTP Password</label> <label for="smtpPassword">SMTP Password</label>
<input id="smtpPassword" v-model="form.smtpPassword" type="password" required /> <input id="smtpPassword" v-model="form.smtpPassword" type="password" :placeholder="form.smtpPassword ? 'Изменить пароль' : 'Введите пароль'" />
</div>
<div class="form-group">
<label for="imapHost">IMAP Host</label>
<input id="imapHost" v-model="form.imapHost" type="text" required />
</div>
<div class="form-group">
<label for="imapPort">IMAP Port</label>
<input id="imapPort" v-model.number="form.imapPort" type="number" required />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="fromEmail">From Email</label> <label for="fromEmail">From Email</label>
<input id="fromEmail" v-model="form.fromEmail" type="email" required /> <input id="fromEmail" v-model="form.fromEmail" type="email" required />
</div> </div>
<button type="submit" class="save-btn">Сохранить</button> <button type="submit" class="save-btn">Сохранить</button>
<button type="button" class="cancel-btn" @click="cancelEdit">Отмена</button>
</form> </form>
<div v-else class="settings-view">
<div class="view-row"><span>SMTP Host:</span> <b>{{ form.smtpHost }}</b></div>
<div class="view-row"><span>SMTP Port:</span> <b>{{ form.smtpPort }}</b></div>
<div class="view-row"><span>SMTP User:</span> <b>{{ form.smtpUser }}</b></div>
<div class="view-row"><span>IMAP Host:</span> <b>{{ form.imapHost }}</b></div>
<div class="view-row"><span>IMAP Port:</span> <b>{{ form.imapPort }}</b></div>
<div class="view-row"><span>From Email:</span> <b>{{ form.fromEmail }}</b></div>
<button type="button" class="edit-btn" @click="editMode = true">Изменить</button>
<button type="button" class="cancel-btn" @click="$emit('cancel')">Закрыть</button>
</div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { reactive } from 'vue'; import { reactive, ref, onMounted } from 'vue';
// TODO: Импортировать API для сохранения import api from '@/api/axios';
const form = reactive({ const form = reactive({
smtpHost: '', smtpHost: '',
smtpPort: 465,
smtpUser: '', smtpUser: '',
smtpPassword: '', smtpPassword: '',
imapHost: '',
imapPort: 993,
fromEmail: '' fromEmail: ''
}); });
const original = reactive({});
const editMode = ref(false);
const loadEmailSettings = async () => {
try {
const res = await api.get('/api/email-settings');
if (res.data.success) {
const s = res.data.settings;
form.smtpHost = s.smtp_host;
form.smtpPort = s.smtp_port;
form.smtpUser = s.smtp_user;
form.imapHost = s.imap_host || '';
form.imapPort = s.imap_port || 993;
form.fromEmail = s.from_email;
form.smtpPassword = '';
Object.assign(original, JSON.parse(JSON.stringify(form)));
}
} catch (e) {
// обработка ошибки
}
};
onMounted(async () => {
await loadEmailSettings();
editMode.value = false;
});
const saveEmailSettings = async () => { const saveEmailSettings = async () => {
// TODO: Реализовать вызов API для сохранения try {
alert('Настройки Email сохранены (заглушка)'); await api.put('/api/email-settings', {
smtp_host: form.smtpHost,
smtp_port: form.smtpPort,
smtp_user: form.smtpUser,
smtp_password: form.smtpPassword || undefined,
imap_host: form.imapHost,
imap_port: form.imapPort,
from_email: form.fromEmail
});
alert('Настройки Email сохранены');
form.smtpPassword = '';
Object.assign(original, JSON.parse(JSON.stringify(form)));
editMode.value = false;
} catch (e) {
alert('Ошибка сохранения email-настроек');
}
};
const cancelEdit = () => {
Object.assign(form, JSON.parse(JSON.stringify(original)));
form.smtpPassword = '';
editMode.value = false;
}; };
</script> </script>
@@ -69,4 +144,43 @@ const saveEmailSettings = async () => {
.save-btn:hover { .save-btn:hover {
background: var(--color-primary-dark); background: var(--color-primary-dark);
} }
.cancel-btn {
background: #eee;
color: #333;
border: none;
border-radius: 6px;
padding: 0.5rem 1.5rem;
cursor: pointer;
font-size: 1rem;
margin-left: 1rem;
}
.settings-view {
display: flex;
flex-direction: column;
gap: 1.2rem;
}
.view-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 1rem;
background: #f8f8f8;
border-radius: 4px;
padding: 0.5rem 1rem;
}
.edit-btn {
background: var(--color-primary);
color: #fff;
border: none;
border-radius: 6px;
padding: 0.5rem 1.5rem;
cursor: pointer;
font-size: 1rem;
align-self: flex-end;
margin-top: 1.5rem;
transition: background 0.2s;
}
.edit-btn:hover {
background: var(--color-primary-dark);
}
</style> </style>

View File

@@ -0,0 +1,63 @@
<template>
<div class="ollama-settings settings-panel">
<h2>Настройки Ollama</h2>
<div class="current-model-block">
<span>Текущая модель:</span>
<b>{{ currentModel }}</b>
</div>
<div class="select-model-block">
<label for="ollamaModel">Доступные модели для загрузки:</label>
<select id="ollamaModel" v-model="selectedModel">
<option v-for="model in availableModels" :key="model" :value="model">{{ model }}</option>
</select>
</div>
<button class="close-btn" @click="$emit('cancel')">Закрыть</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
// TODO: заменить на реальный API Ollama
const currentModel = ref('qwen2.5');
const availableModels = ref(['qwen2.5', 'llama3', 'mistral', 'phi3', 'gemma']);
const selectedModel = ref(currentModel.value);
onMounted(() => {
// Здесь будет запрос к Ollama для получения списка моделей и текущей
});
</script>
<style scoped>
.ollama-settings.settings-panel {
padding: var(--block-padding);
background-color: var(--color-light);
border-radius: var(--radius-md);
margin-top: var(--spacing-lg);
max-width: 500px;
}
.current-model-block {
margin-bottom: 1.5rem;
font-size: 1.1rem;
}
.select-model-block {
margin-bottom: 2rem;
}
select {
padding: 0.5rem 1rem;
border-radius: 6px;
border: 1px solid #ccc;
font-size: 1rem;
}
.close-btn {
background: #eee;
color: #333;
border: none;
border-radius: 6px;
padding: 0.5rem 1.5rem;
cursor: pointer;
font-size: 1rem;
}
.close-btn:hover {
background: #ddd;
}
</style>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="telegram-settings settings-panel"> <div class="telegram-settings settings-panel">
<h2>Настройки Telegram</h2> <h2>Настройки Telegram</h2>
<form @submit.prevent="saveTelegramSettings" class="settings-form"> <form v-if="editMode" @submit.prevent="saveTelegramSettings" class="settings-form">
<div class="form-group"> <div class="form-group">
<label for="botToken">Bot Token</label> <label for="botToken">Bot Token</label>
<input id="botToken" v-model="form.botToken" type="text" required /> <input id="botToken" v-model="form.botToken" type="text" required />
@@ -11,20 +11,66 @@
<input id="botUsername" v-model="form.botUsername" type="text" required /> <input id="botUsername" v-model="form.botUsername" type="text" required />
</div> </div>
<button type="submit" class="save-btn">Сохранить</button> <button type="submit" class="save-btn">Сохранить</button>
<button type="button" class="cancel-btn" @click="cancelEdit">Отмена</button>
</form> </form>
<div v-else class="settings-view">
<div class="view-row"><span>Bot Token:</span> <b></b></div>
<div class="view-row"><span>Bot Username:</span> <b>{{ form.botUsername }}</b></div>
<button type="button" class="edit-btn" @click="editMode = true">Изменить</button>
<button type="button" class="cancel-btn" @click="$emit('cancel')">Закрыть</button>
</div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { reactive } from 'vue'; import { reactive, ref, onMounted } from 'vue';
// TODO: Импортировать API для сохранения import api from '@/api/axios';
const form = reactive({ const form = reactive({
botToken: '', botToken: '',
botUsername: '' botUsername: ''
}); });
const original = reactive({});
const editMode = ref(false);
const loadTelegramSettings = async () => {
try {
const res = await api.get('/api/telegram-settings');
if (res.data.success) {
const s = res.data.settings;
form.botToken = '';
form.botUsername = s.bot_username;
Object.assign(original, JSON.parse(JSON.stringify(form)));
}
} catch (e) {
// обработка ошибки
}
};
onMounted(async () => {
await loadTelegramSettings();
editMode.value = false;
});
const saveTelegramSettings = async () => { const saveTelegramSettings = async () => {
// TODO: Реализовать вызов API для сохранения try {
alert('Настройки Telegram сохранены (заглушка)'); await api.put('/api/telegram-settings', {
bot_token: form.botToken,
bot_username: form.botUsername
});
alert('Настройки Telegram сохранены');
form.botToken = '';
Object.assign(original, JSON.parse(JSON.stringify(form)));
editMode.value = false;
} catch (e) {
alert('Ошибка сохранения telegram-настроек');
}
};
const cancelEdit = () => {
Object.assign(form, JSON.parse(JSON.stringify(original)));
form.botToken = '';
editMode.value = false;
}; };
</script> </script>
@@ -59,4 +105,43 @@ const saveTelegramSettings = async () => {
.save-btn:hover { .save-btn:hover {
background: var(--color-primary-dark); background: var(--color-primary-dark);
} }
.cancel-btn {
background: #eee;
color: #333;
border: none;
border-radius: 6px;
padding: 0.5rem 1.5rem;
cursor: pointer;
font-size: 1rem;
margin-left: 1rem;
}
.settings-view {
display: flex;
flex-direction: column;
gap: 1.2rem;
}
.view-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 1rem;
background: #f8f8f8;
border-radius: 4px;
padding: 0.5rem 1rem;
}
.edit-btn {
background: var(--color-primary);
color: #fff;
border: none;
border-radius: 6px;
padding: 0.5rem 1.5rem;
cursor: pointer;
font-size: 1rem;
align-self: flex-end;
margin-top: 1.5rem;
transition: background 0.2s;
}
.edit-btn:hover {
background: var(--color-primary-dark);
}
</style> </style>