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

This commit is contained in:
2025-04-21 18:03:43 +03:00
parent 4648aab7d5
commit 43569ea38c
37 changed files with 7226 additions and 1425 deletions

View File

@@ -26,46 +26,47 @@ app.set('host', '0.0.0.0');
app.set('port', process.env.PORT || 8000); app.set('port', process.env.PORT || 8000);
// Настройка CORS // Настройка CORS
app.use(cors({ app.use(
origin: [ cors({
'http://localhost:5173', origin: [
'http://127.0.0.1:5173' // Добавляем альтернативный origin 'http://localhost:5173',
], 'http://127.0.0.1:5173', // Добавляем альтернативный origin
credentials: true, ],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], credentials: true,
allowedHeaders: ['Content-Type', 'Authorization', 'Cookie'] methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
})); allowedHeaders: ['Content-Type', 'Authorization', 'Cookie'],
})
);
// Настройка сессии // Настройка сессии
app.use(session({ app.use(
store: new pgSession({ session({
pool, store: new pgSession({
tableName: 'session', pool,
}), tableName: 'session',
secret: process.env.SESSION_SECRET || 'hb3atoken', }),
name: 'sessionId', secret: process.env.SESSION_SECRET || 'hb3atoken',
resave: false, name: 'sessionId',
saveUninitialized: true, resave: false,
cookie: { saveUninitialized: true,
maxAge: 30 * 24 * 60 * 60 * 1000, cookie: {
httpOnly: true, maxAge: 30 * 24 * 60 * 60 * 1000,
secure: false, httpOnly: true,
sameSite: 'lax', secure: false,
path: '/' sameSite: 'lax',
} path: '/',
})); },
})
);
// Добавим middleware для проверки сессии // Добавим middleware для проверки сессии
app.use(async (req, res, next) => { app.use(async (req, res, next) => {
console.log('Request cookies:', req.headers.cookie); console.log('Request cookies:', req.headers.cookie);
console.log('Session ID:', req.sessionID); console.log('Session ID:', req.sessionID);
// Проверяем сессию в базе данных // Проверяем сессию в базе данных
if (req.sessionID) { if (req.sessionID) {
const result = await pool.query( const result = await pool.query('SELECT sess FROM session WHERE sid = $1', [req.sessionID]);
'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);
} }
@@ -80,28 +81,31 @@ 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 pool.query(
`
SELECT u.id, SELECT u.id,
(u.role = 'admin') as is_admin, (u.role = 'admin') as is_admin,
u.address u.address
FROM users u FROM users u
WHERE u.id = $1 WHERE u.id = $1
`, [token]); `,
[token]
);
if (rows.length > 0) { if (rows.length > 0) {
const user = rows[0]; const user = rows[0];
req.session.userId = user.id; req.session.userId = user.id;
req.session.address = user.address; req.session.address = user.address;
req.session.isAdmin = user.is_admin; req.session.isAdmin = user.is_admin;
req.session.authenticated = true; req.session.authenticated = true;
await new Promise(resolve => req.session.save(resolve)); await new Promise((resolve) => req.session.save(resolve));
} }
} catch (error) { } catch (error) {
console.error('Error checking auth header:', error); console.error('Error checking auth header:', error);
} }
} }
next(); next();
}); });
@@ -110,9 +114,11 @@ app.use(express.json());
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
// Настройка безопасности // Настройка безопасности
app.use(helmet({ app.use(
contentSecurityPolicy: false // Отключаем CSP для разработки helmet({
})); contentSecurityPolicy: false, // Отключаем CSP для разработки
})
);
// Логирование запросов // Логирование запросов
app.use((req, res, next) => { app.use((req, res, next) => {
@@ -156,32 +162,35 @@ app.get('/api/health', async (req, res) => {
try { try {
// Проверяем подключение к БД // Проверяем подключение к БД
await pool.query('SELECT NOW()'); await pool.query('SELECT NOW()');
// Проверяем AI сервис // Проверяем AI сервис
const aiStatus = await aiAssistant.checkHealth(); const aiStatus = await aiAssistant.checkHealth();
res.json({ res.json({
status: 'ok', status: 'ok',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
database: 'connected', database: 'connected',
ai: aiStatus ai: aiStatus,
}); });
} catch (error) { } catch (error) {
logger.error('Health check failed:', error); logger.error('Health check failed:', error);
res.status(500).json({ res.status(500).json({
status: 'error', status: 'error',
error: error.message error: error.message,
}); });
} }
}); });
// Очистка старых сессий // Очистка старых сессий
setInterval(async () => { setInterval(
try { async () => {
await pool.query('DELETE FROM session WHERE expire < NOW()'); try {
} catch (error) { await pool.query('DELETE FROM session WHERE expire < NOW()');
console.error('Error cleaning old sessions:', error); } catch (error) {
} console.error('Error cleaning old sessions:', error);
}, 15 * 60 * 1000); // Каждые 15 минут }
},
15 * 60 * 1000
); // Каждые 15 минут
module.exports = { app, nonceStore }; module.exports = { app, nonceStore };

View File

@@ -16,10 +16,10 @@ const sessionConfig = {
httpOnly: true, httpOnly: true,
secure: process.env.NODE_ENV === 'production', secure: process.env.NODE_ENV === 'production',
sameSite: 'lax', sameSite: 'lax',
path: '/' path: '/',
} },
}; };
module.exports = { module.exports = {
sessionMiddleware: session(sessionConfig) sessionMiddleware: session(sessionConfig),
}; };

View File

@@ -19,10 +19,10 @@ const pool = new Pool({
pool.query('SELECT NOW()', (err, res) => { pool.query('SELECT NOW()', (err, res) => {
if (err) { if (err) {
console.error('Ошибка подключения к базе данных:', err); console.error('Ошибка подключения к базе данных:', err);
// Пробуем альтернативное подключение // Пробуем альтернативное подключение
console.log('Попытка альтернативного подключения через прямые параметры...'); console.log('Попытка альтернативного подключения через прямые параметры...');
const altPool = new Pool({ const altPool = new Pool({
host: process.env.DB_HOST || 'localhost', host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'), port: parseInt(process.env.DB_PORT || '5432'),
@@ -30,7 +30,7 @@ pool.query('SELECT NOW()', (err, res) => {
user: process.env.DB_USER || 'dapp_user', user: process.env.DB_USER || 'dapp_user',
password: process.env.DB_PASSWORD, password: process.env.DB_PASSWORD,
}); });
altPool.query('SELECT NOW()', (altErr, altRes) => { altPool.query('SELECT NOW()', (altErr, altRes) => {
if (altErr) { if (altErr) {
console.error('Альтернативное подключение тоже не удалось:', altErr); console.error('Альтернативное подключение тоже не удалось:', altErr);
@@ -56,10 +56,13 @@ const query = (text, params) => {
// Функция для сохранения гостевого сообщения в базе данных // Функция для сохранения гостевого сообщения в базе данных
async function saveGuestMessageToDatabase(message, language, guestId) { async function saveGuestMessageToDatabase(message, language, guestId) {
try { try {
await query(` await query(
`
INSERT INTO guest_messages (guest_id, content, language, created_at) INSERT INTO guest_messages (guest_id, content, language, created_at)
VALUES ($1, $2, $3, NOW()) VALUES ($1, $2, $3, NOW())
`, [guestId, message, language]); `,
[guestId, message, language]
);
console.log('Гостевое сообщение успешно сохранено:', message); console.log('Гостевое сообщение успешно сохранено:', message);
} catch (error) { } catch (error) {
console.error('Ошибка при сохранении гостевого сообщения:', error); console.error('Ошибка при сохранении гостевого сообщения:', error);

View File

@@ -18,4 +18,4 @@ pool.query('SELECT NOW()', (err, res) => {
} }
}); });
module.exports = { pool }; module.exports = { pool };

View File

@@ -13,7 +13,7 @@ async function initRoles() {
WHERE table_name = 'roles' WHERE table_name = 'roles'
); );
`); `);
if (!tableExists.rows[0].exists) { if (!tableExists.rows[0].exists) {
// Создаем таблицу roles // Создаем таблицу roles
await pool.query(` await pool.query(`
@@ -24,40 +24,44 @@ async function initRoles() {
created_at TIMESTAMP NOT NULL DEFAULT NOW() created_at TIMESTAMP NOT NULL DEFAULT NOW()
); );
`); `);
// Добавляем роли // Добавляем роли
await pool.query(` await pool.query(`
INSERT INTO roles (id, name, description) VALUES INSERT INTO roles (id, name, description) VALUES
(3, 'user', 'Обычный пользователь'), (3, 'user', 'Обычный пользователь'),
(4, 'admin', 'Администратор с полным доступом'); (4, 'admin', 'Администратор с полным доступом');
`); `);
console.log('Таблица roles создана и заполнена'); console.log('Таблица roles создана и заполнена');
} else { } else {
// Проверяем наличие ролей // Проверяем наличие ролей
const rolesExist = await pool.query(` const rolesExist = await pool.query(`
SELECT COUNT(*) FROM roles WHERE id IN (3, 4); SELECT COUNT(*) FROM roles WHERE id IN (3, 4);
`); `);
if (rolesExist.rows[0].count < 2) { if (rolesExist.rows[0].count < 2) {
// Добавляем недостающие роли // Добавляем недостающие роли
const userRoleExists = await pool.query(`SELECT EXISTS (SELECT FROM roles WHERE name = 'user');`); const userRoleExists = await pool.query(
const adminRoleExists = await pool.query(`SELECT EXISTS (SELECT FROM roles WHERE name = 'admin');`); `SELECT EXISTS (SELECT FROM roles WHERE name = 'user');`
);
const adminRoleExists = await pool.query(
`SELECT EXISTS (SELECT FROM roles WHERE name = 'admin');`
);
if (!userRoleExists.rows[0].exists) { if (!userRoleExists.rows[0].exists) {
await pool.query(` await pool.query(`
INSERT INTO roles (id, name, description) VALUES INSERT INTO roles (id, name, description) VALUES
(3, 'user', 'Обычный пользователь'); (3, 'user', 'Обычный пользователь');
`); `);
} }
if (!adminRoleExists.rows[0].exists) { if (!adminRoleExists.rows[0].exists) {
await pool.query(` await pool.query(`
INSERT INTO roles (id, name, description) VALUES INSERT INTO roles (id, name, description) VALUES
(4, 'admin', 'Администратор с полным доступом'); (4, 'admin', 'Администратор с полным доступом');
`); `);
} }
console.log('Таблица roles обновлена'); console.log('Таблица roles обновлена');
} }
} }
@@ -80,31 +84,29 @@ async function initializeDatabase() {
// Путь к папке с миграциями // Путь к папке с миграциями
const migrationsPath = path.join(__dirname, 'migrations'); const migrationsPath = path.join(__dirname, 'migrations');
// Получаем все файлы миграций // Получаем все файлы миграций
const migrationFiles = fs.readdirSync(migrationsPath) const migrationFiles = fs
.filter(file => file.endsWith('.sql')) .readdirSync(migrationsPath)
.filter((file) => file.endsWith('.sql'))
.sort(); .sort();
// Получаем выполненные миграции // Получаем выполненные миграции
const { rows } = await pool.query('SELECT name FROM migrations'); const { rows } = await pool.query('SELECT name FROM migrations');
const executedMigrations = new Set(rows.map(row => row.name)); const executedMigrations = new Set(rows.map((row) => row.name));
// Выполняем только новые миграции // Выполняем только новые миграции
for (const file of migrationFiles) { for (const file of migrationFiles) {
if (!executedMigrations.has(file)) { if (!executedMigrations.has(file)) {
const filePath = path.join(migrationsPath, file); const filePath = path.join(migrationsPath, file);
const sql = fs.readFileSync(filePath, 'utf8'); const sql = fs.readFileSync(filePath, 'utf8');
logger.info(`Executing migration: ${file}`); logger.info(`Executing migration: ${file}`);
await pool.query(sql); await pool.query(sql);
// Записываем выполненную миграцию // Записываем выполненную миграцию
await pool.query( await pool.query('INSERT INTO migrations (name) VALUES ($1)', [file]);
'INSERT INTO migrations (name) VALUES ($1)',
[file]
);
logger.info(`Migration completed: ${file}`); logger.info(`Migration completed: ${file}`);
} }
} }
@@ -116,4 +118,4 @@ async function initializeDatabase() {
} }
} }
module.exports = { initializeDatabase }; module.exports = { initializeDatabase };

View File

@@ -7,6 +7,7 @@
Система идентификации пользователей построена на следующих таблицах: Система идентификации пользователей построена на следующих таблицах:
1. **users** - Основная таблица пользователей 1. **users** - Основная таблица пользователей
- `id SERIAL PRIMARY KEY` - Основной идентификатор пользователя - `id SERIAL PRIMARY KEY` - Основной идентификатор пользователя
- `status` - Статус пользователя (active, blocked) - `status` - Статус пользователя (active, blocked)
- `role` - Роль пользователя (user, admin) - `role` - Роль пользователя (user, admin)
@@ -14,6 +15,7 @@
- Поля `username`, `email` и `address` являются устаревшими и должны быть NULL - Поля `username`, `email` и `address` являются устаревшими и должны быть NULL
2. **user_identities** - Таблица идентификаторов пользователей 2. **user_identities** - Таблица идентификаторов пользователей
- `id SERIAL PRIMARY KEY` - Идентификатор записи - `id SERIAL PRIMARY KEY` - Идентификатор записи
- `user_id INTEGER REFERENCES users(id)` - Ссылка на пользователя - `user_id INTEGER REFERENCES users(id)` - Ссылка на пользователя
- `provider VARCHAR(50)` - Тип идентификатора (email, wallet, telegram, username) - `provider VARCHAR(50)` - Тип идентификатора (email, wallet, telegram, username)
@@ -22,6 +24,7 @@
- Ограничение `CHECK (provider IN ('email', 'wallet', 'telegram', 'username'))` - запрещает тип 'guest' - Ограничение `CHECK (provider IN ('email', 'wallet', 'telegram', 'username'))` - запрещает тип 'guest'
3. **guest_user_mapping** - Таблица связи гостевых идентификаторов с пользователями 3. **guest_user_mapping** - Таблица связи гостевых идентификаторов с пользователями
- `id SERIAL PRIMARY KEY` - Идентификатор записи - `id SERIAL PRIMARY KEY` - Идентификатор записи
- `user_id INTEGER REFERENCES users(id)` - Ссылка на пользователя - `user_id INTEGER REFERENCES users(id)` - Ссылка на пользователя
- `guest_id VARCHAR(255)` - Гостевой идентификатор - `guest_id VARCHAR(255)` - Гостевой идентификатор
@@ -29,6 +32,7 @@
- Уникальный ключ `guest_id` - Уникальный ключ `guest_id`
4. **messages** - Таблица сообщений 4. **messages** - Таблица сообщений
- `id SERIAL PRIMARY KEY` - Идентификатор сообщения - `id SERIAL PRIMARY KEY` - Идентификатор сообщения
- `conversation_id INTEGER REFERENCES conversations(id)` - Ссылка на диалог - `conversation_id INTEGER REFERENCES conversations(id)` - Ссылка на диалог
- `user_id INTEGER REFERENCES users(id)` - Прямая ссылка на пользователя - `user_id INTEGER REFERENCES users(id)` - Прямая ссылка на пользователя
@@ -56,7 +60,6 @@
- Создается запись в таблице `users` - Создается запись в таблице `users`
- Создается запись в таблице `user_identities` с соответствующим провайдером - Создается запись в таблице `user_identities` с соответствующим провайдером
- Гостевой ID сохраняется в таблице `guest_user_mapping` (не в user_identities) - Гостевой ID сохраняется в таблице `guest_user_mapping` (не в user_identities)
2. После аутентификации система автоматически обрабатывает гостевые сообщения: 2. После аутентификации система автоматически обрабатывает гостевые сообщения:
- Вызывается метод `linkGuestMessages` - Вызывается метод `linkGuestMessages`
- Создается новый диалог для гостевых сообщений - Создается новый диалог для гостевых сообщений
@@ -69,7 +72,7 @@
Если пользователь аутентифицируется разными способами, система может объединить его данные: Если пользователь аутентифицируется разными способами, система может объединить его данные:
1. Система проверяет связанных пользователей через `user_identities` 1. Система проверяет связанных пользователей через `user_identities`
2. Если находятся связанные пользователи, вызывается метод `migrateUserData` 2. Если находятся связанные пользователи, вызывается метод `migrateUserData`
3. Данные от вторичных аккаунтов мигрируют к основному: 3. Данные от вторичных аккаунтов мигрируют к основному:
- Идентификаторы в таблице `user_identities` - Идентификаторы в таблице `user_identities`
- Гостевые связи в таблице `guest_user_mapping` - Гостевые связи в таблице `guest_user_mapping`
@@ -89,6 +92,7 @@
## Обработка ошибок ## Обработка ошибок
1. Если возникает ошибка при обработке гостевых сообщений, система: 1. Если возникает ошибка при обработке гостевых сообщений, система:
- Логирует ошибку - Логирует ошибку
- Продолжает попытки обработки при следующих авторизациях - Продолжает попытки обработки при следующих авторизациях
- Не удаляет гостевые сообщения до успешной обработки - Не удаляет гостевые сообщения до успешной обработки
@@ -98,6 +102,7 @@
## Оптимизации ## Оптимизации
1. Индексы созданы для всех полей, используемых в запросах: 1. Индексы созданы для всех полей, используемых в запросах:
- `user_identities(user_id)` - `user_identities(user_id)`
- `user_identities(provider, provider_id)` - `user_identities(provider, provider_id)`
- `guest_user_mapping(guest_id)` - `guest_user_mapping(guest_id)`
@@ -106,6 +111,7 @@
- `messages(conversation_id)` - `messages(conversation_id)`
2. Триггеры автоматически поддерживают целостность данных: 2. Триггеры автоматически поддерживают целостность данных:
- Автоматическое заполнение `user_id` в таблице `messages` - Автоматическое заполнение `user_id` в таблице `messages`
- Очистка неиспользуемых полей в таблице `users` - Очистка неиспользуемых полей в таблице `users`
@@ -117,6 +123,7 @@
## Функции для диагностики ## Функции для диагностики
1. **verify_migration_017()** - проверяет состояние гостевых идентификаторов 1. **verify_migration_017()** - проверяет состояние гостевых идентификаторов
- `guest_identities_count` - количество гостевых идентификаторов в таблице user_identities - `guest_identities_count` - количество гостевых идентификаторов в таблице user_identities
- `guest_mapping_count` - количество записей в таблице guest_user_mapping - `guest_mapping_count` - количество записей в таблице guest_user_mapping
- `missing_mappings` - количество гостевых ID, которые отсутствуют в guest_user_mapping - `missing_mappings` - количество гостевых ID, которые отсутствуют в guest_user_mapping
@@ -127,4 +134,4 @@
- `wallet_identities` - количество идентификаторов wallet - `wallet_identities` - количество идентификаторов wallet
- `email_identities` - количество идентификаторов email - `email_identities` - количество идентификаторов email
- `telegram_identities` - количество идентификаторов telegram - `telegram_identities` - количество идентификаторов telegram
- `duplicate_provider_ids` - количество дублирующихся идентификаторов - `duplicate_provider_ids` - количество дублирующихся идентификаторов

View File

@@ -47,10 +47,10 @@ CREATE TABLE IF NOT EXISTS example_table (
); );
-- Добавление колонки, если она отсутствует -- Добавление колонки, если она отсутствует
DO $$ DO $$
BEGIN BEGIN
IF NOT EXISTS ( IF NOT EXISTS (
SELECT 1 FROM information_schema.columns SELECT 1 FROM information_schema.columns
WHERE table_name = 'example_table' AND column_name = 'new_column' WHERE table_name = 'example_table' AND column_name = 'new_column'
) THEN ) THEN
ALTER TABLE example_table ADD COLUMN new_column INTEGER; ALTER TABLE example_table ADD COLUMN new_column INTEGER;
@@ -69,6 +69,7 @@ END $$;
- `verify_identity_system()` - проверка состояния системы идентификации пользователей - `verify_identity_system()` - проверка состояния системы идентификации пользователей
Пример использования: Пример использования:
```sql ```sql
SELECT * FROM verify_identity_system(); SELECT * FROM verify_identity_system();
``` ```

View File

@@ -1,6 +1,6 @@
import globals from 'globals'; const globals = require('globals');
export default [ module.exports = [
{ {
ignores: ['node_modules/**', 'artifacts/**', 'sessions/**', 'logs/**', 'data/**'], ignores: ['node_modules/**', 'artifacts/**', 'sessions/**', 'logs/**', 'data/**'],
}, },
@@ -8,10 +8,11 @@ export default [
files: ['**/*.js'], files: ['**/*.js'],
languageOptions: { languageOptions: {
ecmaVersion: 2022, ecmaVersion: 2022,
sourceType: 'module', sourceType: 'module', // Оставляем module, т.к. ESLint может анализировать ES модули
globals: { globals: {
...globals.node, ...globals.node,
...globals.es2021, ...globals.es2021,
// Для тестов Mocha
describe: 'readonly', describe: 'readonly',
it: 'readonly', it: 'readonly',
beforeEach: 'readonly', beforeEach: 'readonly',
@@ -21,10 +22,10 @@ export default [
}, },
}, },
rules: { rules: {
'no-unused-vars': 'off', 'no-unused-vars': 'warn', // Лучше warn, чем off
'no-console': 'off', 'no-console': 'off', // Оставляем off для логов в Node.js
'no-undef': 'error', 'no-undef': 'error',
'no-duplicate-imports': 'error', 'no-duplicate-imports': 'error',
}, },
}, },
]; ];

View File

@@ -13,19 +13,19 @@ const requireAuth = async (req, res, next) => {
console.log('Session in requireAuth:', { console.log('Session in requireAuth:', {
id: req.sessionID, id: req.sessionID,
userId: req.session?.userId, userId: req.session?.userId,
authenticated: req.session?.authenticated authenticated: req.session?.authenticated,
}); });
// Проверяем сессию // Проверяем сессию
if (req.session?.authenticated && req.session?.userId) { if (req.session?.authenticated && req.session?.userId) {
// Обновляем время жизни сессии // Обновляем время жизни сессии
req.session.touch(); req.session.touch();
req.user = { req.user = {
userId: req.session.userId, userId: req.session.userId,
address: req.session.address, address: req.session.address,
isAdmin: req.session.isAdmin, isAdmin: req.session.isAdmin,
authType: req.session.authType authType: req.session.authType,
}; };
return next(); return next();
} }
@@ -34,19 +34,22 @@ const requireAuth = async (req, res, next) => {
const authHeader = req.headers.authorization; const authHeader = req.headers.authorization;
if (authHeader?.startsWith('Bearer ')) { if (authHeader?.startsWith('Bearer ')) {
const address = authHeader.split(' ')[1]; const address = authHeader.split(' ')[1];
if (address.startsWith('0x')) { if (address.startsWith('0x')) {
const result = await db.query(` const result = await db.query(
`
SELECT u.id, u.is_admin SELECT u.id, u.is_admin
FROM users u 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.identity_type = 'wallet' WHERE ui.identity_type = 'wallet'
AND LOWER(ui.identity_value) = LOWER($1) AND LOWER(ui.identity_value) = LOWER($1)
`, [address]); `,
[address]
);
if (result.rows.length > 0) { if (result.rows.length > 0) {
const user = result.rows[0]; const user = result.rows[0];
// Создаем новую сессию // Создаем новую сессию
req.session.regenerate(async (err) => { req.session.regenerate(async (err) => {
if (err) { if (err) {
@@ -68,7 +71,7 @@ const requireAuth = async (req, res, next) => {
userId: user.id, userId: user.id,
address: address, address: address,
isAdmin: user.is_admin, isAdmin: user.is_admin,
authType: 'wallet' authType: 'wallet',
}; };
next(); next();
}); });
@@ -111,7 +114,9 @@ async function requireAdmin(req, res, next) {
// Проверка через ID пользователя // Проверка через ID пользователя
if (req.session.userId) { if (req.session.userId) {
const userResult = await db.query('SELECT role FROM users WHERE id = $1', [req.session.userId]); const userResult = await db.query('SELECT role FROM users WHERE id = $1', [
req.session.userId,
]);
if (userResult.rows.length > 0 && userResult.rows[0].role === USER_ROLES.ADMIN) { if (userResult.rows.length > 0 && userResult.rows[0].role === USER_ROLES.ADMIN) {
// Обновляем сессию // Обновляем сессию
req.session.isAdmin = true; req.session.isAdmin = true;
@@ -146,7 +151,9 @@ function requireRole(role) {
// Проверка через ID пользователя // Проверка через ID пользователя
if (req.session.userId) { if (req.session.userId) {
const userResult = await db.query('SELECT role FROM users WHERE id = $1', [req.session.userId]); const userResult = await db.query('SELECT role FROM users WHERE id = $1', [
req.session.userId,
]);
if (userResult.rows.length > 0 && userResult.rows[0].role === role) { if (userResult.rows.length > 0 && userResult.rows[0].role === role) {
return next(); return next();
} }
@@ -192,5 +199,5 @@ module.exports = {
requireAuth, requireAuth,
requireAdmin, requireAdmin,
requireRole, requireRole,
checkRole checkRole,
}; };

View File

@@ -11,7 +11,7 @@ function errorHandler(err, req, res, next) {
url: req.originalUrl, url: req.originalUrl,
method: req.method, method: req.method,
ip: req.ip, ip: req.ip,
userId: req.session?.userId userId: req.session?.userId,
}); });
// Определяем тип ошибки // Определяем тип ошибки
@@ -39,17 +39,15 @@ function errorHandler(err, req, res, next) {
} }
// В режиме разработки возвращаем стек ошибки // В режиме разработки возвращаем стек ошибки
const devError = process.env.NODE_ENV === 'development' const devError = process.env.NODE_ENV === 'development' ? { stack: err.stack } : {};
? { stack: err.stack }
: {};
// Отправляем ответ клиенту // Отправляем ответ клиенту
res.status(statusCode).json({ res.status(statusCode).json({
error: { error: {
code: errorCode, code: errorCode,
message: errorMessage, message: errorMessage,
...devError ...devError,
} },
}); });
} }
@@ -67,5 +65,5 @@ function createError(message, status) {
module.exports = { module.exports = {
errorHandler, errorHandler,
createError createError,
}; };

View File

@@ -11,4 +11,4 @@ const requestLogger = (req, res, next) => {
next(); next();
}; };
module.exports = requestLogger; module.exports = requestLogger;

View File

@@ -1,5 +1,6 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const db = require('../db');
const { requireAdmin } = require('../middleware/auth'); const { requireAdmin } = require('../middleware/auth');
const authService = require('../services/auth-service'); const authService = require('../services/auth-service');
const logger = require('../utils/logger'); const logger = require('../utils/logger');

View File

@@ -35,29 +35,28 @@ router.get('/nonce', async (req, res) => {
// Генерируем случайный nonce // Генерируем случайный nonce
const nonce = crypto.randomBytes(16).toString('hex'); const nonce = crypto.randomBytes(16).toString('hex');
// Проверяем, существует ли уже nonce для этого адреса // Проверяем, существует ли уже nonce для этого адреса
const existingNonce = await db.query( const existingNonce = await db.query('SELECT id FROM nonces WHERE identity_value = $1', [
'SELECT id FROM nonces WHERE identity_value = $1', address.toLowerCase(),
[address.toLowerCase()] ]);
);
if (existingNonce.rows.length > 0) { if (existingNonce.rows.length > 0) {
// Обновляем существующий nonce // Обновляем существующий nonce
await db.query( await db.query(
'UPDATE nonces SET nonce = $1, expires_at = NOW() + INTERVAL \'15 minutes\' WHERE identity_value = $2', "UPDATE nonces SET nonce = $1, expires_at = NOW() + INTERVAL '15 minutes' WHERE identity_value = $2",
[nonce, address.toLowerCase()] [nonce, address.toLowerCase()]
); );
} else { } else {
// Создаем новый nonce // Создаем новый nonce
await db.query( await db.query(
'INSERT INTO nonces (identity_value, nonce, expires_at) VALUES ($1, $2, NOW() + INTERVAL \'15 minutes\')', "INSERT INTO nonces (identity_value, nonce, expires_at) VALUES ($1, $2, NOW() + INTERVAL '15 minutes')",
[address.toLowerCase(), nonce] [address.toLowerCase(), nonce]
); );
} }
logger.info(`Nonce ${nonce} сохранен для адреса ${address}`); logger.info(`Nonce ${nonce} сохранен для адреса ${address}`);
res.json({ nonce }); res.json({ nonce });
} catch (error) { } catch (error) {
logger.error('Error generating nonce:', error); logger.error('Error generating nonce:', error);
@@ -69,53 +68,58 @@ router.get('/nonce', async (req, res) => {
router.post('/verify', async (req, res) => { router.post('/verify', async (req, res) => {
try { try {
const { address, message, signature } = req.body; const { address, message, signature } = req.body;
logger.info(`[verify] Verifying signature for address: ${address}`); logger.info(`[verify] Verifying signature for address: ${address}`);
// Сохраняем гостевые ID до проверки // Сохраняем гостевые ID до проверки
const guestId = req.session.guestId; const guestId = req.session.guestId;
const previousGuestId = req.session.previousGuestId; const previousGuestId = req.session.previousGuestId;
// Проверяем подпись // Проверяем подпись
const isValid = await authService.verifySignature(message, signature, address); const isValid = await authService.verifySignature(message, signature, address);
if (!isValid) { if (!isValid) {
return res.status(401).json({ success: false, error: 'Invalid signature' }); return res.status(401).json({ success: false, error: 'Invalid signature' });
} }
// Нормализуем адрес для использования в запросах // Нормализуем адрес для использования в запросах
const normalizedAddress = ethers.getAddress(address).toLowerCase(); const normalizedAddress = ethers.getAddress(address).toLowerCase();
// Проверяем nonce // Проверяем nonce
const nonceResult = await db.query('SELECT nonce FROM nonces WHERE identity_value = $1', [normalizedAddress]); const nonceResult = await db.query('SELECT nonce FROM nonces WHERE identity_value = $1', [
if (nonceResult.rows.length === 0 || nonceResult.rows[0].nonce !== message.match(/Nonce: ([^\n]+)/)[1]) { normalizedAddress,
]);
if (
nonceResult.rows.length === 0 ||
nonceResult.rows[0].nonce !== message.match(/Nonce: ([^\n]+)/)[1]
) {
return res.status(401).json({ success: false, error: 'Invalid nonce' }); return res.status(401).json({ success: false, error: 'Invalid nonce' });
} }
let userId; let userId;
let isAdmin = false; let isAdmin = false;
// Проверяем, авторизован ли пользователь уже // Проверяем, авторизован ли пользователь уже
if (req.session.authenticated && req.session.userId) { if (req.session.authenticated && req.session.userId) {
// Если пользователь уже авторизован, привязываем кошелек к существующему пользователю // Если пользователь уже авторизован, привязываем кошелек к существующему пользователю
userId = req.session.userId; userId = req.session.userId;
logger.info(`[verify] Using existing authenticated user ${userId} for wallet ${normalizedAddress}`); logger.info(
`[verify] Using existing authenticated user ${userId} for wallet ${normalizedAddress}`
// Связываем кошелек с пользователем через identity-service для предотвращения дубликатов
const linkResult = await authService.linkIdentity(
userId,
'wallet',
address
); );
// Связываем кошелек с пользователем через identity-service для предотвращения дубликатов
const linkResult = await authService.linkIdentity(userId, 'wallet', address);
if (!linkResult.success && linkResult.error) { if (!linkResult.success && linkResult.error) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
error: linkResult.error error: linkResult.error,
}); });
} }
// Если linkResult.message содержит 'already exists', значит кошелек уже привязан // Если linkResult.message содержит 'already exists', значит кошелек уже привязан
logger.info(`[verify] Wallet ${normalizedAddress} linked to user ${userId}: ${linkResult.message || 'success'}`); logger.info(
`[verify] Wallet ${normalizedAddress} linked to user ${userId}: ${linkResult.message || 'success'}`
);
} else { } else {
// Находим или создаем пользователя, если не авторизован // Находим или создаем пользователя, если не авторизован
const result = await authService.findOrCreateUser(address); const result = await authService.findOrCreateUser(address);
@@ -123,49 +127,48 @@ router.post('/verify', async (req, res) => {
isAdmin = result.isAdmin; isAdmin = result.isAdmin;
logger.info(`[verify] Found or created user ${userId} for wallet ${normalizedAddress}`); logger.info(`[verify] Found or created user ${userId} for wallet ${normalizedAddress}`);
} }
// Сохраняем идентификаторы гостевой сессии // Сохраняем идентификаторы гостевой сессии
if (guestId) { if (guestId) {
await identityService.saveIdentity(userId, 'guest', guestId, true); await identityService.saveIdentity(userId, 'guest', guestId, true);
} }
if (previousGuestId && previousGuestId !== guestId) { if (previousGuestId && previousGuestId !== guestId) {
await identityService.saveIdentity(userId, 'guest', previousGuestId, true); await identityService.saveIdentity(userId, 'guest', previousGuestId, true);
} }
// Проверяем наличие админских токенов // Проверяем наличие админских токенов
const adminStatus = await authService.checkAdminTokens(normalizedAddress); const adminStatus = await authService.checkAdminTokens(normalizedAddress);
if (adminStatus) { if (adminStatus) {
await db.query('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]); await db.query('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]);
isAdmin = true; isAdmin = true;
} }
// Обновляем сессию // Обновляем сессию
req.session.userId = userId; req.session.userId = userId;
req.session.authenticated = true; req.session.authenticated = true;
req.session.authType = 'wallet'; req.session.authType = 'wallet';
req.session.isAdmin = adminStatus || isAdmin; req.session.isAdmin = adminStatus || isAdmin;
req.session.address = normalizedAddress; // Всегда сохраняем нормализованный адрес req.session.address = normalizedAddress; // Всегда сохраняем нормализованный адрес
// Удаляем временный ID // Удаляем временный ID
delete req.session.tempUserId; delete req.session.tempUserId;
// Сохраняем сессию // Сохраняем сессию
await sessionService.saveSession(req.session); await sessionService.saveSession(req.session);
// Связываем гостевые сообщения с пользователем // Связываем гостевые сообщения с пользователем
await sessionService.linkGuestMessages(req.session, userId); await sessionService.linkGuestMessages(req.session, userId);
// Возвращаем успешный ответ // Возвращаем успешный ответ
return res.json({ return res.json({
success: true, success: true,
userId, userId,
address: normalizedAddress, // Возвращаем нормализованный адрес address: normalizedAddress, // Возвращаем нормализованный адрес
isAdmin: adminStatus || isAdmin, isAdmin: adminStatus || isAdmin,
authenticated: true authenticated: true,
}); });
} catch (error) { } catch (error) {
logger.error('[verify] Error:', error); logger.error('[verify] Error:', error);
res.status(500).json({ success: false, error: 'Server error' }); res.status(500).json({ success: false, error: 'Server error' });
@@ -176,17 +179,17 @@ router.post('/verify', async (req, res) => {
router.post('/telegram/verify', async (req, res) => { router.post('/telegram/verify', async (req, res) => {
try { try {
const { telegramId, verificationCode } = req.body; const { telegramId, verificationCode } = req.body;
if (!telegramId || !verificationCode) { if (!telegramId || !verificationCode) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
error: 'Missing required fields' error: 'Missing required fields',
}); });
} }
// Сохраняем гостевой ID из текущей сессии // Сохраняем гостевой ID из текущей сессии
const guestId = req.session.guestId; const guestId = req.session.guestId;
// Передаем сессию в метод верификации // Передаем сессию в метод верификации
const verificationResult = await authService.verifyTelegramAuth( const verificationResult = await authService.verifyTelegramAuth(
telegramId, telegramId,
@@ -197,7 +200,7 @@ router.post('/telegram/verify', async (req, res) => {
if (!verificationResult.success) { if (!verificationResult.success) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
error: verificationResult.error || 'Verification failed' error: verificationResult.error || 'Verification failed',
}); });
} }
@@ -205,9 +208,9 @@ router.post('/telegram/verify', async (req, res) => {
req.session.regenerate(async (err) => { req.session.regenerate(async (err) => {
if (err) { if (err) {
logger.error('[telegram/verify] Error regenerating session:', err); logger.error('[telegram/verify] Error regenerating session:', err);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
error: 'Session error' error: 'Session error',
}); });
} }
@@ -217,7 +220,7 @@ router.post('/telegram/verify', async (req, res) => {
req.session.authType = 'telegram'; req.session.authType = 'telegram';
req.session.authenticated = true; req.session.authenticated = true;
req.session.role = verificationResult.role; req.session.role = verificationResult.role;
// Восстанавливаем гостевой ID, если он был // Восстанавливаем гостевой ID, если он был
if (guestId) { if (guestId) {
req.session.guestId = guestId; req.session.guestId = guestId;
@@ -236,14 +239,14 @@ router.post('/telegram/verify', async (req, res) => {
userId: verificationResult.userId, userId: verificationResult.userId,
role: verificationResult.role, role: verificationResult.role,
telegramId, telegramId,
isNewUser: verificationResult.isNewUser isNewUser: verificationResult.isNewUser,
}); });
}); });
} catch (error) { } catch (error) {
logger.error('[telegram/verify] Error:', error); logger.error('[telegram/verify] Error:', error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
error: 'Internal server error' error: 'Internal server error',
}); });
} }
}); });
@@ -252,26 +255,26 @@ router.post('/telegram/verify', async (req, res) => {
router.post('/email/request', authLimiter, async (req, res) => { router.post('/email/request', authLimiter, async (req, res) => {
try { try {
const { email } = req.body; const { email } = req.body;
if (!email || !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) { if (!email || !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
return res.status(400).json({ error: 'Invalid email format' }); return res.status(400).json({ error: 'Invalid email format' });
} }
// Инициализация email аутентификации // Инициализация email аутентификации
const result = await emailAuth.initEmailAuth(req.session, email); const result = await emailAuth.initEmailAuth(req.session, email);
// Сохраняем сессию после установки pendingEmail // Сохраняем сессию после установки pendingEmail
await sessionService.saveSession(req.session); await sessionService.saveSession(req.session);
if (result.success) { if (result.success) {
res.json({ res.json({
success: true, success: true,
message: 'Код подтверждения отправлен на email' message: 'Код подтверждения отправлен на email',
}); });
} else { } else {
res.status(500).json({ res.status(500).json({
success: false, success: false,
error: result.error || 'Ошибка отправки кода' error: result.error || 'Ошибка отправки кода',
}); });
} }
} catch (error) { } catch (error) {
@@ -284,150 +287,155 @@ router.post('/email/request', authLimiter, async (req, res) => {
router.post('/email/verify-code', async (req, res) => { router.post('/email/verify-code', async (req, res) => {
try { try {
const { code, email } = req.body; const { code, email } = req.body;
if (!code) { if (!code) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
error: 'Код подтверждения обязателен' error: 'Код подтверждения обязателен',
}); });
} }
// Если email передан в запросе, сохраняем его в сессии // Если email передан в запросе, сохраняем его в сессии
if (email && !req.session.pendingEmail) { if (email && !req.session.pendingEmail) {
req.session.pendingEmail = email.toLowerCase(); req.session.pendingEmail = email.toLowerCase();
} }
if (!req.session.pendingEmail) { if (!req.session.pendingEmail) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
error: 'Email не найден в сессии. Пожалуйста, запросите код подтверждения снова.' error: 'Email не найден в сессии. Пожалуйста, запросите код подтверждения снова.',
}); });
} }
// Сохраняем гостевой ID до проверки // Сохраняем гостевой ID до проверки
const guestId = req.session.guestId; const guestId = req.session.guestId;
const previousGuestId = req.session.previousGuestId; const previousGuestId = req.session.previousGuestId;
// Проверяем код через сервис верификации // Проверяем код через сервис верификации
const verificationResult = await verificationService.verifyCode( const verificationResult = await verificationService.verifyCode(
code, code,
'email', 'email',
req.session.pendingEmail req.session.pendingEmail
); );
if (!verificationResult.success) { if (!verificationResult.success) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
error: verificationResult.error || 'Неверный код подтверждения' error: verificationResult.error || 'Неверный код подтверждения',
}); });
} }
// Получаем или создаем пользователя // Получаем или создаем пользователя
let userId; let userId;
let isNewAuth = false; let isNewAuth = false;
// Проверяем, авторизован ли пользователь // Проверяем, авторизован ли пользователь
if (req.session.authenticated && req.session.userId) { if (req.session.authenticated && req.session.userId) {
// Связываем email с существующим пользователем // Связываем email с существующим пользователем
userId = req.session.userId; userId = req.session.userId;
logger.info(`[email/verify-code] Linking email ${req.session.pendingEmail} to existing authenticated user ${userId}`); logger.info(
`[email/verify-code] Linking email ${req.session.pendingEmail} to existing authenticated user ${userId}`
// Связываем email с текущим аккаунтом
const linkResult = await authService.linkIdentity(
userId,
'email',
req.session.pendingEmail
); );
// Связываем email с текущим аккаунтом
const linkResult = await authService.linkIdentity(userId, 'email', req.session.pendingEmail);
// Сохраняем email в сессии // Сохраняем email в сессии
req.session.email = req.session.pendingEmail; req.session.email = req.session.pendingEmail;
// Удаляем временные данные // Удаляем временные данные
delete req.session.pendingEmail; delete req.session.pendingEmail;
// Сохраняем сессию // Сохраняем сессию
await sessionService.saveSession(req.session); await sessionService.saveSession(req.session);
return res.json({ return res.json({
success: true, success: true,
userId, userId,
email: req.session.email, email: req.session.email,
authenticated: true, authenticated: true,
linked: true linked: true,
}); });
} else { } else {
// Если пользователь не авторизован, ищем существующего пользователя или создаем нового // Если пользователь не авторизован, ищем существующего пользователя или создаем нового
// Ищем существующего пользователя по email // Ищем существующего пользователя по email
const existingUser = await identityService.findUserByIdentity( const existingUser = await identityService.findUserByIdentity(
'email', 'email',
req.session.pendingEmail req.session.pendingEmail
); );
if (existingUser) { if (existingUser) {
// Используем существующего пользователя // Используем существующего пользователя
userId = existingUser.id; userId = existingUser.id;
logger.info(`[email/verify-code] Using existing user ${userId} with email ${req.session.pendingEmail}`); logger.info(
`[email/verify-code] Using existing user ${userId} with email ${req.session.pendingEmail}`
);
} else if (req.session.userId) { } else if (req.session.userId) {
// Используем текущего пользователя // Используем текущего пользователя
userId = req.session.userId; userId = req.session.userId;
logger.info(`[email/verify-code] Using current user ${userId} for email ${req.session.pendingEmail}`); logger.info(
`[email/verify-code] Using current user ${userId} for email ${req.session.pendingEmail}`
);
} else if (req.session.tempUserId) { } else if (req.session.tempUserId) {
// Используем временного пользователя // Используем временного пользователя
userId = req.session.tempUserId; userId = req.session.tempUserId;
logger.info(`[email/verify-code] Using temporary user ${userId} for email ${req.session.pendingEmail}`); logger.info(
`[email/verify-code] Using temporary user ${userId} for email ${req.session.pendingEmail}`
);
} else { } else {
// Создаем нового пользователя // Создаем нового пользователя
const newUser = await db.query( const newUser = await db.query('INSERT INTO users (role) VALUES ($1) RETURNING id', [
'INSERT INTO users (role) VALUES ($1) RETURNING id', 'user',
['user'] ]);
);
userId = newUser.rows[0].id; userId = newUser.rows[0].id;
isNewAuth = true; isNewAuth = true;
logger.info(`[email/verify-code] Created new user ${userId} for email ${req.session.pendingEmail}`); logger.info(
`[email/verify-code] Created new user ${userId} for email ${req.session.pendingEmail}`
);
} }
// Сохраняем email как идентификатор // Сохраняем email как идентификатор
await identityService.saveIdentity(userId, 'email', req.session.pendingEmail, true); await identityService.saveIdentity(userId, 'email', req.session.pendingEmail, true);
// Сохраняем гостевые идентификаторы // Сохраняем гостевые идентификаторы
if (guestId) { if (guestId) {
await identityService.saveIdentity(userId, 'guest', guestId, true); await identityService.saveIdentity(userId, 'guest', guestId, true);
} }
if (previousGuestId && previousGuestId !== guestId) { if (previousGuestId && previousGuestId !== guestId) {
await identityService.saveIdentity(userId, 'guest', previousGuestId, true); await identityService.saveIdentity(userId, 'guest', previousGuestId, true);
} }
// Устанавливаем сессию // Устанавливаем сессию
req.session.userId = userId; req.session.userId = userId;
req.session.authenticated = true; req.session.authenticated = true;
req.session.authType = 'email'; req.session.authType = 'email';
req.session.email = req.session.pendingEmail; req.session.email = req.session.pendingEmail;
// Удаляем временные данные // Удаляем временные данные
delete req.session.tempUserId; delete req.session.tempUserId;
delete req.session.pendingEmail; delete req.session.pendingEmail;
// Сохраняем сессию // Сохраняем сессию
await sessionService.saveSession(req.session); await sessionService.saveSession(req.session);
// Связываем гостевые сообщения // Связываем гостевые сообщения
await sessionService.linkGuestMessages(req.session, userId); await sessionService.linkGuestMessages(req.session, userId);
return res.json({ return res.json({
success: true, success: true,
userId, userId,
email: req.session.email, email: req.session.email,
authenticated: true, authenticated: true,
isNewAuth isNewAuth,
}); });
} }
} catch (error) { } catch (error) {
logger.error('[email/verify-code] Error:', error); logger.error('[email/verify-code] Error:', error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
error: 'Ошибка сервера' error: 'Ошибка сервера',
}); });
} }
}); });
@@ -436,29 +444,29 @@ router.post('/email/verify-code', async (req, res) => {
router.post('/telegram/init', async (req, res) => { router.post('/telegram/init', async (req, res) => {
try { try {
const { verificationCode, botLink } = await initTelegramAuth(req.session); const { verificationCode, botLink } = await initTelegramAuth(req.session);
if (!verificationCode || !botLink) { if (!verificationCode || !botLink) {
throw new Error('Failed to generate verification code'); throw new Error('Failed to generate verification code');
} }
res.json({ res.json({
success: true, success: true,
verificationCode, verificationCode,
botLink botLink,
}); });
} catch (error) { } catch (error) {
logger.error('Error initializing Telegram auth:', error); logger.error('Error initializing Telegram auth:', error);
if (error.message === 'Telegram уже привязан к этому аккаунту') { if (error.message === 'Telegram уже привязан к этому аккаунту') {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
error: error.message error: error.message,
}); });
} }
res.status(500).json({ res.status(500).json({
success: false, success: false,
error: 'Failed to initialize Telegram auth' error: 'Failed to initialize Telegram auth',
}); });
} }
}); });
@@ -467,29 +475,29 @@ router.post('/telegram/init', async (req, res) => {
router.post('/email/init', async (req, res) => { router.post('/email/init', async (req, res) => {
try { try {
const { email } = req.body; const { email } = req.body;
if (!email || !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) { if (!email || !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
error: 'Некорректный формат email' error: 'Некорректный формат email',
}); });
} }
// Инициализация email аутентификации // Инициализация email аутентификации
const result = await emailAuth.initEmailAuth(req.session, email); const result = await emailAuth.initEmailAuth(req.session, email);
// Сохраняем сессию // Сохраняем сессию
await sessionService.saveSession(req.session); await sessionService.saveSession(req.session);
return res.json({ return res.json({
success: true, success: true,
message: 'Код верификации отправлен на email' message: 'Код верификации отправлен на email',
}); });
} catch (error) { } catch (error) {
logger.error('Error in email auth initialization:', error); logger.error('Error in email auth initialization:', error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
error: 'Внутренняя ошибка сервера' error: 'Внутренняя ошибка сервера',
}); });
} }
}); });
@@ -499,21 +507,20 @@ router.get('/check', async (req, res) => {
try { try {
const authenticated = req.session.authenticated || false; const authenticated = req.session.authenticated || false;
const authType = req.session.authType || null; const authType = req.session.authType || null;
let identities = []; let identities = [];
let isAdmin = false; let isAdmin = false;
if (authenticated && req.session.userId) { if (authenticated && req.session.userId) {
// Если пользователь аутентифицирован, получаем его идентификаторы из БД // Если пользователь аутентифицирован, получаем его идентификаторы из БД
try { try {
identities = await identityService.getUserIdentities(req.session.userId); identities = await identityService.getUserIdentities(req.session.userId);
// Проверяем роль пользователя // Проверяем роль пользователя
const roleResult = await db.query( const roleResult = await db.query('SELECT role FROM users WHERE id = $1', [
'SELECT role FROM users WHERE id = $1', req.session.userId,
[req.session.userId] ]);
);
if (roleResult.rows.length > 0) { if (roleResult.rows.length > 0) {
isAdmin = roleResult.rows[0].role === 'admin'; isAdmin = roleResult.rows[0].role === 'admin';
req.session.isAdmin = isAdmin; req.session.isAdmin = isAdmin;
@@ -522,15 +529,15 @@ router.get('/check', async (req, res) => {
logger.error(`[session/check] Error fetching identities: ${error.message}`); logger.error(`[session/check] Error fetching identities: ${error.message}`);
} }
} }
// Проверяем, нужно ли создать новый гостевой ID // Проверяем, нужно ли создать новый гостевой ID
if (!authenticated && !req.session.guestId) { if (!authenticated && !req.session.guestId) {
req.session.guestId = crypto.randomBytes(16).toString('hex'); req.session.guestId = crypto.randomBytes(16).toString('hex');
// Сохраняем сессию с новым гостевым ID // Сохраняем сессию с новым гостевым ID
await sessionService.saveSession(req.session); await sessionService.saveSession(req.session);
} }
// Формируем ответ // Формируем ответ
const response = { const response = {
success: true, success: true,
@@ -539,9 +546,9 @@ router.get('/check', async (req, res) => {
guestId: req.session.guestId || null, guestId: req.session.guestId || null,
authType, authType,
identitiesCount: identities.length, identitiesCount: identities.length,
isAdmin: isAdmin || false isAdmin: isAdmin || false,
}; };
// Добавляем специфические поля в зависимости от типа аутентификации // Добавляем специфические поля в зависимости от типа аутентификации
if (authType === 'wallet') { if (authType === 'wallet') {
response.address = req.session.address || null; response.address = req.session.address || null;
@@ -556,13 +563,13 @@ router.get('/check', async (req, res) => {
response.telegramFirstName = req.session.telegramFirstName; response.telegramFirstName = req.session.telegramFirstName;
} }
} }
return res.json(response); return res.json(response);
} catch (error) { } catch (error) {
logger.error('[session/check] Error:', error); logger.error('[session/check] Error:', error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
error: 'Internal server error' error: 'Internal server error',
}); });
} }
}); });
@@ -609,7 +616,7 @@ router.get('/check-access', requireAuth, async (req, res) => {
if (address) { if (address) {
const isAdmin = await authService.checkAdminTokens(address); const isAdmin = await authService.checkAdminTokens(address);
// Обновляем сессию // Обновляем сессию
req.session.isAdmin = isAdmin; req.session.isAdmin = isAdmin;
await sessionService.saveSession(req.session); await sessionService.saveSession(req.session);
@@ -618,7 +625,7 @@ router.get('/check-access', requireAuth, async (req, res) => {
success: true, success: true,
isAdmin, isAdmin,
userId, userId,
address address,
}); });
} }
@@ -626,9 +633,8 @@ router.get('/check-access', requireAuth, async (req, res) => {
success: true, success: true,
isAdmin: false, isAdmin: false,
userId, userId,
address: null address: null,
}); });
} catch (error) { } catch (error) {
logger.error('Error checking access:', error); logger.error('Error checking access:', error);
return res.status(500).json({ error: 'Internal server error' }); return res.status(500).json({ error: 'Internal server error' });
@@ -639,22 +645,22 @@ router.get('/check-access', requireAuth, async (req, res) => {
router.post('/refresh-session', async (req, res) => { router.post('/refresh-session', async (req, res) => {
try { try {
const { address } = req.body; const { address } = req.body;
if (req.session && req.session.authenticated) { if (req.session && req.session.authenticated) {
logger.info('Обновление сессии для пользователя:', req.session.userId); logger.info('Обновление сессии для пользователя:', req.session.userId);
// Обновляем время жизни сессии // Обновляем время жизни сессии
req.session.cookie.maxAge = 30 * 24 * 60 * 60 * 1000; // 30 дней req.session.cookie.maxAge = 30 * 24 * 60 * 60 * 1000; // 30 дней
// Сохраняем обновленную сессию // Сохраняем обновленную сессию
await sessionService.saveSession(req.session); await sessionService.saveSession(req.session);
return res.json({ success: true }); return res.json({ success: true });
} else if (address) { } else if (address) {
// Если сессия не аутентифицирована, но есть адрес // Если сессия не аутентифицирована, но есть адрес
try { try {
const user = await identityService.findUserByIdentity('wallet', address.toLowerCase()); const user = await identityService.findUserByIdentity('wallet', address.toLowerCase());
if (user) { if (user) {
// Обновляем сессию // Обновляем сессию
req.session.authenticated = true; req.session.authenticated = true;
@@ -662,17 +668,17 @@ router.post('/refresh-session', async (req, res) => {
req.session.address = address.toLowerCase(); req.session.address = address.toLowerCase();
req.session.isAdmin = user.role === 'admin'; req.session.isAdmin = user.role === 'admin';
req.session.authType = 'wallet'; req.session.authType = 'wallet';
// Сохраняем обновленную сессию // Сохраняем обновленную сессию
await sessionService.saveSession(req.session); await sessionService.saveSession(req.session);
return res.json({ success: true }); return res.json({ success: true });
} }
} catch (error) { } catch (error) {
logger.error('Ошибка при проверке пользователя:', error); logger.error('Ошибка при проверке пользователя:', error);
} }
} }
// Если не удалось обновить сессию, возвращаем успех=false, но не ошибку // Если не удалось обновить сессию, возвращаем успех=false, но не ошибку
return res.json({ success: false }); return res.json({ success: false });
} catch (error) { } catch (error) {
@@ -685,82 +691,78 @@ router.post('/refresh-session', async (req, res) => {
router.post('/wallet', async (req, res) => { router.post('/wallet', async (req, res) => {
try { try {
const { address, nonce, signature } = req.body; const { address, nonce, signature } = req.body;
if (!address || !nonce || !signature) { if (!address || !nonce || !signature) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
error: 'Missing required fields' error: 'Missing required fields',
}); });
} }
// Сохраняем гостевые ID до аутентификации // Сохраняем гостевые ID до аутентификации
const guestId = req.session.guestId; const guestId = req.session.guestId;
const previousGuestId = req.session.previousGuestId; const previousGuestId = req.session.previousGuestId;
// Формируем сообщение для проверки // Формируем сообщение для проверки
const message = `Sign this message to authenticate with HB3 DApp: ${nonce}`; const message = `Sign this message to authenticate with HB3 DApp: ${nonce}`;
// Проверяем подпись // Проверяем подпись
const validSignature = await authService.verifySignature(message, signature, address); const validSignature = await authService.verifySignature(message, signature, address);
if (!validSignature) { if (!validSignature) {
return res.status(401).json({ return res.status(401).json({
success: false, success: false,
error: 'Invalid signature' error: 'Invalid signature',
}); });
} }
// Получаем или создаем пользователя // Получаем или создаем пользователя
const { userId } = await authService.findOrCreateUser(address); const { userId } = await authService.findOrCreateUser(address);
// Проверяем наличие админских токенов // Проверяем наличие админских токенов
const isAdmin = await authService.checkAdminTokens(address); const isAdmin = await authService.checkAdminTokens(address);
// Обновляем роль пользователя в базе данных, если нужно // Обновляем роль пользователя в базе данных, если нужно
if (isAdmin) { if (isAdmin) {
await db.query( await db.query('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]);
'UPDATE users SET role = $1 WHERE id = $2',
['admin', userId]
);
} }
// Сохраняем идентификаторы // Сохраняем идентификаторы
await identityService.saveIdentity(userId, 'wallet', address.toLowerCase(), true); await identityService.saveIdentity(userId, 'wallet', address.toLowerCase(), true);
if (guestId) { if (guestId) {
await identityService.saveIdentity(userId, 'guest', guestId, true); await identityService.saveIdentity(userId, 'guest', guestId, true);
} }
if (previousGuestId && previousGuestId !== guestId) { if (previousGuestId && previousGuestId !== guestId) {
await identityService.saveIdentity(userId, 'guest', previousGuestId, true); await identityService.saveIdentity(userId, 'guest', previousGuestId, true);
} }
// Устанавливаем сессию // Устанавливаем сессию
req.session.userId = userId; req.session.userId = userId;
req.session.address = address.toLowerCase(); req.session.address = address.toLowerCase();
req.session.authType = 'wallet'; req.session.authType = 'wallet';
req.session.authenticated = true; req.session.authenticated = true;
req.session.isAdmin = isAdmin; req.session.isAdmin = isAdmin;
// Сохраняем сессию // Сохраняем сессию
await sessionService.saveSession(req.session); await sessionService.saveSession(req.session);
// Связываем гостевые сообщения с пользователем // Связываем гостевые сообщения с пользователем
await sessionService.linkGuestMessages(req.session, userId); await sessionService.linkGuestMessages(req.session, userId);
// Возвращаем успешный ответ // Возвращаем успешный ответ
return res.json({ return res.json({
success: true, success: true,
userId, userId,
address, address,
isAdmin, isAdmin,
authenticated: true authenticated: true,
}); });
} catch (error) { } catch (error) {
logger.error('[wallet] Error:', error); logger.error('[wallet] Error:', error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
error: 'Server error during wallet authentication' error: 'Server error during wallet authentication',
}); });
} }
}); });
@@ -769,19 +771,19 @@ router.post('/wallet', async (req, res) => {
router.get('/identities', requireAuth, async (req, res) => { router.get('/identities', requireAuth, async (req, res) => {
try { try {
const { userId } = req.session; const { userId } = req.session;
// Получаем все идентификаторы пользователя // Получаем все идентификаторы пользователя
const identities = await identityService.getUserIdentities(userId); const identities = await identityService.getUserIdentities(userId);
res.json({ res.json({
success: true, success: true,
identities identities,
}); });
} catch (error) { } catch (error) {
logger.error('Error getting user identities:', error); logger.error('Error getting user identities:', error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
error: 'Internal server error' error: 'Internal server error',
}); });
} }
}); });
@@ -794,17 +796,17 @@ router.get('/check-session', async (req, res) => {
req.session.guestId = crypto.randomBytes(16).toString('hex'); req.session.guestId = crypto.randomBytes(16).toString('hex');
await sessionService.saveSession(req.session); await sessionService.saveSession(req.session);
} }
res.json({ res.json({
success: true, success: true,
guestId: req.session.guestId, guestId: req.session.guestId,
isAuthenticated: req.session.authenticated || false isAuthenticated: req.session.authenticated || false,
}); });
} catch (error) { } catch (error) {
logger.error('Error checking session:', error); logger.error('Error checking session:', error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
error: 'Internal server error' error: 'Internal server error',
}); });
} }
}); });
@@ -813,21 +815,21 @@ router.get('/check-session', async (req, res) => {
router.get('/check-tokens/:address', async (req, res) => { router.get('/check-tokens/:address', async (req, res) => {
try { try {
const { address } = req.params; const { address } = req.params;
// Получаем балансы токенов на всех сетях // Получаем балансы токенов на всех сетях
const balances = await authService.getTokenBalances(address); const balances = await authService.getTokenBalances(address);
res.json({ res.json({
success: true, success: true,
balances balances,
}); });
} catch (error) { } catch (error) {
logger.error('Error checking token balances:', error); logger.error('Error checking token balances:', error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
error: 'Internal server error' error: 'Internal server error',
}); });
} }
}); });
module.exports = router; module.exports = router;

View File

@@ -12,19 +12,19 @@ const { v4: uuidv4 } = require('uuid');
async function processGuestMessages(userId, guestId) { async function processGuestMessages(userId, guestId) {
try { try {
console.log(`Processing guest messages for user ${userId} with guest ID ${guestId}`); console.log(`Processing guest messages for user ${userId} with guest ID ${guestId}`);
// Проверяем, обрабатывались ли уже эти сообщения // Проверяем, обрабатывались ли уже эти сообщения
const mappingCheck = await db.query( const mappingCheck = await db.query(
'SELECT processed FROM guest_user_mapping WHERE guest_id = $1', 'SELECT processed FROM guest_user_mapping WHERE guest_id = $1',
[guestId] [guestId]
); );
// Если сообщения уже обработаны, пропускаем // Если сообщения уже обработаны, пропускаем
if (mappingCheck.rows.length > 0 && mappingCheck.rows[0].processed) { if (mappingCheck.rows.length > 0 && mappingCheck.rows[0].processed) {
console.log(`Guest messages for guest ID ${guestId} were already processed.`); console.log(`Guest messages for guest ID ${guestId} were already processed.`);
return { success: true, message: 'Guest messages already processed' }; return { success: true, message: 'Guest messages already processed' };
} }
// Проверяем наличие mapping записи и создаем если нет // Проверяем наличие mapping записи и создаем если нет
if (mappingCheck.rows.length === 0) { if (mappingCheck.rows.length === 0) {
await db.query( await db.query(
@@ -33,49 +33,49 @@ async function processGuestMessages(userId, guestId) {
); );
console.log(`Created mapping for guest ID ${guestId} to user ${userId}`); console.log(`Created mapping for guest ID ${guestId} to user ${userId}`);
} }
// Получаем все гостевые сообщения // Получаем все гостевые сообщения
const guestMessagesResult = await db.query( const guestMessagesResult = await db.query(
'SELECT * FROM guest_messages WHERE guest_id = $1 ORDER BY created_at ASC', 'SELECT * FROM guest_messages WHERE guest_id = $1 ORDER BY created_at ASC',
[guestId] [guestId]
); );
if (guestMessagesResult.rows.length === 0) { if (guestMessagesResult.rows.length === 0) {
console.log('No guest messages found'); console.log('No guest messages found');
// Помечаем как обработанные, даже если сообщений нет // Помечаем как обработанные, даже если сообщений нет
await db.query( await db.query('UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', [
'UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', guestId,
[guestId] ]);
);
return { success: true, message: 'No guest messages found' }; return { success: true, message: 'No guest messages found' };
} }
const guestMessages = guestMessagesResult.rows; const guestMessages = guestMessagesResult.rows;
console.log(`Found ${guestMessages.length} guest messages`); console.log(`Found ${guestMessages.length} guest messages`);
// Создаем новый диалог для этих сообщений // Создаем новый диалог для этих сообщений
const firstMessage = guestMessages[0]; const firstMessage = guestMessages[0];
const title = firstMessage.content.length > 30 const title =
? `${firstMessage.content.substring(0, 30)}...` firstMessage.content.length > 30
: firstMessage.content; ? `${firstMessage.content.substring(0, 30)}...`
: firstMessage.content;
const newConversationResult = await db.query( const newConversationResult = await db.query(
'INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING *', 'INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING *',
[userId, title] [userId, title]
); );
const conversation = newConversationResult.rows[0]; const conversation = newConversationResult.rows[0];
console.log('Created new conversation for guest messages:', conversation); console.log('Created new conversation for guest messages:', conversation);
// Отслеживаем успешные сохранения сообщений // Отслеживаем успешные сохранения сообщений
const savedMessageIds = []; const savedMessageIds = [];
// Обрабатываем каждое гостевое сообщение // Обрабатываем каждое гостевое сообщение
for (const guestMessage of guestMessages) { for (const guestMessage of guestMessages) {
console.log(`Processing guest message ID ${guestMessage.id}: ${guestMessage.content}`); console.log(`Processing guest message ID ${guestMessage.id}: ${guestMessage.content}`);
try { try {
// Сохраняем сообщение пользователя // Сохраняем сообщение пользователя
const userMessageResult = await db.query( const userMessageResult = await db.query(
@@ -85,26 +85,26 @@ async function processGuestMessages(userId, guestId) {
($1, $2, $3, $4, $5, $6, $7) ($1, $2, $3, $4, $5, $6, $7)
RETURNING *`, RETURNING *`,
[ [
conversation.id, conversation.id,
guestMessage.content, guestMessage.content,
'user', 'user',
'user', 'user',
'web', 'web',
guestMessage.created_at, guestMessage.created_at,
userId // Добавляем userId в сообщение для прямой связи userId, // Добавляем userId в сообщение для прямой связи
] ]
); );
console.log(`Saved user message with ID ${userMessageResult.rows[0].id}`); console.log(`Saved user message with ID ${userMessageResult.rows[0].id}`);
savedMessageIds.push(guestMessage.id); savedMessageIds.push(guestMessage.id);
// Получаем ответ от ИИ только для сообщений пользователя (не AI) // Получаем ответ от ИИ только для сообщений пользователя (не AI)
if (!guestMessage.is_ai) { if (!guestMessage.is_ai) {
console.log('Getting AI response for:', guestMessage.content); console.log('Getting AI response for:', guestMessage.content);
const language = guestMessage.language || 'auto'; const language = guestMessage.language || 'auto';
const aiResponse = await aiAssistant.getResponse(guestMessage.content, language); const aiResponse = await aiAssistant.getResponse(guestMessage.content, language);
console.log('AI response received:', aiResponse); console.log('AI response received:', aiResponse);
// Сохраняем ответ от ИИ // Сохраняем ответ от ИИ
const aiMessageResult = await db.query( const aiMessageResult = await db.query(
`INSERT INTO messages `INSERT INTO messages
@@ -113,16 +113,16 @@ async function processGuestMessages(userId, guestId) {
($1, $2, $3, $4, $5, $6, $7) ($1, $2, $3, $4, $5, $6, $7)
RETURNING *`, RETURNING *`,
[ [
conversation.id, conversation.id,
aiResponse, aiResponse,
'assistant', 'assistant',
'assistant', 'assistant',
'web', 'web',
new Date(), new Date(),
userId // Добавляем userId в сообщение для прямой связи userId, // Добавляем userId в сообщение для прямой связи
] ]
); );
console.log(`Saved AI response with ID ${aiMessageResult.rows[0].id}`); console.log(`Saved AI response with ID ${aiMessageResult.rows[0].id}`);
} }
} catch (error) { } catch (error) {
@@ -130,25 +130,26 @@ async function processGuestMessages(userId, guestId) {
// Продолжаем с другими сообщениями в случае ошибки // Продолжаем с другими сообщениями в случае ошибки
} }
} }
// Удаляем только успешно обработанные гостевые сообщения // Удаляем только успешно обработанные гостевые сообщения
if (savedMessageIds.length > 0) { if (savedMessageIds.length > 0) {
await db.query('DELETE FROM guest_messages WHERE id = ANY($1)', [savedMessageIds]); await db.query('DELETE FROM guest_messages WHERE id = ANY($1)', [savedMessageIds]);
console.log(`Deleted ${savedMessageIds.length} processed guest messages for guest ID ${guestId}`); console.log(
`Deleted ${savedMessageIds.length} processed guest messages for guest ID ${guestId}`
// Помечаем гостевой ID как обработанный
await db.query(
'UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1',
[guestId]
); );
// Помечаем гостевой ID как обработанный
await db.query('UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', [
guestId,
]);
} else { } else {
console.log('No guest messages were successfully processed, skipping deletion'); console.log('No guest messages were successfully processed, skipping deletion');
} }
return { return {
success: true, success: true,
message: `Processed ${savedMessageIds.length} of ${guestMessages.length} guest messages`, message: `Processed ${savedMessageIds.length} of ${guestMessages.length} guest messages`,
conversationId: conversation.id conversationId: conversation.id,
}; };
} catch (error) { } catch (error) {
console.error('Error processing guest messages:', error); console.error('Error processing guest messages:', error);
@@ -160,11 +161,11 @@ async function processGuestMessages(userId, guestId) {
router.post('/guest-message', async (req, res) => { router.post('/guest-message', async (req, res) => {
try { try {
const { content, language, guestId: requestGuestId } = req.body; const { content, language, guestId: requestGuestId } = req.body;
if (!content) { if (!content) {
return res.status(400).json({ success: false, error: 'Content is required' }); return res.status(400).json({ success: false, error: 'Content is required' });
} }
// Используем гостевой ID из запроса или из сессии, или генерируем новый // Используем гостевой ID из запроса или из сессии, или генерируем новый
const guestId = requestGuestId || req.session.guestId || crypto.randomBytes(16).toString('hex'); const guestId = requestGuestId || req.session.guestId || crypto.randomBytes(16).toString('hex');
@@ -182,9 +183,9 @@ router.post('/guest-message', async (req, res) => {
console.log('Guest message saved:', result.rows[0]); console.log('Guest message saved:', result.rows[0]);
res.json({ res.json({
success: true, success: true,
messageId: result.rows[0].id messageId: result.rows[0].id,
}); });
} catch (error) { } catch (error) {
console.error('Error saving guest message:', error); console.error('Error saving guest message:', error);
@@ -195,43 +196,48 @@ router.post('/guest-message', async (req, res) => {
// Маршрут для обычных сообщений (для аутентифицированных пользователей) // Маршрут для обычных сообщений (для аутентифицированных пользователей)
router.post('/message', requireAuth, async (req, res) => { router.post('/message', requireAuth, async (req, res) => {
const { message, conversationId, language = 'auto' } = req.body; const { message, conversationId, language = 'auto' } = req.body;
if (!message) { if (!message) {
return res.status(400).json({ error: 'Message is required' }); return res.status(400).json({ error: 'Message is required' });
} }
try { try {
console.log('Processing message:', { message, conversationId, language, userId: req.session.userId }); console.log('Processing message:', {
message,
conversationId,
language,
userId: req.session.userId,
});
const userId = req.session.userId; const userId = req.session.userId;
let conversation; let conversation;
// Если указан ID диалога, проверяем его существование и принадлежность пользователю // Если указан ID диалога, проверяем его существование и принадлежность пользователю
if (conversationId) { if (conversationId) {
const conversationResult = await db.query( const conversationResult = await db.query(
'SELECT * FROM conversations WHERE id = $1 AND user_id = $2', 'SELECT * FROM conversations WHERE id = $1 AND user_id = $2',
[conversationId, userId] [conversationId, userId]
); );
if (conversationResult.rows.length === 0) { if (conversationResult.rows.length === 0) {
return res.status(404).json({ error: 'Conversation not found or access denied' }); return res.status(404).json({ error: 'Conversation not found or access denied' });
} }
conversation = conversationResult.rows[0]; conversation = conversationResult.rows[0];
console.log('Using existing conversation:', conversation); console.log('Using existing conversation:', conversation);
} else { } else {
// Создаем новый диалог // Создаем новый диалог
const title = message.length > 30 ? `${message.substring(0, 30)}...` : message; const title = message.length > 30 ? `${message.substring(0, 30)}...` : message;
const newConversationResult = await db.query( const newConversationResult = await db.query(
'INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING *', 'INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING *',
[userId, title] [userId, title]
); );
conversation = newConversationResult.rows[0]; conversation = newConversationResult.rows[0];
console.log('Created new conversation:', conversation); console.log('Created new conversation:', conversation);
} }
// Сохраняем сообщение пользователя // Сохраняем сообщение пользователя
console.log('Saving user message'); console.log('Saving user message');
const userMessageResult = await db.query( const userMessageResult = await db.query(
@@ -242,12 +248,12 @@ router.post('/message', requireAuth, async (req, res) => {
RETURNING *`, RETURNING *`,
[conversation.id, message, 'user', 'user', 0, 'web', new Date()] [conversation.id, message, 'user', 'user', 0, 'web', new Date()]
); );
// Получаем ответ от ИИ // Получаем ответ от ИИ
console.log('Getting AI response'); console.log('Getting AI response');
const aiResponse = await aiAssistant.getResponse(message, language); const aiResponse = await aiAssistant.getResponse(message, language);
console.log('AI response received:', aiResponse); console.log('AI response received:', aiResponse);
// Сохраняем ответ от ИИ // Сохраняем ответ от ИИ
console.log('Saving AI response'); console.log('Saving AI response');
const aiMessageResult = await db.query( const aiMessageResult = await db.query(
@@ -258,14 +264,14 @@ router.post('/message', requireAuth, async (req, res) => {
RETURNING *`, RETURNING *`,
[conversation.id, aiResponse, 'assistant', 'assistant', 0, 'web', new Date()] [conversation.id, aiResponse, 'assistant', 'assistant', 0, 'web', new Date()]
); );
const response = { const response = {
success: true, success: true,
userMessage: userMessageResult.rows[0], userMessage: userMessageResult.rows[0],
aiMessage: aiMessageResult.rows[0], aiMessage: aiMessageResult.rows[0],
conversation conversation,
}; };
res.json(response); res.json(response);
} catch (error) { } catch (error) {
console.error('Error processing message:', error); console.error('Error processing message:', error);
@@ -296,9 +302,9 @@ router.get('/history', async (req, res) => {
userId: req.session.userId, userId: req.session.userId,
address: req.session.address, address: req.session.address,
authenticated: req.session.authenticated, authenticated: req.session.authenticated,
guestId: req.session.guestId guestId: req.session.guestId,
}); });
const limit = parseInt(req.query.limit) || 50; const limit = parseInt(req.query.limit) || 50;
const offset = parseInt(req.query.offset) || 0; const offset = parseInt(req.query.offset) || 0;
@@ -308,11 +314,11 @@ router.get('/history', async (req, res) => {
try { try {
console.log('Automatically linking guest messages before fetching history'); console.log('Automatically linking guest messages before fetching history');
await processGuestMessages(req.session.userId, req.session.guestId); await processGuestMessages(req.session.userId, req.session.guestId);
// Очищаем guestId из сессии после связывания // Очищаем guestId из сессии после связывания
req.session.guestId = null; req.session.guestId = null;
await req.session.save(); await req.session.save();
console.log('Guest messages automatically linked'); console.log('Guest messages automatically linked');
} catch (linkError) { } catch (linkError) {
console.error('Error auto-linking guest messages:', linkError); console.error('Error auto-linking guest messages:', linkError);
@@ -349,17 +355,16 @@ router.get('/history', async (req, res) => {
LIMIT $2 OFFSET $3`, LIMIT $2 OFFSET $3`,
[req.session.userId, limit, offset] [req.session.userId, limit, offset]
); );
messages = result.rows; messages = result.rows;
console.log(`Found ${messages.length} messages for authenticated user`); console.log(`Found ${messages.length} messages for authenticated user`);
} }
return res.json({ return res.json({
success: true, success: true,
messages: messages, messages: messages,
total: total total: total,
}); });
} catch (error) { } catch (error) {
logger.error('Error getting chat history:', error); logger.error('Error getting chat history:', error);
return res.status(500).json({ error: 'Internal server error' }); return res.status(500).json({ error: 'Internal server error' });
@@ -368,4 +373,4 @@ router.get('/history', async (req, res) => {
// Экспортируем маршрутизатор и функцию processGuestMessages отдельно // Экспортируем маршрутизатор и функцию processGuestMessages отдельно
module.exports = router; module.exports = router;
module.exports.processGuestMessages = processGuestMessages; module.exports.processGuestMessages = processGuestMessages;

View File

@@ -26,20 +26,20 @@ router.post('/link', requireAuth, async (req, res) => {
// Если тип - wallet, сначала проверим, не привязан ли он уже к другому пользователю // Если тип - wallet, сначала проверим, не привязан ли он уже к другому пользователю
if (type === 'wallet') { if (type === 'wallet') {
const normalizedWallet = value.toLowerCase(); const normalizedWallet = value.toLowerCase();
// Проверяем, существует ли уже такой кошелек // Проверяем, существует ли уже такой кошелек
const existingCheck = await db.query( const existingCheck = await db.query(
`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]
); );
if (existingCheck.rows.length > 0) { if (existingCheck.rows.length > 0) {
const existingUserId = existingCheck.rows[0].user_id; const existingUserId = existingCheck.rows[0].user_id;
if (existingUserId !== userId) { if (existingUserId !== userId) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
error: `This wallet (${value}) is already linked to another account` error: `This wallet (${value}) is already linked to another account`,
}); });
} }
} }
@@ -57,22 +57,22 @@ router.post('/link', requireAuth, async (req, res) => {
req.session.email = value; req.session.email = value;
} }
res.json({ res.json({
success: true, success: true,
message: 'Identity linked successfully', message: 'Identity linked successfully',
isAdmin: req.session.isAdmin isAdmin: req.session.isAdmin,
}); });
} catch (error) { } catch (error) {
logger.error('Error linking identity:', error); logger.error('Error linking identity:', error);
// Делаем более понятные сообщения об ошибках // Делаем более понятные сообщения об ошибках
if (error.message && error.message.includes('already belongs to another user')) { if (error.message && error.message.includes('already belongs to another user')) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
error: `This identity is already linked to another account` error: `This identity is already linked to another account`,
}); });
} }
res.status(500).json({ error: error.message || 'Internal server error' }); res.status(500).json({ error: error.message || 'Internal server error' });
} }
}); });
@@ -93,13 +93,13 @@ router.get('/token-balances', requireAuth, async (req, res) => {
// Здесь логирование инициирования получения баланса может быть полезно // Здесь логирование инициирования получения баланса может быть полезно
logger.info(`Fetching token balances for user ${userId} with wallet ${wallet}`); logger.info(`Fetching token balances for user ${userId} with wallet ${wallet}`);
// Получаем балансы токенов // Получаем балансы токенов
const balances = await authService.getTokenBalances(wallet); const balances = await authService.getTokenBalances(wallet);
res.json({ res.json({
success: true, success: true,
balances balances,
}); });
} catch (error) { } catch (error) {
logger.error('Error getting token balances:', error); logger.error('Error getting token balances:', error);

View File

@@ -8,23 +8,23 @@ const logger = require('../utils/logger');
router.get('/balances', requireAuth, async (req, res) => { router.get('/balances', requireAuth, async (req, res) => {
try { try {
const { address } = req.session; const { address } = req.session;
if (!address) { if (!address) {
return res.status(400).json({ return res.status(400).json({
error: 'No wallet address in session' error: 'No wallet address in session',
}); });
} }
logger.info(`Fetching token balances for address: ${address}`); logger.info(`Fetching token balances for address: ${address}`);
const balances = await authService.getTokenBalances(address); const balances = await authService.getTokenBalances(address);
res.json(balances); res.json(balances);
} catch (error) { } catch (error) {
logger.error('Error fetching token balances:', error); logger.error('Error fetching token balances:', error);
res.status(500).json({ res.status(500).json({
error: 'Failed to fetch token balances' error: 'Failed to fetch token balances',
}); });
} }
}); });
module.exports = router; module.exports = router;

View File

@@ -23,19 +23,16 @@ router.post('/update-language', requireAuth, async (req, res) => {
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.query( await db.query('UPDATE users SET preferred_language = $1 WHERE id = $2', [language, userId]);
'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);
@@ -48,22 +45,23 @@ router.post('/update-profile', requireAuth, async (req, res) => {
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.query( await db.query('UPDATE users SET first_name = $1, last_name = $2 WHERE id = $3', [
'UPDATE users SET first_name = $1, last_name = $2 WHERE id = $3', firstName || null,
[firstName || null, lastName || null, userId] lastName || null,
); 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);
@@ -75,29 +73,29 @@ router.post('/update-profile', requireAuth, async (req, res) => {
router.get('/profile/current', requireAuth, async (req, res) => { router.get('/profile/current', requireAuth, async (req, res) => {
try { try {
const userId = req.session.userId; const userId = req.session.userId;
// Получение данных пользователя // Получение данных пользователя
const userResult = await db.query( const userResult = await db.query(
'SELECT id, username, first_name, last_name, role, status, created_at, preferred_language FROM users WHERE id = $1', 'SELECT id, username, first_name, last_name, role, status, created_at, preferred_language FROM users WHERE id = $1',
[userId] [userId]
); );
if (userResult.rows.length === 0) { if (userResult.rows.length === 0) {
return res.status(404).json({ error: 'Пользователь не найден' }); return res.status(404).json({ error: 'Пользователь не найден' });
} }
// Получение идентификаторов пользователя // Получение идентификаторов пользователя
const identitiesResult = await db.query( const identitiesResult = await db.query(
'SELECT provider, provider_id FROM user_identities WHERE user_id = $1', 'SELECT provider, provider_id FROM user_identities WHERE user_id = $1',
[userId] [userId]
); );
const user = userResult.rows[0]; const user = userResult.rows[0];
const identities = identitiesResult.rows.reduce((acc, identity) => { const identities = identitiesResult.rows.reduce((acc, identity) => {
acc[identity.provider] = identity.provider_id; acc[identity.provider] = identity.provider_id;
return acc; return acc;
}, {}); }, {});
res.json({ res.json({
id: user.id, id: user.id,
username: user.username, username: user.username,
@@ -107,7 +105,7 @@ router.get('/profile/current', requireAuth, async (req, res) => {
status: user.status, status: user.status,
createdAt: user.created_at, createdAt: user.created_at,
preferredLanguage: user.preferred_language, preferredLanguage: user.preferred_language,
identities identities,
}); });
} catch (error) { } catch (error) {
logger.error('Error getting user profile:', error); logger.error('Error getting user profile:', error);

View File

@@ -15,7 +15,7 @@ if (!fs.existsSync(logDir)) {
const logFile = path.join(logDir, 'fix-duplicates.log'); const logFile = path.join(logDir, 'fix-duplicates.log');
const logger = { const logger = {
log: message => { log: (message) => {
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${message}\n`; const logMessage = `[${timestamp}] ${message}\n`;
console.log(message); console.log(message);
@@ -27,7 +27,7 @@ const logger = {
const logMessage = `[${timestamp}] ERROR: ${message}${errorDetail}\n`; const logMessage = `[${timestamp}] ERROR: ${message}${errorDetail}\n`;
console.error(`ERROR: ${message}${errorDetail}`); console.error(`ERROR: ${message}${errorDetail}`);
fs.appendFileSync(logFile, logMessage); fs.appendFileSync(logFile, logMessage);
} },
}; };
// Создаем подключение к базе данных // Создаем подключение к базе данных
@@ -54,10 +54,10 @@ function normalizeWalletAddress(address) {
*/ */
async function findDuplicateWallets() { async function findDuplicateWallets() {
const client = await pool.connect(); const client = await pool.connect();
try { try {
logger.log('Поиск дубликатов wallet-идентификаторов...'); logger.log('Поиск дубликатов wallet-идентификаторов...');
// Находим пары идентификаторов, которые отличаются только регистром // Находим пары идентификаторов, которые отличаются только регистром
const result = await client.query(` const result = await client.query(`
SELECT SELECT
@@ -77,9 +77,9 @@ async function findDuplicateWallets() {
LOWER(ui1.provider_id) = LOWER(ui2.provider_id) AND LOWER(ui1.provider_id) = LOWER(ui2.provider_id) AND
ui1.provider_id <> ui2.provider_id ui1.provider_id <> ui2.provider_id
`); `);
logger.log(`Найдено ${result.rows.length} потенциальных дубликатов wallet-идентификаторов`); logger.log(`Найдено ${result.rows.length} потенциальных дубликатов wallet-идентификаторов`);
return result.rows; return result.rows;
} catch (error) { } catch (error) {
logger.error('Ошибка при поиске дубликатов wallet-идентификаторов', error); logger.error('Ошибка при поиске дубликатов wallet-идентификаторов', error);
@@ -95,46 +95,52 @@ async function findDuplicateWallets() {
*/ */
async function fixDuplicates(duplicates) { async function fixDuplicates(duplicates) {
const client = await pool.connect(); const client = await pool.connect();
try { try {
logger.log('Исправление дубликатов идентификаторов...'); logger.log('Исправление дубликатов идентификаторов...');
await client.query('BEGIN'); await client.query('BEGIN');
for (const dup of duplicates) { for (const dup of duplicates) {
// Проверяем, принадлежат ли идентификаторы одному пользователю // Проверяем, принадлежат ли идентификаторы одному пользователю
if (dup.user_id1 === dup.user_id2) { if (dup.user_id1 === dup.user_id2) {
// Если да, удаляем один из дубликатов (не в нижнем регистре) // Если да, удаляем один из дубликатов (не в нижнем регистре)
const normalizedAddress = normalizeWalletAddress(dup.provider_id1); const normalizedAddress = normalizeWalletAddress(dup.provider_id1);
// Определяем, какой идентификатор нужно удалить // Определяем, какой идентификатор нужно удалить
const idToDelete = dup.provider_id1 === normalizedAddress ? dup.id2 : dup.id1; const idToDelete = dup.provider_id1 === normalizedAddress ? dup.id2 : dup.id1;
logger.log(`Удаление дубликата ID ${idToDelete} для адреса ${normalizedAddress}`); logger.log(`Удаление дубликата ID ${idToDelete} для адреса ${normalizedAddress}`);
await client.query('DELETE FROM user_identities WHERE id = $1', [idToDelete]); await client.query('DELETE FROM user_identities WHERE id = $1', [idToDelete]);
// Проверяем, что второй идентификатор в нормализованной форме // Проверяем, что второй идентификатор в нормализованной форме
const remainingId = dup.provider_id1 === normalizedAddress ? dup.id1 : dup.id2; const remainingId = dup.provider_id1 === normalizedAddress ? dup.id1 : dup.id2;
const remainingAddress = dup.provider_id1 === normalizedAddress ? dup.provider_id1 : dup.provider_id2; const remainingAddress =
dup.provider_id1 === normalizedAddress ? dup.provider_id1 : dup.provider_id2;
if (remainingAddress !== normalizedAddress) { if (remainingAddress !== normalizedAddress) {
logger.log(`Обновление идентификатора ID ${remainingId} до нормализованного значения ${normalizedAddress}`); logger.log(
`Обновление идентификатора ID ${remainingId} до нормализованного значения ${normalizedAddress}`
await client.query(
'UPDATE user_identities SET provider_id = $1 WHERE id = $2',
[normalizedAddress, remainingId]
); );
await client.query('UPDATE user_identities SET provider_id = $1 WHERE id = $2', [
normalizedAddress,
remainingId,
]);
} }
} else { } else {
// Если идентификаторы принадлежат разным пользователям, нужно решить конфликт // Если идентификаторы принадлежат разным пользователям, нужно решить конфликт
// Для определения какой пользователь является основным, можно использовать: // Для определения какой пользователь является основным, можно использовать:
// 1. Количество сообщений/активности // 1. Количество сообщений/активности
// 2. Дату создания аккаунта // 2. Дату создания аккаунта
logger.log(`Конфликт: адрес ${dup.provider_id1}/${dup.provider_id2} привязан к разным пользователям: ${dup.user_id1} и ${dup.user_id2}`); logger.log(
`Конфликт: адрес ${dup.provider_id1}/${dup.provider_id2} привязан к разным пользователям: ${dup.user_id1} и ${dup.user_id2}`
);
// Определяем, какой пользователь является основным // Определяем, какой пользователь является основным
const userInfoResult = await client.query(` const userInfoResult = await client.query(
`
SELECT SELECT
id, id,
(SELECT COUNT(*) FROM messages WHERE user_id = users.id) as message_count, (SELECT COUNT(*) FROM messages WHERE user_id = users.id) as message_count,
@@ -145,45 +151,55 @@ async function fixDuplicates(duplicates) {
id IN ($1, $2) id IN ($1, $2)
ORDER BY ORDER BY
message_count DESC, created_at ASC message_count DESC, created_at ASC
`, [dup.user_id1, dup.user_id2]); `,
[dup.user_id1, dup.user_id2]
);
// Если нет пользователей, пропускаем // Если нет пользователей, пропускаем
if (userInfoResult.rows.length === 0) { if (userInfoResult.rows.length === 0) {
logger.log(`Пропуск: не найдены пользователи ${dup.user_id1} и ${dup.user_id2}`); logger.log(`Пропуск: не найдены пользователи ${dup.user_id1} и ${dup.user_id2}`);
continue; continue;
} }
// Выбираем первого пользователя как основного (с наибольшим количеством сообщений или самого старого) // Выбираем первого пользователя как основного (с наибольшим количеством сообщений или самого старого)
const mainUserId = userInfoResult.rows[0].id; const mainUserId = userInfoResult.rows[0].id;
const secondaryUserId = mainUserId === dup.user_id1 ? dup.user_id2 : dup.user_id1; const secondaryUserId = mainUserId === dup.user_id1 ? dup.user_id2 : dup.user_id1;
logger.log(`Объединение пользователей: сохраняем ID ${mainUserId}, удаляем ID ${secondaryUserId}`); logger.log(
`Объединение пользователей: сохраняем ID ${mainUserId}, удаляем ID ${secondaryUserId}`
);
// Переносим все идентификаторы от вторичного пользователя к основному // Переносим все идентификаторы от вторичного пользователя к основному
await client.query(` await client.query(
`
INSERT INTO user_identities (user_id, provider, provider_id) INSERT INTO user_identities (user_id, provider, provider_id)
SELECT $1, provider, provider_id SELECT $1, provider, provider_id
FROM user_identities FROM user_identities
WHERE user_id = $2 WHERE user_id = $2
ON CONFLICT DO NOTHING ON CONFLICT DO NOTHING
`, [mainUserId, secondaryUserId]); `,
[mainUserId, secondaryUserId]
);
// Переносим сообщения // Переносим сообщения
await client.query(` await client.query(
`
UPDATE messages UPDATE messages
SET user_id = $1 SET user_id = $1
WHERE user_id = $2 WHERE user_id = $2
`, [mainUserId, secondaryUserId]); `,
[mainUserId, secondaryUserId]
);
// Переносим другие связанные данные... // Переносим другие связанные данные...
// ... // ...
// Удаляем вторичного пользователя // Удаляем вторичного пользователя
await client.query('DELETE FROM user_identities WHERE user_id = $1', [secondaryUserId]); await client.query('DELETE FROM user_identities WHERE user_id = $1', [secondaryUserId]);
await client.query('DELETE FROM users WHERE id = $1', [secondaryUserId]); await client.query('DELETE FROM users WHERE id = $1', [secondaryUserId]);
} }
} }
await client.query('COMMIT'); await client.query('COMMIT');
logger.log('Исправление дубликатов успешно завершено'); logger.log('Исправление дубликатов успешно завершено');
} catch (error) { } catch (error) {
@@ -201,43 +217,43 @@ async function fixDuplicates(duplicates) {
async function main() { async function main() {
try { try {
logger.log('Запуск скрипта исправления дубликатов идентификаторов...'); logger.log('Запуск скрипта исправления дубликатов идентификаторов...');
// Шаг 1: Нормализация всех адресов кошельков (приведение к нижнему регистру) // Шаг 1: Нормализация всех адресов кошельков (приведение к нижнему регистру)
const client = await pool.connect(); const client = await pool.connect();
try { try {
logger.log('Нормализация всех существующих адресов кошельков...'); logger.log('Нормализация всех существующих адресов кошельков...');
await client.query('BEGIN'); await client.query('BEGIN');
// Получаем все идентификаторы кошельков // Получаем все идентификаторы кошельков
const walletsResult = await client.query(` const walletsResult = await client.query(`
SELECT id, provider_id SELECT id, provider_id
FROM user_identities FROM user_identities
WHERE provider = 'wallet' WHERE provider = 'wallet'
`); `);
logger.log(`Найдено ${walletsResult.rows.length} идентификаторов кошельков`); logger.log(`Найдено ${walletsResult.rows.length} идентификаторов кошельков`);
// Обновляем каждый адрес к нормализованной форме // Обновляем каждый адрес к нормализованной форме
let updatedCount = 0; let updatedCount = 0;
for (const wallet of walletsResult.rows) { for (const wallet of walletsResult.rows) {
try { try {
const normalizedAddress = normalizeWalletAddress(wallet.provider_id); const normalizedAddress = normalizeWalletAddress(wallet.provider_id);
if (normalizedAddress !== wallet.provider_id) { if (normalizedAddress !== wallet.provider_id) {
await client.query( await client.query('UPDATE user_identities SET provider_id = $1 WHERE id = $2', [
'UPDATE user_identities SET provider_id = $1 WHERE id = $2', normalizedAddress,
[normalizedAddress, wallet.id] wallet.id,
); ]);
updatedCount++; updatedCount++;
} }
} catch (error) { } catch (error) {
logger.error(`Ошибка при нормализации адреса ${wallet.provider_id}`, error); logger.error(`Ошибка при нормализации адреса ${wallet.provider_id}`, error);
} }
} }
await client.query('COMMIT'); await client.query('COMMIT');
logger.log(`Нормализовано ${updatedCount} адресов кошельков`); logger.log(`Нормализовано ${updatedCount} адресов кошельков`);
} catch (error) { } catch (error) {
@@ -246,16 +262,16 @@ async function main() {
} finally { } finally {
client.release(); client.release();
} }
// Шаг 2: Поиск и исправление дубликатов // Шаг 2: Поиск и исправление дубликатов
const duplicates = await findDuplicateWallets(); const duplicates = await findDuplicateWallets();
if (duplicates.length > 0) { if (duplicates.length > 0) {
await fixDuplicates(duplicates); await fixDuplicates(duplicates);
} else { } else {
logger.log('Дубликатов wallet-идентификаторов не найдено'); logger.log('Дубликатов wallet-идентификаторов не найдено');
} }
logger.log('Скрипт успешно завершил работу'); logger.log('Скрипт успешно завершил работу');
} catch (error) { } catch (error) {
logger.error('Критическая ошибка при выполнении скрипта', error); logger.error('Критическая ошибка при выполнении скрипта', error);
@@ -265,4 +281,4 @@ async function main() {
} }
// Запускаем скрипт // Запускаем скрипт
main(); main();

View File

@@ -19,7 +19,7 @@ async function runMigrations() {
// Получаем список выполненных миграций // Получаем список выполненных миграций
const { rows } = await pool.query('SELECT name FROM migrations'); const { rows } = await pool.query('SELECT name FROM migrations');
const executedMigrations = new Set(rows.map(row => row.name)); const executedMigrations = new Set(rows.map((row) => row.name));
// Читаем файлы миграций // Читаем файлы миграций
const migrationsDir = path.join(__dirname, '../db/migrations'); const migrationsDir = path.join(__dirname, '../db/migrations');
@@ -27,7 +27,7 @@ async function runMigrations() {
// Сортируем файлы по номеру // Сортируем файлы по номеру
const migrationFiles = files const migrationFiles = files
.filter(f => f.endsWith('.sql')) .filter((f) => f.endsWith('.sql'))
.sort((a, b) => { .sort((a, b) => {
const numA = parseInt(a.split('_')[0]); const numA = parseInt(a.split('_')[0]);
const numB = parseInt(b.split('_')[0]); const numB = parseInt(b.split('_')[0]);
@@ -55,14 +55,19 @@ async function runMigrations() {
// Выполняем SQL-функции // Выполняем SQL-функции
const functionsDir = path.join(migrationsDir, 'functions'); const functionsDir = path.join(migrationsDir, 'functions');
if (await fs.stat(functionsDir).then(() => true).catch(() => false)) { if (
await fs
.stat(functionsDir)
.then(() => true)
.catch(() => false)
) {
const functionFiles = await fs.readdir(functionsDir); const functionFiles = await fs.readdir(functionsDir);
for (const file of functionFiles) { for (const file of functionFiles) {
if (file.endsWith('.sql')) { if (file.endsWith('.sql')) {
const filePath = path.join(functionsDir, file); const filePath = path.join(functionsDir, file);
const sql = await fs.readFile(filePath, 'utf-8'); const sql = await fs.readFile(filePath, 'utf-8');
try { try {
await pool.query(sql); await pool.query(sql);
logger.info(`Function ${file} executed successfully`); logger.info(`Function ${file} executed successfully`);

View File

@@ -26,15 +26,15 @@ console.log('Используемый порт:', process.env.PORT || 8000);
async function initServices() { async function initServices() {
try { try {
console.log('Инициализация сервисов...'); console.log('Инициализация сервисов...');
// Останавливаем предыдущий экземпляр бота // Останавливаем предыдущий экземпляр бота
await stopBot(); await stopBot();
// Добавляем обработку ошибок при запуске бота // Добавляем обработку ошибок при запуске бота
try { try {
await getBot(); // getBot теперь асинхронный и сам запускает бота await getBot(); // getBot теперь асинхронный и сам запускает бота
console.log('Telegram bot started'); console.log('Telegram bot started');
// Добавляем graceful shutdown // Добавляем graceful shutdown
process.once('SIGINT', async () => { process.once('SIGINT', async () => {
await stopBot(); await stopBot();
@@ -46,14 +46,16 @@ async function initServices() {
}); });
} catch (error) { } catch (error) {
if (error.code === 409) { if (error.code === 409) {
logger.warn('Another instance of Telegram bot is running. This is normal during development with nodemon'); logger.warn(
'Another instance of Telegram bot is running. This is normal during development with nodemon'
);
// Просто логируем ошибку и продолжаем работу // Просто логируем ошибку и продолжаем работу
// Бот будет запущен при следующем перезапуске // Бот будет запущен при следующем перезапуске
} else { } else {
logger.error('Error launching Telegram bot:', error); logger.error('Error launching Telegram bot:', error);
} }
} }
console.log('Все сервисы успешно инициализированы'); console.log('Все сервисы успешно инициализированы');
} catch (error) { } catch (error) {
console.error('Ошибка при инициализации сервисов:', error); console.error('Ошибка при инициализации сервисов:', error);
@@ -61,20 +63,22 @@ async function initServices() {
} }
// Настройка сессий // Настройка сессий
app.use(session({ app.use(
store: new pgSession({ session({
pool: pool, store: new pgSession({
tableName: 'session' pool: pool,
}), tableName: 'session',
secret: process.env.SESSION_SECRET || 'hb3atoken', }),
resave: false, secret: process.env.SESSION_SECRET || 'hb3atoken',
saveUninitialized: true, resave: false,
cookie: { saveUninitialized: true,
secure: process.env.NODE_ENV === 'production', cookie: {
httpOnly: true, secure: process.env.NODE_ENV === 'production',
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 дней httpOnly: true,
} maxAge: 30 * 24 * 60 * 60 * 1000, // 30 дней
})); },
})
);
// Маршруты API // Маршруты API
app.use('/api/users', usersRouter); app.use('/api/users', usersRouter);

View File

@@ -12,9 +12,10 @@ class AIAssistant {
// Создание экземпляра ChatOllama с нужными параметрами // Создание экземпляра ChatOllama с нужными параметрами
createChat(language = 'ru') { createChat(language = 'ru') {
const systemPrompt = language === 'ru' const systemPrompt =
? 'Вы - полезный ассистент. Отвечайте на русском языке.' language === 'ru'
: 'You are a helpful assistant. Respond in English.'; ? 'Вы - полезный ассистент. Отвечайте на русском языке.'
: 'You are a helpful assistant. Respond in English.';
return new ChatOllama({ return new ChatOllama({
baseUrl: this.baseUrl, baseUrl: this.baseUrl,
@@ -22,7 +23,7 @@ class AIAssistant {
system: systemPrompt, system: systemPrompt,
temperature: 0.7, temperature: 0.7,
maxTokens: 1000, maxTokens: 1000,
timeout: 30000 // 30 секунд таймаут timeout: 30000, // 30 секунд таймаут
}); });
} }
@@ -36,14 +37,12 @@ class AIAssistant {
async getResponse(message, language = 'auto') { async getResponse(message, language = 'auto') {
try { try {
console.log('getResponse called with:', { message, language }); console.log('getResponse called with:', { message, language });
// Определяем язык, если не указан явно // Определяем язык, если не указан явно
const detectedLanguage = language === 'auto' const detectedLanguage = language === 'auto' ? this.detectLanguage(message) : language;
? this.detectLanguage(message)
: language;
console.log('Detected language:', detectedLanguage); console.log('Detected language:', detectedLanguage);
// Сначала пробуем прямой API запрос // Сначала пробуем прямой API запрос
try { try {
console.log('Trying direct API request...'); console.log('Trying direct API request...');
@@ -67,7 +66,7 @@ class AIAssistant {
} }
} catch (error) { } catch (error) {
console.error('Error in getResponse:', error); console.error('Error in getResponse:', error);
return "Извините, я не смог обработать ваш запрос. Пожалуйста, попробуйте позже."; return 'Извините, я не смог обработать ваш запрос. Пожалуйста, попробуйте позже.';
} }
} }
@@ -75,10 +74,11 @@ class AIAssistant {
async fallbackRequest(message, language) { async fallbackRequest(message, language) {
try { try {
console.log('Using fallback request method with:', { message, language }); console.log('Using fallback request method with:', { message, language });
const systemPrompt = language === 'ru' const systemPrompt =
? 'Вы - полезный ассистент. Отвечайте на русском языке.' language === 'ru'
: 'You are a helpful assistant. Respond in English.'; ? 'Вы - полезный ассистент. Отвечайте на русском языке.'
: 'You are a helpful assistant. Respond in English.';
console.log('Sending request to Ollama API...'); console.log('Sending request to Ollama API...');
const response = await fetch(`${this.baseUrl}/api/generate`, { const response = await fetch(`${this.baseUrl}/api/generate`, {
@@ -91,15 +91,15 @@ class AIAssistant {
stream: false, stream: false,
options: { options: {
temperature: 0.7, temperature: 0.7,
num_predict: 1000 num_predict: 1000,
} },
}), }),
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
const data = await response.json(); const data = await response.json();
console.log('Ollama API response:', data); console.log('Ollama API response:', data);
return data.response; return data.response;

View File

@@ -6,15 +6,13 @@ const { processMessage } = require('./ai-assistant'); // Используем AI
const verificationService = require('./verification-service'); // Используем сервис верификации const verificationService = require('./verification-service'); // Используем сервис верификации
const ADMIN_CONTRACTS = [ const ADMIN_CONTRACTS = [
{ address: "0xd95a45fc46a7300e6022885afec3d618d7d3f27c", network: "eth" }, { address: '0xd95a45fc46a7300e6022885afec3d618d7d3f27c', network: 'eth' },
{ address: "0x4B294265720B09ca39BFBA18c7E368413c0f68eB", network: "bsc" }, { address: '0x4B294265720B09ca39BFBA18c7E368413c0f68eB', network: 'bsc' },
{ address: "0xdce769b847a0a697239777d0b1c7dd33b6012ba0", network: "arbitrum" }, { address: '0xdce769b847a0a697239777d0b1c7dd33b6012ba0', network: 'arbitrum' },
{ address: "0x351f59de4fedbdf7601f5592b93db3b9330c1c1d", network: "polygon" } { address: '0x351f59de4fedbdf7601f5592b93db3b9330c1c1d', network: 'polygon' },
]; ];
const ERC20_ABI = [ const ERC20_ABI = ['function balanceOf(address owner) view returns (uint256)'];
"function balanceOf(address owner) view returns (uint256)"
];
class AuthService { class AuthService {
constructor() { constructor() {
@@ -22,7 +20,7 @@ class AuthService {
eth: new ethers.JsonRpcProvider(process.env.RPC_URL_ETH), eth: new ethers.JsonRpcProvider(process.env.RPC_URL_ETH),
polygon: new ethers.JsonRpcProvider(process.env.RPC_URL_POLYGON), polygon: new ethers.JsonRpcProvider(process.env.RPC_URL_POLYGON),
bsc: new ethers.JsonRpcProvider(process.env.RPC_URL_BSC), bsc: new ethers.JsonRpcProvider(process.env.RPC_URL_BSC),
arbitrum: new ethers.JsonRpcProvider(process.env.RPC_URL_ARBITRUM) arbitrum: new ethers.JsonRpcProvider(process.env.RPC_URL_ARBITRUM),
}; };
} }
@@ -30,13 +28,13 @@ class AuthService {
async verifySignature(message, signature, address) { async verifySignature(message, signature, address) {
try { try {
if (!message || !signature || !address) return false; if (!message || !signature || !address) return false;
// Нормализуем входящий адрес // Нормализуем входящий адрес
const normalizedAddress = ethers.getAddress(address).toLowerCase(); const normalizedAddress = ethers.getAddress(address).toLowerCase();
// Восстанавливаем адрес из подписи // Восстанавливаем адрес из подписи
const recoveredAddress = ethers.verifyMessage(message, signature); const recoveredAddress = ethers.verifyMessage(message, signature);
// Сравниваем нормализованные адреса // Сравниваем нормализованные адреса
return ethers.getAddress(recoveredAddress).toLowerCase() === normalizedAddress; return ethers.getAddress(recoveredAddress).toLowerCase() === normalizedAddress;
} catch (error) { } catch (error) {
@@ -54,20 +52,23 @@ class AuthService {
try { try {
// Нормализуем адрес - всегда приводим к нижнему регистру // Нормализуем адрес - всегда приводим к нижнему регистру
const normalizedAddress = ethers.getAddress(address).toLowerCase(); const normalizedAddress = ethers.getAddress(address).toLowerCase();
// Ищем пользователя по адресу в таблице user_identities // Ищем пользователя по адресу в таблице user_identities
const userResult = await db.query(` const userResult = await db.query(
`
SELECT u.* FROM users u SELECT u.* 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 = 'wallet' AND ui.provider_id = $1 WHERE ui.provider = 'wallet' AND ui.provider_id = $1
`, [normalizedAddress]); `,
[normalizedAddress]
);
if (userResult.rows.length > 0) { if (userResult.rows.length > 0) {
const user = userResult.rows[0]; const user = userResult.rows[0];
// Проверяем роль администратора при каждой аутентификации // Проверяем роль администратора при каждой аутентификации
const isAdmin = await this.checkAdminRole(normalizedAddress); const isAdmin = await this.checkAdminRole(normalizedAddress);
// Если статус админа изменился, обновляем роль в базе данных // Если статус админа изменился, обновляем роль в базе данных
if (user.role === 'admin' && !isAdmin) { if (user.role === 'admin' && !isAdmin) {
await db.query('UPDATE users SET role = $1 WHERE id = $2', ['user', user.id]); await db.query('UPDATE users SET role = $1 WHERE id = $2', ['user', user.id]);
@@ -78,37 +79,38 @@ class AuthService {
logger.info(`Updated user ${user.id} role to admin (admin tokens found)`); logger.info(`Updated user ${user.id} role to admin (admin tokens found)`);
return { userId: user.id, isAdmin: true }; return { userId: user.id, isAdmin: true };
} }
return { return {
userId: user.id, userId: user.id,
isAdmin: user.role === 'admin' isAdmin: user.role === 'admin',
}; };
} }
// Если пользователь не найден, создаем нового // Если пользователь не найден, создаем нового
const newUserResult = await db.query( const newUserResult = await db.query('INSERT INTO users (role) VALUES ($1) RETURNING id', [
'INSERT INTO users (role) VALUES ($1) RETURNING id', 'user',
['user'] ]);
);
const userId = newUserResult.rows[0].id; const userId = newUserResult.rows[0].id;
// Добавляем идентификатор кошелька (всегда в нижнем регистре) // Добавляем идентификатор кошелька (всегда в нижнем регистре)
await db.query( await db.query(
'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, 'wallet', normalizedAddress] [userId, 'wallet', normalizedAddress]
); );
// Проверяем, есть ли у пользователя роль админа // Проверяем, есть ли у пользователя роль админа
const isAdmin = await this.checkAdminRole(normalizedAddress); const isAdmin = await this.checkAdminRole(normalizedAddress);
logger.info(`New user ${userId} role check result: ${isAdmin ? 'admin' : 'user'}`); logger.info(`New user ${userId} role check result: ${isAdmin ? 'admin' : 'user'}`);
// Если у пользователя есть админские токены, обновляем его роль // Если у пользователя есть админские токены, обновляем его роль
if (isAdmin) { if (isAdmin) {
await db.query('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]); await db.query('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]);
logger.info(`New user ${userId} with wallet ${normalizedAddress} automatically granted admin role`); logger.info(
`New user ${userId} with wallet ${normalizedAddress} automatically granted admin role`
);
} }
return { userId, isAdmin }; return { userId, isAdmin };
} catch (error) { } catch (error) {
logger.error('Error finding or creating user:', error); logger.error('Error finding or creating user:', error);
@@ -123,13 +125,13 @@ class AuthService {
*/ */
async checkAdminRole(address) { async checkAdminRole(address) {
if (!address) return false; if (!address) return false;
logger.info(`Checking admin role for address: ${address}`); logger.info(`Checking admin role for address: ${address}`);
let foundTokens = false; let foundTokens = false;
let errorCount = 0; let errorCount = 0;
const balances = {}; const balances = {};
const totalNetworks = ADMIN_CONTRACTS.length; const totalNetworks = ADMIN_CONTRACTS.length;
// Создаем массив промисов для параллельной проверки балансов // Создаем массив промисов для параллельной проверки балансов
const checkPromises = ADMIN_CONTRACTS.map(async (contract) => { const checkPromises = ADMIN_CONTRACTS.map(async (contract) => {
try { try {
@@ -140,83 +142,83 @@ class AuthService {
errorCount++; errorCount++;
return null; return null;
} }
// Проверяем доступность провайдера // Проверяем доступность провайдера
try { try {
// Проверка доступности сети с таймаутом // Проверка доступности сети с таймаутом
const networkCheckPromise = provider.getNetwork(); const networkCheckPromise = provider.getNetwork();
const timeoutPromise = new Promise((_, reject) => const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Network check timeout')), 3000) setTimeout(() => reject(new Error('Network check timeout')), 3000)
); );
await Promise.race([networkCheckPromise, timeoutPromise]); await Promise.race([networkCheckPromise, timeoutPromise]);
} catch (networkError) { } catch (networkError) {
logger.error(`Provider for ${contract.network} is not available: ${networkError.message}`); logger.error(
`Provider for ${contract.network} is not available: ${networkError.message}`
);
balances[contract.network] = 'Error: Network unavailable'; balances[contract.network] = 'Error: Network unavailable';
errorCount++; errorCount++;
return null; return null;
} }
const tokenContract = new ethers.Contract( const tokenContract = new ethers.Contract(contract.address, ERC20_ABI, provider);
contract.address,
ERC20_ABI,
provider
);
// Создаем промис с таймаутом // Создаем промис с таймаутом
const balancePromise = tokenContract.balanceOf(address); const balancePromise = tokenContract.balanceOf(address);
const timeoutPromise = new Promise((_, reject) => const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 3000) setTimeout(() => reject(new Error('Timeout')), 3000)
); );
// Ждем первый выполненный промис // Ждем первый выполненный промис
const balance = await Promise.race([balancePromise, timeoutPromise]); const balance = await Promise.race([balancePromise, timeoutPromise]);
const formattedBalance = ethers.formatUnits(balance, 18); const formattedBalance = ethers.formatUnits(balance, 18);
balances[contract.network] = formattedBalance; balances[contract.network] = formattedBalance;
logger.info(`Token balance on ${contract.network}:`, { logger.info(`Token balance on ${contract.network}:`, {
address, address,
contract: contract.address, contract: contract.address,
balance: formattedBalance, balance: formattedBalance,
hasTokens: balance > 0 hasTokens: balance > 0,
}); });
if (parseFloat(formattedBalance) > 0) { if (parseFloat(formattedBalance) > 0) {
logger.info(`Found admin tokens on ${contract.network}`); logger.info(`Found admin tokens on ${contract.network}`);
foundTokens = true; foundTokens = true;
} }
return { network: contract.network, balance: formattedBalance }; return { network: contract.network, balance: formattedBalance };
} catch (error) { } catch (error) {
logger.error(`Error checking balance in ${contract.network}:`, { logger.error(`Error checking balance in ${contract.network}:`, {
address, address,
contract: contract.address, contract: contract.address,
error: error.message || 'Unknown error' error: error.message || 'Unknown error',
}); });
balances[contract.network] = 'Error'; balances[contract.network] = 'Error';
errorCount++; errorCount++;
return null; return null;
} }
}); });
// Ждем выполнения всех проверок // Ждем выполнения всех проверок
await Promise.all(checkPromises); await Promise.all(checkPromises);
// Если все запросы завершились с ошибкой, считаем, что проверка не удалась // Если все запросы завершились с ошибкой, считаем, что проверка не удалась
if (errorCount === totalNetworks) { if (errorCount === totalNetworks) {
logger.error(`All network checks for ${address} failed. Cannot verify admin status.`); logger.error(`All network checks for ${address} failed. Cannot verify admin status.`);
return false; return false;
} }
if (foundTokens) { if (foundTokens) {
logger.info(`Admin role summary for ${address}:`, { logger.info(`Admin role summary for ${address}:`, {
networks: Object.keys(balances).filter(net => balances[net] > 0 && balances[net] !== 'Error'), networks: Object.keys(balances).filter(
balances (net) => balances[net] > 0 && balances[net] !== 'Error'
),
balances,
}); });
logger.info(`Admin role granted for ${address}`); logger.info(`Admin role granted for ${address}`);
return true; return true;
} }
logger.info(`Admin role denied - no tokens found for ${address}`); logger.info(`Admin role denied - no tokens found for ${address}`);
return false; return false;
} }
@@ -233,13 +235,13 @@ class AuthService {
eth: '0', eth: '0',
bsc: '0', bsc: '0',
arbitrum: '0', arbitrum: '0',
polygon: '0' polygon: '0',
}; };
} }
const balances = {}; const balances = {};
const timeout = 3000; // 3 секунды таймаут const timeout = 3000; // 3 секунды таймаут
for (const contract of ADMIN_CONTRACTS) { for (const contract of ADMIN_CONTRACTS) {
try { try {
const provider = this.providers[contract.network]; const provider = this.providers[contract.network];
@@ -253,37 +255,35 @@ class AuthService {
try { try {
// Проверка доступности сети с таймаутом // Проверка доступности сети с таймаутом
const networkCheckPromise = provider.getNetwork(); const networkCheckPromise = provider.getNetwork();
const networkTimeoutPromise = new Promise((_, reject) => const networkTimeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Network check timeout')), timeout) setTimeout(() => reject(new Error('Network check timeout')), timeout)
); );
await Promise.race([networkCheckPromise, networkTimeoutPromise]); await Promise.race([networkCheckPromise, networkTimeoutPromise]);
} catch (networkError) { } catch (networkError) {
logger.error(`Provider for ${contract.network} is not available: ${networkError.message}`); logger.error(
`Provider for ${contract.network} is not available: ${networkError.message}`
);
balances[contract.network] = '0'; balances[contract.network] = '0';
continue; continue;
} }
const tokenContract = new ethers.Contract( const tokenContract = new ethers.Contract(contract.address, ERC20_ABI, provider);
contract.address,
ERC20_ABI,
provider
);
// Создаем промис с таймаутом // Создаем промис с таймаутом
const balancePromise = tokenContract.balanceOf(address); const balancePromise = tokenContract.balanceOf(address);
const timeoutPromise = new Promise((_, reject) => const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout) setTimeout(() => reject(new Error('Timeout')), timeout)
); );
// Ждем первый выполненный промис // Ждем первый выполненный промис
const balance = await Promise.race([balancePromise, timeoutPromise]); const balance = await Promise.race([balancePromise, timeoutPromise]);
const formattedBalance = ethers.formatUnits(balance, 18); const formattedBalance = ethers.formatUnits(balance, 18);
logger.info(`Token balance for ${address} on ${contract.network}:`, { logger.info(`Token balance for ${address} on ${contract.network}:`, {
contract: contract.address, contract: contract.address,
balance: formattedBalance, balance: formattedBalance,
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}); });
balances[contract.network] = formattedBalance; balances[contract.network] = formattedBalance;
@@ -292,17 +292,17 @@ class AuthService {
address, address,
contract: contract.address, contract: contract.address,
error: error.message || 'Unknown error', error: error.message || 'Unknown error',
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}); });
balances[contract.network] = '0'; balances[contract.network] = '0';
} }
} }
logger.info(`Token balances fetched for ${address}:`, { logger.info(`Token balances fetched for ${address}:`, {
...balances, ...balances,
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}); });
return balances; return balances;
} }
@@ -318,26 +318,29 @@ class AuthService {
session.userId = userId; session.userId = userId;
session.authenticated = authenticated; session.authenticated = authenticated;
session.authType = authType; session.authType = authType;
// Сохраняем адрес кошелька если есть // Сохраняем адрес кошелька если есть
if (address) { if (address) {
session.address = address; session.address = address;
} }
// Сохраняем сессию в БД // Сохраняем сессию в БД
const result = await db.query( const result = await db.query(
`UPDATE session `UPDATE session
SET sess = $1 SET sess = $1
WHERE sid = $2`, WHERE sid = $2`,
[JSON.stringify({ [
userId, JSON.stringify({
authenticated, userId,
authType, authenticated,
address, authType,
cookie: session.cookie address,
}), session.id] cookie: session.cookie,
}),
session.id,
]
); );
return true; return true;
} catch (error) { } catch (error) {
logger.error('Error creating session:', error); logger.error('Error creating session:', error);
@@ -400,17 +403,19 @@ class AuthService {
try { try {
// Проверяем наличие связанного кошелька // Проверяем наличие связанного кошелька
const wallet = await this.getLinkedWallet(userId); const wallet = await this.getLinkedWallet(userId);
// Если кошелек не привязан, пользователь получает роль user // Если кошелек не привязан, пользователь получает роль user
// с базовым доступом к чату и истории сообщений // с базовым доступом к чату и истории сообщений
if (!wallet) { if (!wallet) {
logger.info(`No wallet linked for user ${userId}, assigning basic user role`); logger.info(`No wallet linked for user ${userId}, assigning basic user role`);
return 'user'; return 'user';
} }
// Если есть кошелек, проверяем админские токены // Если есть кошелек, проверяем админские токены
const isAdmin = await this.checkAdminRole(wallet); const isAdmin = await this.checkAdminRole(wallet);
logger.info(`Role check for user ${userId} with wallet ${wallet}: ${isAdmin ? 'admin' : 'user'}`); logger.info(
`Role check for user ${userId} with wallet ${wallet}: ${isAdmin ? 'admin' : 'user'}`
);
return isAdmin ? 'admin' : 'user'; return isAdmin ? 'admin' : 'user';
} catch (error) { } catch (error) {
logger.error('Error checking user role:', error); logger.error('Error checking user role:', error);
@@ -423,20 +428,17 @@ class AuthService {
try { try {
// Проверяем код через сервис верификации // Проверяем код через сервис верификации
const result = await verificationService.verifyCode(code, 'email', null); const result = await verificationService.verifyCode(code, 'email', null);
if (!result.success) { if (!result.success) {
return { verified: false }; return { verified: false };
} }
const userId = result.userId; const userId = result.userId;
const email = result.providerId; const email = result.providerId;
// Проверяем, существует ли пользователь с таким email // Проверяем, существует ли пользователь с таким email
const userResult = await db.query( const userResult = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
'SELECT * FROM users WHERE id = $1',
[userId]
);
if (userResult.rows.length === 0) { if (userResult.rows.length === 0) {
return { verified: false }; return { verified: false };
} }
@@ -444,7 +446,7 @@ class AuthService {
// Проверяем наличие кошелька и определяем роль // Проверяем наличие кошелька и определяем роль
const wallet = await this.getLinkedWallet(userId); const wallet = await this.getLinkedWallet(userId);
let role = 'user'; // Базовая роль для доступа к чату let role = 'user'; // Базовая роль для доступа к чату
if (wallet) { if (wallet) {
// Если есть кошелек, проверяем баланс токенов // Если есть кошелек, проверяем баланс токенов
const isAdmin = await this.checkAdminRole(wallet); const isAdmin = await this.checkAdminRole(wallet);
@@ -453,13 +455,13 @@ class AuthService {
} else { } else {
logger.info(`User ${userId} has no wallet, using basic user role`); logger.info(`User ${userId} has no wallet, using basic user role`);
} }
return { return {
verified: true, verified: true,
userId, userId,
email, email,
role, role,
wallet: wallet || null wallet: wallet || null,
}; };
} catch (error) { } catch (error) {
logger.error('Error checking email verification:', error); logger.error('Error checking email verification:', error);
@@ -473,28 +475,30 @@ class AuthService {
async verifyTelegramAuth(telegramId, verificationCode, session) { async verifyTelegramAuth(telegramId, verificationCode, session) {
try { try {
logger.info(`[verifyTelegramAuth] Starting for telegramId: ${telegramId}`); logger.info(`[verifyTelegramAuth] Starting for telegramId: ${telegramId}`);
let userId; let userId;
let isNewUser = false; let isNewUser = false;
// Проверяем наличие аутентифицированного пользователя в сессии // Проверяем наличие аутентифицированного пользователя в сессии
if (session && session.authenticated && session.userId) { if (session && session.authenticated && session.userId) {
// Если есть авторизованный пользователь в сессии, связываем Telegram с ним // Если есть авторизованный пользователь в сессии, связываем Telegram с ним
userId = session.userId; userId = session.userId;
logger.info(`[verifyTelegramAuth] Using existing authenticated user ${userId} from session`); logger.info(
`[verifyTelegramAuth] Using existing authenticated user ${userId} from session`
);
// Связываем Telegram с текущим пользователем // Связываем Telegram с текущим пользователем
await this.linkIdentity(userId, 'telegram', telegramId); await this.linkIdentity(userId, 'telegram', telegramId);
return { return {
success: true, success: true,
userId, userId,
role: session.isAdmin ? 'admin' : 'user', role: session.isAdmin ? 'admin' : 'user',
telegramId, telegramId,
isNewUser: false isNewUser: false,
}; };
} }
// Если в сессии нет авторизованного пользователя, проверяем существующие идентификаторы // Если в сессии нет авторизованного пользователя, проверяем существующие идентификаторы
// Проверяем, существует ли уже пользователь с таким Telegram ID // Проверяем, существует ли уже пользователь с таким Telegram ID
const existingUserResult = await db.query( const existingUserResult = await db.query(
@@ -509,23 +513,26 @@ class AuthService {
if (existingUserResult.rows.length > 0) { if (existingUserResult.rows.length > 0) {
const existingUser = existingUserResult.rows[0]; const existingUser = existingUserResult.rows[0];
userId = existingUser.id; userId = existingUser.id;
logger.info(`[verifyTelegramAuth] Found existing user ${userId} for Telegram ID ${telegramId}`); logger.info(
`[verifyTelegramAuth] Found existing user ${userId} for Telegram ID ${telegramId}`
);
} else { } else {
// Создаем нового пользователя для нового telegramId // Создаем нового пользователя для нового telegramId
const newUserResult = await db.query( const newUserResult = await db.query('INSERT INTO users (role) VALUES ($1) RETURNING id', [
'INSERT INTO users (role) VALUES ($1) RETURNING id', 'user',
['user'] ]);
);
userId = newUserResult.rows[0].id; userId = newUserResult.rows[0].id;
isNewUser = true; isNewUser = true;
// Добавляем Telegram идентификатор // Добавляем Telegram идентификатор
await db.query( await db.query(
'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, 'telegram', telegramId] [userId, 'telegram', telegramId]
); );
logger.info(`[verifyTelegramAuth] Created new user ${userId} for Telegram ID ${telegramId}`); logger.info(
`[verifyTelegramAuth] Created new user ${userId} for Telegram ID ${telegramId}`
);
} }
// Если есть гостевой ID в сессии, сохраняем его для нового пользователя // Если есть гостевой ID в сессии, сохраняем его для нового пользователя
@@ -542,7 +549,7 @@ class AuthService {
userId, userId,
role: 'user', role: 'user',
telegramId, telegramId,
isNewUser isNewUser,
}; };
} catch (error) { } catch (error) {
logger.error('[verifyTelegramAuth] Error:', error); logger.error('[verifyTelegramAuth] Error:', error);
@@ -553,30 +560,28 @@ class AuthService {
// Добавляем псевдоним функции checkAdminRole для обратной совместимости // Добавляем псевдоним функции checkAdminRole для обратной совместимости
async checkAdminTokens(address) { async checkAdminTokens(address) {
if (!address) return false; if (!address) return false;
logger.info(`Checking admin tokens for address: ${address}`); logger.info(`Checking admin tokens for address: ${address}`);
try { try {
const isAdmin = await this.checkAdminRole(address); const isAdmin = await this.checkAdminRole(address);
// Обновляем роль пользователя в базе данных, если есть админские токены // Обновляем роль пользователя в базе данных, если есть админские токены
if (isAdmin) { if (isAdmin) {
try { try {
// Находим userId по адресу // Находим userId по адресу
const userResult = await db.query(` const userResult = await db.query(
`
SELECT u.id FROM users u SELECT u.id 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 = 'wallet' AND ui.provider_id = $1`, WHERE ui.provider = 'wallet' AND ui.provider_id = $1`,
[address.toLowerCase()] [address.toLowerCase()]
); );
if (userResult.rows.length > 0) { if (userResult.rows.length > 0) {
const userId = userResult.rows[0].id; const userId = userResult.rows[0].id;
// Обновляем роль пользователя // Обновляем роль пользователя
await db.query( await db.query('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]);
'UPDATE users SET role = $1 WHERE id = $2',
['admin', userId]
);
logger.info(`Updated user ${userId} role to admin based on token holdings`); logger.info(`Updated user ${userId} role to admin based on token holdings`);
} }
} catch (error) { } catch (error) {
@@ -586,26 +591,24 @@ class AuthService {
} else { } else {
// Если пользователь не является администратором, сбрасываем роль на "user", если она была "admin" // Если пользователь не является администратором, сбрасываем роль на "user", если она была "admin"
try { try {
const userResult = await db.query(` const userResult = await db.query(
`
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 = 'wallet' AND ui.provider_id = $1`, WHERE ui.provider = 'wallet' AND ui.provider_id = $1`,
[address.toLowerCase()] [address.toLowerCase()]
); );
if (userResult.rows.length > 0 && userResult.rows[0].role === 'admin') { if (userResult.rows.length > 0 && userResult.rows[0].role === 'admin') {
const userId = userResult.rows[0].id; const userId = userResult.rows[0].id;
await db.query( await db.query('UPDATE users SET role = $1 WHERE id = $2', ['user', userId]);
'UPDATE users SET role = $1 WHERE id = $2',
['user', userId]
);
logger.info(`Reset user ${userId} role from admin to user (no tokens found)`); logger.info(`Reset user ${userId} role from admin to user (no tokens found)`);
} }
} catch (error) { } catch (error) {
logger.error('Error updating user role:', error); logger.error('Error updating user role:', error);
} }
} }
return isAdmin; return isAdmin;
} catch (error) { } catch (error) {
logger.error(`Error in checkAdminTokens: ${error.message}`); logger.error(`Error in checkAdminTokens: ${error.message}`);
@@ -622,24 +625,21 @@ class AuthService {
try { try {
// Получаем все идентификаторы пользователя // Получаем все идентификаторы пользователя
const identities = await this.getUserIdentities(userId); const identities = await this.getUserIdentities(userId);
// Фильтруем только гостевые идентификаторы // Фильтруем только гостевые идентификаторы
const guestIdentities = identities.filter(id => id.identity_type === 'guest'); const guestIdentities = identities.filter((id) => id.identity_type === 'guest');
// Если гостевых идентификаторов больше 3, удаляем старые // Если гостевых идентификаторов больше 3, удаляем старые
if (guestIdentities.length > 3) { if (guestIdentities.length > 3) {
// Сортируем по дате создания (новые первые) // Сортируем по дате создания (новые первые)
guestIdentities.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); guestIdentities.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
// Оставляем только 3 последних идентификатора // Оставляем только 3 последних идентификатора
const identitiesToDelete = guestIdentities.slice(3); const identitiesToDelete = guestIdentities.slice(3);
// Удаляем старые идентификаторы // Удаляем старые идентификаторы
for (const identity of identitiesToDelete) { for (const identity of identitiesToDelete) {
await db.query( await db.query('DELETE FROM user_identities WHERE id = $1', [identity.id]);
'DELETE FROM user_identities WHERE id = $1',
[identity.id]
);
logger.info(`Deleted old guest identity: ${identity.identity_value}`); logger.info(`Deleted old guest identity: ${identity.identity_value}`);
} }
} }
@@ -676,9 +676,7 @@ class AuthService {
try { try {
const balance = await Promise.race([ const balance = await Promise.race([
this.getTokenBalance(address, ADMIN_CONTRACTS.ARBITRUM), this.getTokenBalance(address, ADMIN_CONTRACTS.ARBITRUM),
new Promise((_, reject) => new Promise((_, reject) => setTimeout(() => reject(new Error('TIMEOUT')), timeout)),
setTimeout(() => reject(new Error('TIMEOUT')), timeout)
)
]); ]);
return { balance, hasTokens: balance > 0 }; return { balance, hasTokens: balance > 0 };
} catch (error) { } catch (error) {
@@ -697,10 +695,12 @@ class AuthService {
async linkIdentity(userId, provider, providerId) { async linkIdentity(userId, provider, providerId) {
try { try {
if (!userId || !provider || !providerId) { if (!userId || !provider || !providerId) {
logger.warn(`[AuthService] Missing parameters for linkIdentity: userId=${userId}, provider=${provider}, providerId=${providerId}`); logger.warn(
`[AuthService] Missing parameters for linkIdentity: userId=${userId}, provider=${provider}, providerId=${providerId}`
);
throw new Error('Missing parameters'); throw new Error('Missing parameters');
} }
// Нормализуем значение идентификатора // Нормализуем значение идентификатора
let normalizedProviderId = providerId; let normalizedProviderId = providerId;
if (provider === 'wallet') { if (provider === 'wallet') {
@@ -714,113 +714,63 @@ class AuthService {
} else if (provider === 'email') { } else if (provider === 'email') {
normalizedProviderId = providerId.toLowerCase(); normalizedProviderId = providerId.toLowerCase();
} }
logger.info(`[AuthService] Linking identity ${provider}:${normalizedProviderId} to user ${userId}`); logger.info(
`[AuthService] Linking identity ${provider}:${normalizedProviderId} to user ${userId}`
);
// Проверяем, существует ли уже такой идентификатор // Проверяем, существует ли уже такой идентификатор
const existingResult = await db.query( const existingResult = await db.query(
`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`,
[provider, normalizedProviderId] [provider, normalizedProviderId]
); );
if (existingResult.rows.length > 0) { if (existingResult.rows.length > 0) {
const existingUserId = existingResult.rows[0].user_id; const existingUserId = existingResult.rows[0].user_id;
// Если идентификатор уже принадлежит этому пользователю, ничего не делаем // Если идентификатор уже принадлежит этому пользователю, ничего не делаем
if (existingUserId === userId) { if (existingUserId === userId) {
logger.info(`[AuthService] Identity ${provider}:${normalizedProviderId} already exists for user ${userId}`); logger.info(
`[AuthService] Identity ${provider}:${normalizedProviderId} already exists for user ${userId}`
);
return { success: true, message: 'Identity already exists' }; return { success: true, message: 'Identity already exists' };
} else { } else {
// Если идентификатор принадлежит другому пользователю, возвращаем ошибку // Если идентификатор принадлежит другому пользователю, возвращаем ошибку
logger.warn(`[AuthService] Identity ${provider}:${normalizedProviderId} already belongs to user ${existingUserId}, not user ${userId}`); logger.warn(
`[AuthService] Identity ${provider}:${normalizedProviderId} already belongs to user ${existingUserId}, not user ${userId}`
);
throw new Error(`Identity already belongs to another user (${existingUserId})`); throw new Error(`Identity already belongs to another user (${existingUserId})`);
} }
} }
// Добавляем новый идентификатор для пользователя // Добавляем новый идентификатор для пользователя
await db.query( await db.query(
`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, provider, normalizedProviderId] [userId, provider, normalizedProviderId]
); );
// Проверяем и обновляем роль администратора, если это идентификатор кошелька // Проверяем и обновляем роль администратора, если это идентификатор кошелька
let isAdmin = false; let isAdmin = false;
if (provider === 'wallet') { if (provider === 'wallet') {
isAdmin = await this.checkAdminTokens(normalizedProviderId); isAdmin = await this.checkAdminTokens(normalizedProviderId);
// Обновляем роль пользователя в базе данных, если нужно // Обновляем роль пользователя в базе данных, если нужно
if (isAdmin) { if (isAdmin) {
await db.query( await db.query('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]);
'UPDATE users SET role = $1 WHERE id = $2',
['admin', userId]
);
logger.info(`[AuthService] Updated user ${userId} role to admin based on token holdings`); logger.info(`[AuthService] Updated user ${userId} role to admin based on token holdings`);
} }
} }
logger.info(`[AuthService] Identity ${provider}:${normalizedProviderId} successfully linked to user ${userId}`); logger.info(
`[AuthService] Identity ${provider}:${normalizedProviderId} successfully linked to user ${userId}`
);
return { success: true, isAdmin }; return { success: true, isAdmin };
} catch (error) { } catch (error) {
logger.error(`[AuthService] Error linking identity ${provider}:${providerId} to user ${userId}:`, error); logger.error(
throw error; `[AuthService] Error linking identity ${provider}:${providerId} to user ${userId}:`,
} error
}
/**
* Обработка гостевых сообщений после аутентификации
* ПРИМЕЧАНИЕ: Эта функция оставлена для обратной совместимости.
* Фактически все маршруты теперь используют версию функции из auth.js,
* которая корректно обрабатывает сообщения для всех типов аутентификации.
* @deprecated Используйте функцию linkGuestMessagesAfterAuth из routes/auth.js
*/
async linkGuestMessagesAfterAuth(userId, currentGuestId, previousGuestId) {
try {
logger.info(`[linkGuestMessagesAfterAuth] Starting for user ${userId} with guestId=${currentGuestId}`);
// Проверяем, есть ли идентификатор для обработки
if (!currentGuestId) {
logger.debug('[linkGuestMessagesAfterAuth] No guest ID to process');
return { success: true, message: 'No guest ID to process' };
}
// Проверяем, не привязаны ли уже эти гостевые сообщения к другому пользователю
const existingMessagesCheck = await db.query(
`SELECT DISTINCT user_id
FROM messages
WHERE guest_message_id IN (
SELECT id FROM guest_messages WHERE guest_id = $1
)`,
[currentGuestId]
); );
if (existingMessagesCheck.rows.length > 0) {
const existingUserId = existingMessagesCheck.rows[0].user_id;
if (existingUserId !== userId) {
logger.warn(`[linkGuestMessagesAfterAuth] Guest messages for ${currentGuestId} are already linked to user ${existingUserId}`);
return {
success: false,
error: 'Guest messages are already linked to another user'
};
}
}
// Используем ту же функцию processGuestMessages что и в auth.js
const result = await processGuestMessages(userId, currentGuestId);
logger.info(`[linkGuestMessagesAfterAuth] Guest messages processed: ${JSON.stringify(result)}`);
// Если есть предыдущий гостевой ID, обработаем и его
if (previousGuestId && previousGuestId !== currentGuestId) {
const prevResult = await processGuestMessages(userId, previousGuestId);
logger.info(`[linkGuestMessagesAfterAuth] Previous guest messages processed: ${JSON.stringify(prevResult)}`);
}
return {
success: true,
result: result
};
} catch (error) {
logger.error('[linkGuestMessagesAfterAuth] Error:', error);
throw error; throw error;
} }
} }
@@ -828,4 +778,4 @@ class AuthService {
// Создаем и экспортируем единственный экземпляр // Создаем и экспортируем единственный экземпляр
const authService = new AuthService(); const authService = new AuthService();
module.exports = authService; module.exports = authService;

View File

@@ -15,7 +15,7 @@ class EmailAuth {
if (!email || !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) { if (!email || !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
throw new Error('Некорректный формат email'); throw new Error('Некорректный формат email');
} }
// Проверяем, существует ли пользователь с таким email // Проверяем, существует ли пользователь с таким email
const existingEmailUser = await db.query( const existingEmailUser = await db.query(
`SELECT u.id FROM users u `SELECT u.id FROM users u
@@ -23,44 +23,47 @@ class EmailAuth {
WHERE i.provider = 'email' AND i.provider_id = $1`, WHERE i.provider = 'email' AND i.provider_id = $1`,
[email.toLowerCase()] [email.toLowerCase()]
); );
// Создаем или получаем ID пользователя // Создаем или получаем ID пользователя
let userId; let userId;
if (session.authenticated && session.userId) { if (session.authenticated && session.userId) {
// Если пользователь уже аутентифицирован, используем его ID // Если пользователь уже аутентифицирован, используем его ID
userId = session.userId; userId = session.userId;
logger.info(`[initEmailAuth] Using existing authenticated user ${userId} for email ${email}`); logger.info(
`[initEmailAuth] Using existing authenticated user ${userId} for email ${email}`
);
} else if (existingEmailUser.rows.length > 0) { } else if (existingEmailUser.rows.length > 0) {
// Если найден пользователь с таким email, используем его ID // Если найден пользователь с таким email, используем его ID
userId = existingEmailUser.rows[0].id; userId = existingEmailUser.rows[0].id;
logger.info(`[initEmailAuth] Found existing user ${userId} with email ${email}`); logger.info(`[initEmailAuth] Found existing user ${userId} with email ${email}`);
} else { } else {
// Создаем временного пользователя, если нужно будет создать нового // Создаем временного пользователя, если нужно будет создать нового
const userResult = await db.query( const userResult = await db.query('INSERT INTO users (role) VALUES ($1) RETURNING id', [
'INSERT INTO users (role) VALUES ($1) RETURNING id', 'user',
['user'] ]);
);
userId = userResult.rows[0].id; userId = userResult.rows[0].id;
session.tempUserId = userId; session.tempUserId = userId;
logger.info(`[initEmailAuth] Created temporary user ${userId} for email ${email}`); logger.info(`[initEmailAuth] Created temporary user ${userId} for email ${email}`);
} }
// Сохраняем email в сессии // Сохраняем email в сессии
session.pendingEmail = email.toLowerCase(); session.pendingEmail = email.toLowerCase();
// Создаем код через сервис верификации // Создаем код через сервис верификации
const verificationCode = await verificationService.createVerificationCode( const verificationCode = await verificationService.createVerificationCode(
'email', 'email',
email.toLowerCase(), email.toLowerCase(),
userId userId
); );
// Отправляем код на email // Отправляем код на email
await this.emailBot.sendVerificationCode(email, verificationCode); await this.emailBot.sendVerificationCode(email, verificationCode);
logger.info(`Generated verification code for Email auth for ${email} and sent to user's email`); logger.info(
`Generated verification code for Email auth for ${email} and sent to user's email`
);
return { success: true, verificationCode }; return { success: true, verificationCode };
} catch (error) { } catch (error) {
logger.error('Error in email auth initialization:', error); logger.error('Error in email auth initialization:', error);
@@ -80,7 +83,7 @@ class EmailAuth {
// Проверяем код через сервис верификации // Проверяем код через сервис верификации
const result = await verificationService.verifyCode(code, 'email', session.pendingEmail); const result = await verificationService.verifyCode(code, 'email', session.pendingEmail);
if (!result.success) { if (!result.success) {
// Используем сообщение об ошибке из сервиса верификации // Используем сообщение об ошибке из сервиса верификации
return { verified: false, message: result.error || 'Неверный код верификации' }; return { verified: false, message: result.error || 'Неверный код верификации' };
@@ -93,28 +96,31 @@ class EmailAuth {
if (session.authenticated && session.userId) { if (session.authenticated && session.userId) {
finalUserId = session.userId; finalUserId = session.userId;
logger.info(`[checkEmailVerification] Using existing authenticated user ${finalUserId}`); logger.info(`[checkEmailVerification] Using existing authenticated user ${finalUserId}`);
// Связываем email с существующим пользователем // Связываем email с существующим пользователем
await authService.linkIdentity(finalUserId, 'email', email); await authService.linkIdentity(finalUserId, 'email', email);
// Очищаем временные данные // Очищаем временные данные
delete session.pendingEmail; delete session.pendingEmail;
return { return {
verified: true, verified: true,
userId: finalUserId, userId: finalUserId,
email: email email: email,
}; };
} }
// Если пользователь не авторизован, ищем всех пользователей с похожими идентификаторами // Если пользователь не авторизован, ищем всех пользователей с похожими идентификаторами
const identities = { const identities = {
email: email, email: email,
guest: session.guestId guest: session.guestId,
}; };
const relatedUsers = await authService.identityService.findRelatedUsers(identities); const relatedUsers = await authService.identityService.findRelatedUsers(identities);
logger.info(`[checkEmailVerification] Found ${relatedUsers.length} related users for identities:`, identities); logger.info(
`[checkEmailVerification] Found ${relatedUsers.length} related users for identities:`,
identities
);
if (relatedUsers.length > 0) { if (relatedUsers.length > 0) {
// Берем первого найденного пользователя как основного // Берем первого найденного пользователя как основного
@@ -124,13 +130,17 @@ class EmailAuth {
// Мигрируем данные от остальных пользователей к основному // Мигрируем данные от остальных пользователей к основному
for (const userId of relatedUsers.slice(1)) { for (const userId of relatedUsers.slice(1)) {
await authService.identityService.migrateUserData(userId, finalUserId); await authService.identityService.migrateUserData(userId, finalUserId);
logger.info(`[checkEmailVerification] Migrated data from user ${userId} to ${finalUserId}`); logger.info(
`[checkEmailVerification] Migrated data from user ${userId} to ${finalUserId}`
);
} }
// Если у нас есть временный пользователь, мигрируем его данные тоже // Если у нас есть временный пользователь, мигрируем его данные тоже
if (session.tempUserId && !relatedUsers.includes(session.tempUserId)) { if (session.tempUserId && !relatedUsers.includes(session.tempUserId)) {
await authService.identityService.migrateUserData(session.tempUserId, finalUserId); await authService.identityService.migrateUserData(session.tempUserId, finalUserId);
logger.info(`[checkEmailVerification] Migrated temporary user ${session.tempUserId} to ${finalUserId}`); logger.info(
`[checkEmailVerification] Migrated temporary user ${session.tempUserId} to ${finalUserId}`
);
} }
} else { } else {
// Если связанных пользователей нет, используем временного или создаем нового // Если связанных пользователей нет, используем временного или создаем нового
@@ -154,7 +164,9 @@ class EmailAuth {
// Если есть гостевой ID, добавляем его тоже // Если есть гостевой ID, добавляем его тоже
if (session.guestId) { if (session.guestId) {
await authService.identityService.saveIdentity(finalUserId, 'guest', session.guestId, true); await authService.identityService.saveIdentity(finalUserId, 'guest', session.guestId, true);
logger.info(`[checkEmailVerification] Added guest identity ${session.guestId} for user ${finalUserId}`); logger.info(
`[checkEmailVerification] Added guest identity ${session.guestId} for user ${finalUserId}`
);
} }
// Очищаем временные данные // Очищаем временные данные
@@ -166,7 +178,7 @@ class EmailAuth {
return { return {
verified: true, verified: true,
userId: finalUserId, userId: finalUserId,
email: email email: email,
}; };
} catch (error) { } catch (error) {
logger.error('Error checking email verification:', error); logger.error('Error checking email verification:', error);
@@ -177,4 +189,4 @@ class EmailAuth {
// Создаем и экспортируем единственный экземпляр // Создаем и экспортируем единственный экземпляр
const emailAuth = new EmailAuth(); const emailAuth = new EmailAuth();
module.exports = emailAuth; module.exports = emailAuth;

View File

@@ -19,8 +19,8 @@ const transporter = nodemailer.createTransport({
maxConnections: 3, maxConnections: 3,
maxMessages: 5, maxMessages: 5,
tls: { tls: {
rejectUnauthorized: false rejectUnauthorized: false,
} },
}); });
// Конфигурация для получения писем // Конфигурация для получения писем
@@ -31,11 +31,11 @@ const imapConfig = {
port: process.env.EMAIL_IMAP_PORT, port: process.env.EMAIL_IMAP_PORT,
tls: true, tls: true,
tlsOptions: { rejectUnauthorized: false }, tlsOptions: { rejectUnauthorized: false },
keepalive: { keepalive: {
interval: 10000, interval: 10000,
idleInterval: 300000, idleInterval: 300000,
forceNoop: true forceNoop: true,
} },
}; };
class EmailBotService { class EmailBotService {
@@ -66,7 +66,7 @@ class EmailBotService {
try { try {
// Отправляем код на email // Отправляем код на email
await this.sendVerificationCode(email, code); await this.sendVerificationCode(email, code);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
logger.error('Error initializing email verification:', error); logger.error('Error initializing email verification:', error);
@@ -91,7 +91,7 @@ class EmailBotService {
</div> </div>
<p style="font-size: 14px; color: #999;">Код действителен в течение 15 минут.</p> <p style="font-size: 14px; color: #999;">Код действителен в течение 15 минут.</p>
</div> </div>
` `,
}; };
await this.transporter.sendMail(mailOptions); await this.transporter.sendMail(mailOptions);
@@ -121,7 +121,7 @@ class EmailBotService {
this.imap.end(); this.imap.end();
return; return;
} }
// Ищем непрочитанные письма // Ищем непрочитанные письма
this.imap.search(['UNSEEN'], (err, results) => { this.imap.search(['UNSEEN'], (err, results) => {
if (err) { if (err) {
@@ -129,16 +129,16 @@ class EmailBotService {
this.imap.end(); this.imap.end();
return; return;
} }
if (!results || results.length === 0) { if (!results || results.length === 0) {
logger.info('No new messages found'); logger.info('No new messages found');
this.imap.end(); this.imap.end();
return; return;
} }
try { try {
const f = this.imap.fetch(results, { bodies: '' }); const f = this.imap.fetch(results, { bodies: '' });
f.on('message', (msg, seqno) => { f.on('message', (msg, seqno) => {
msg.on('body', (stream, info) => { msg.on('body', (stream, info) => {
simpleParser(stream, async (err, parsed) => { simpleParser(stream, async (err, parsed) => {
@@ -149,11 +149,11 @@ class EmailBotService {
}); });
}); });
}); });
f.once('error', (err) => { f.once('error', (err) => {
logger.error(`Fetch error: ${err}`); logger.error(`Fetch error: ${err}`);
}); });
f.once('end', () => { f.once('end', () => {
try { try {
this.imap.end(); this.imap.end();
@@ -172,7 +172,7 @@ class EmailBotService {
}); });
}); });
}); });
this.imap.connect(); this.imap.connect();
} catch (error) { } catch (error) {
logger.error(`Global error checking emails: ${error.message}`); logger.error(`Global error checking emails: ${error.message}`);
@@ -191,7 +191,7 @@ class EmailBotService {
from: process.env.EMAIL_USER, from: process.env.EMAIL_USER,
to, to,
subject, subject,
text text,
}; };
await this.transporter.sendMail(mailOptions); await this.transporter.sendMail(mailOptions);

View File

@@ -15,19 +15,19 @@ class IdentityService {
if (!provider || !providerId) { if (!provider || !providerId) {
return { provider, providerId }; return { provider, providerId };
} }
// Приводим провайдер к нижнему регистру // Приводим провайдер к нижнему регистру
const normalizedProvider = provider.toLowerCase(); const normalizedProvider = provider.toLowerCase();
// Для email и wallet приводим значение к нижнему регистру // Для email и wallet приводим значение к нижнему регистру
let normalizedProviderId = providerId; let normalizedProviderId = providerId;
if (normalizedProvider === 'wallet' || normalizedProvider === 'email') { if (normalizedProvider === 'wallet' || normalizedProvider === 'email') {
normalizedProviderId = providerId.toLowerCase(); normalizedProviderId = providerId.toLowerCase();
} }
return { return {
provider: normalizedProvider, provider: normalizedProvider,
providerId: normalizedProviderId providerId: normalizedProviderId,
}; };
} }
@@ -42,21 +42,25 @@ class IdentityService {
async saveIdentity(userId, provider, providerId, verified = true) { async saveIdentity(userId, provider, providerId, verified = true) {
try { try {
if (!userId || !provider || !providerId) { if (!userId || !provider || !providerId) {
logger.warn(`[IdentityService] Missing required parameters: userId=${userId}, provider=${provider}, providerId=${providerId}`); logger.warn(
`[IdentityService] Missing required parameters: userId=${userId}, provider=${provider}, providerId=${providerId}`
);
return { return {
success: false, success: false,
error: 'Missing required parameters' error: 'Missing required parameters',
}; };
} }
// Нормализуем значения // Нормализуем значения
const { provider: normalizedProvider, providerId: normalizedProviderId } = const { provider: normalizedProvider, providerId: normalizedProviderId } =
this.normalizeIdentity(provider, providerId); this.normalizeIdentity(provider, providerId);
// Проверяем тип провайдера и перенаправляем гостевые идентификаторы в guest_user_mapping // Проверяем тип провайдера и перенаправляем гостевые идентификаторы в guest_user_mapping
if (normalizedProvider === 'guest') { if (normalizedProvider === 'guest') {
logger.info(`[IdentityService] Converting guest identity for user ${userId} to guest_user_mapping: ${normalizedProviderId}`); logger.info(
`[IdentityService] Converting guest identity for user ${userId} to guest_user_mapping: ${normalizedProviderId}`
);
try { try {
await db.query( await db.query(
'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',
@@ -64,41 +68,50 @@ class IdentityService {
); );
return { success: true }; return { success: true };
} catch (guestError) { } catch (guestError) {
logger.error(`[IdentityService] Error saving guest identity for user ${userId}:`, guestError); logger.error(
`[IdentityService] Error saving guest identity for user ${userId}:`,
guestError
);
return { success: false, error: guestError.message }; return { success: false, error: guestError.message };
} }
} }
// Проверяем, разрешен ли такой тип провайдера // Проверяем, разрешен ли такой тип провайдера
const allowedProviders = ['email', 'wallet', 'telegram', 'username']; const allowedProviders = ['email', 'wallet', 'telegram', 'username'];
if (!allowedProviders.includes(normalizedProvider)) { if (!allowedProviders.includes(normalizedProvider)) {
logger.warn(`[IdentityService] Invalid provider type: ${normalizedProvider}`); logger.warn(`[IdentityService] Invalid provider type: ${normalizedProvider}`);
return { return {
success: false, success: false,
error: `Invalid provider type. Allowed types: ${allowedProviders.join(', ')}` error: `Invalid provider type. Allowed types: ${allowedProviders.join(', ')}`,
}; };
} }
logger.info(`[IdentityService] Saving identity for user ${userId}: ${normalizedProvider}:${normalizedProviderId}`); logger.info(
`[IdentityService] Saving identity for user ${userId}: ${normalizedProvider}:${normalizedProviderId}`
);
// Проверяем, существует ли уже такой идентификатор // Проверяем, существует ли уже такой идентификатор
const existingResult = await db.query( const existingResult = await db.query(
`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]
); );
if (existingResult.rows.length > 0) { if (existingResult.rows.length > 0) {
const existingUserId = existingResult.rows[0].user_id; const existingUserId = existingResult.rows[0].user_id;
// Если идентификатор уже принадлежит этому пользователю, ничего не делаем // Если идентификатор уже принадлежит этому пользователю, ничего не делаем
if (existingUserId === userId) { if (existingUserId === userId) {
logger.info(`[IdentityService] Identity ${normalizedProvider}:${normalizedProviderId} already exists for user ${userId}`); logger.info(
`[IdentityService] Identity ${normalizedProvider}:${normalizedProviderId} already exists for user ${userId}`
);
} else { } else {
// Если идентификатор принадлежит другому пользователю, логируем это // Если идентификатор принадлежит другому пользователю, логируем это
logger.warn(`[IdentityService] Identity ${normalizedProvider}:${normalizedProviderId} already belongs to user ${existingUserId}, not user ${userId}`); logger.warn(
`[IdentityService] Identity ${normalizedProvider}:${normalizedProviderId} already belongs to user ${existingUserId}, not user ${userId}`
);
return { return {
success: false, success: false,
error: `Identity already belongs to another user (${existingUserId})` error: `Identity already belongs to another user (${existingUserId})`,
}; };
} }
} else { } else {
@@ -108,16 +121,21 @@ class IdentityService {
VALUES ($1, $2, $3)`, VALUES ($1, $2, $3)`,
[userId, normalizedProvider, normalizedProviderId] [userId, normalizedProvider, normalizedProviderId]
); );
logger.info(`[IdentityService] Created new identity ${normalizedProvider}:${normalizedProviderId} for user ${userId}`); logger.info(
`[IdentityService] Created new identity ${normalizedProvider}:${normalizedProviderId} for user ${userId}`
);
} }
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
logger.error(`[IdentityService] Error saving identity ${provider}:${providerId} for user ${userId}:`, error); logger.error(
`[IdentityService] Error saving identity ${provider}:${providerId} for user ${userId}:`,
error
);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
} }
/** /**
* Получает все идентификаторы пользователя * Получает все идентификаторы пользователя
* @param {number} userId - ID пользователя * @param {number} userId - ID пользователя
@@ -129,12 +147,12 @@ class IdentityService {
logger.warn('[IdentityService] Missing userId parameter'); logger.warn('[IdentityService] Missing userId parameter');
return []; return [];
} }
const result = await db.query( const result = await db.query(
`SELECT provider, provider_id FROM user_identities WHERE user_id = $1`, `SELECT provider, provider_id FROM user_identities WHERE user_id = $1`,
[userId] [userId]
); );
logger.info(`[IdentityService] Found ${result.rows.length} identities for user ${userId}`); logger.info(`[IdentityService] Found ${result.rows.length} identities for user ${userId}`);
return result.rows; return result.rows;
} catch (error) { } catch (error) {
@@ -142,7 +160,7 @@ class IdentityService {
return []; return [];
} }
} }
/** /**
* Получает все идентификаторы пользователя определенного типа * Получает все идентификаторы пользователя определенного типа
* @param {number} userId - ID пользователя * @param {number} userId - ID пользователя
@@ -155,20 +173,25 @@ class IdentityService {
logger.warn(`[IdentityService] Missing parameters: userId=${userId}, provider=${provider}`); logger.warn(`[IdentityService] Missing parameters: userId=${userId}, provider=${provider}`);
return []; return [];
} }
const result = await db.query( const result = await db.query(
`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]
); );
logger.info(`[IdentityService] Found ${result.rows.length} ${provider} identities for user ${userId}`); logger.info(
return result.rows.map(row => row.provider_id); `[IdentityService] Found ${result.rows.length} ${provider} identities for user ${userId}`
);
return result.rows.map((row) => row.provider_id);
} catch (error) { } catch (error) {
logger.error(`[IdentityService] Error getting ${provider} identities for user ${userId}:`, error); logger.error(
`[IdentityService] Error getting ${provider} identities for user ${userId}:`,
error
);
return []; return [];
} }
} }
/** /**
* Находит пользователя по идентификатору * Находит пользователя по идентификатору
* @param {string} provider - Тип идентификатора * @param {string} provider - Тип идентификатора
@@ -178,34 +201,43 @@ class IdentityService {
async findUserByIdentity(provider, providerId) { async findUserByIdentity(provider, providerId) {
try { try {
if (!provider || !providerId) { if (!provider || !providerId) {
logger.warn(`[IdentityService] Missing parameters: provider=${provider}, providerId=${providerId}`); logger.warn(
`[IdentityService] Missing parameters: provider=${provider}, providerId=${providerId}`
);
return null; return null;
} }
// Нормализуем значения // Нормализуем значения
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.query(
`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`,
[normalizedProvider, normalizedProviderId] [normalizedProvider, normalizedProviderId]
); );
if (result.rows.length === 0) { if (result.rows.length === 0) {
logger.info(`[IdentityService] No user found with identity ${normalizedProvider}:${normalizedProviderId}`); logger.info(
`[IdentityService] No user found with identity ${normalizedProvider}:${normalizedProviderId}`
);
return null; return null;
} }
logger.info(`[IdentityService] Found user ${result.rows[0].id} with identity ${normalizedProvider}:${normalizedProviderId}`); logger.info(
`[IdentityService] Found user ${result.rows[0].id} with identity ${normalizedProvider}:${normalizedProviderId}`
);
return result.rows[0]; return result.rows[0];
} catch (error) { } catch (error) {
logger.error(`[IdentityService] Error finding user by identity ${provider}:${providerId}:`, error); logger.error(
`[IdentityService] Error finding user by identity ${provider}:${providerId}:`,
error
);
return null; return null;
} }
} }
/** /**
* Сохраняет идентификаторы из сессии для пользователя * Сохраняет идентификаторы из сессии для пользователя
* @param {object} session - Объект сессии * @param {object} session - Объект сессии
@@ -218,25 +250,30 @@ class IdentityService {
logger.warn(`[IdentityService] Missing parameters: session=${!!session}, userId=${userId}`); logger.warn(`[IdentityService] Missing parameters: session=${!!session}, userId=${userId}`);
return { success: false, error: 'Missing required parameters' }; return { success: false, error: 'Missing required parameters' };
} }
const results = []; const results = [];
// Сохраняем все постоянные идентификаторы из сессии // Сохраняем все постоянные идентификаторы из сессии
if (session.email) { if (session.email) {
const emailResult = await this.saveIdentity(userId, 'email', session.email, true); const emailResult = await this.saveIdentity(userId, 'email', session.email, true);
results.push({ type: 'email', result: emailResult }); results.push({ type: 'email', result: emailResult });
} }
if (session.address) { if (session.address) {
const walletResult = await this.saveIdentity(userId, 'wallet', session.address, true); const walletResult = await this.saveIdentity(userId, 'wallet', session.address, true);
results.push({ type: 'wallet', result: walletResult }); results.push({ type: 'wallet', result: walletResult });
} }
if (session.telegramId) { if (session.telegramId) {
const telegramResult = await this.saveIdentity(userId, 'telegram', session.telegramId, true); const telegramResult = await this.saveIdentity(
userId,
'telegram',
session.telegramId,
true
);
results.push({ type: 'telegram', result: telegramResult }); results.push({ type: 'telegram', result: telegramResult });
} }
// Сохраняем гостевые идентификаторы в guest_user_mapping // Сохраняем гостевые идентификаторы в guest_user_mapping
if (session.guestId) { if (session.guestId) {
try { try {
@@ -250,7 +287,7 @@ class IdentityService {
results.push({ type: 'guest', result: { success: false, error: error.message } }); results.push({ type: 'guest', result: { success: false, error: error.message } });
} }
} }
if (session.previousGuestId && session.previousGuestId !== session.guestId) { if (session.previousGuestId && session.previousGuestId !== session.guestId) {
try { try {
await db.query( await db.query(
@@ -259,19 +296,27 @@ class IdentityService {
); );
results.push({ type: 'previousGuest', result: { success: true } }); results.push({ type: 'previousGuest', result: { success: true } });
} catch (error) { } catch (error) {
logger.error(`[IdentityService] Error saving previous guest ID for user ${userId}:`, error); logger.error(
`[IdentityService] Error saving previous guest ID for user ${userId}:`,
error
);
results.push({ type: 'previousGuest', result: { success: false, error: error.message } }); results.push({ type: 'previousGuest', result: { success: false, error: error.message } });
} }
} }
logger.info(`[IdentityService] Saved ${results.length} identities from session for user ${userId}`); logger.info(
`[IdentityService] Saved ${results.length} identities from session for user ${userId}`
);
return { success: true, results }; return { success: true, results };
} catch (error) { } catch (error) {
logger.error(`[IdentityService] Error saving identities from session for user ${userId}:`, error); logger.error(
`[IdentityService] Error saving identities from session for user ${userId}:`,
error
);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
} }
/** /**
* Мигрирует все идентификаторы и сообщения от одного пользователя к другому * Мигрирует все идентификаторы и сообщения от одного пользователя к другому
* @param {number} fromUserId - ID исходного пользователя * @param {number} fromUserId - ID исходного пользователя
@@ -281,7 +326,9 @@ class IdentityService {
async migrateUserData(fromUserId, toUserId) { async migrateUserData(fromUserId, toUserId) {
try { try {
if (!fromUserId || !toUserId) { if (!fromUserId || !toUserId) {
logger.warn(`[IdentityService] Missing parameters: fromUserId=${fromUserId}, toUserId=${toUserId}`); logger.warn(
`[IdentityService] Missing parameters: fromUserId=${fromUserId}, toUserId=${toUserId}`
);
return { success: false, error: 'Missing required parameters' }; return { success: false, error: 'Missing required parameters' };
} }
@@ -295,7 +342,7 @@ class IdentityService {
`SELECT provider, provider_id FROM user_identities WHERE user_id = $1`, `SELECT provider, provider_id FROM user_identities WHERE user_id = $1`,
[fromUserId] [fromUserId]
); );
// Переносим каждый идентификатор // Переносим каждый идентификатор
for (const identity of identitiesResult.rows) { for (const identity of identitiesResult.rows) {
await client.query( await client.query(
@@ -304,7 +351,7 @@ class IdentityService {
ON CONFLICT (provider, provider_id) DO NOTHING`, ON CONFLICT (provider, provider_id) DO NOTHING`,
[toUserId, identity.provider, identity.provider_id] [toUserId, identity.provider, identity.provider_id]
); );
// Удаляем старый идентификатор // Удаляем старый идентификатор
await client.query( await client.query(
`DELETE FROM user_identities `DELETE FROM user_identities
@@ -312,13 +359,13 @@ class IdentityService {
[fromUserId, identity.provider, identity.provider_id] [fromUserId, identity.provider, identity.provider_id]
); );
} }
// Мигрируем гостевые идентификаторы из новой таблицы guest_user_mapping // Мигрируем гостевые идентификаторы из новой таблицы guest_user_mapping
const guestMappingsResult = await client.query( const guestMappingsResult = await client.query(
`SELECT guest_id, processed FROM guest_user_mapping WHERE user_id = $1`, `SELECT guest_id, processed FROM guest_user_mapping WHERE user_id = $1`,
[fromUserId] [fromUserId]
); );
// Переносим каждый гостевой идентификатор // Переносим каждый гостевой идентификатор
for (const mapping of guestMappingsResult.rows) { for (const mapping of guestMappingsResult.rows) {
await client.query( await client.query(
@@ -329,12 +376,9 @@ class IdentityService {
[toUserId, mapping.guest_id, mapping.processed] [toUserId, mapping.guest_id, mapping.processed]
); );
} }
// Удаляем старые гостевые маппинги // Удаляем старые гостевые маппинги
await client.query( await client.query(`DELETE FROM guest_user_mapping WHERE user_id = $1`, [fromUserId]);
`DELETE FROM guest_user_mapping WHERE user_id = $1`,
[fromUserId]
);
// Переносим все сообщения // Переносим все сообщения
await client.query( await client.query(
@@ -351,7 +395,7 @@ class IdentityService {
WHERE user_id = $2`, WHERE user_id = $2`,
[toUserId, fromUserId] [toUserId, fromUserId]
); );
// Переносим настройки пользователя // Переносим настройки пользователя
await client.query( await client.query(
`UPDATE user_preferences `UPDATE user_preferences
@@ -362,8 +406,10 @@ class IdentityService {
// Завершаем транзакцию // Завершаем транзакцию
await client.query('COMMIT'); await client.query('COMMIT');
logger.info(`[IdentityService] Successfully migrated data from user ${fromUserId} to ${toUserId}`); logger.info(
`[IdentityService] Successfully migrated data from user ${fromUserId} to ${toUserId}`
);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
await client.query('ROLLBACK'); await client.query('ROLLBACK');
@@ -386,20 +432,20 @@ class IdentityService {
async findRelatedUsers(identities) { async findRelatedUsers(identities) {
try { try {
const userIds = new Set(); const userIds = new Set();
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.query(
`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`,
[provider, providerId] [provider, providerId]
); );
result.rows.forEach(row => userIds.add(row.user_id)); result.rows.forEach((row) => userIds.add(row.user_id));
} }
return Array.from(userIds); return Array.from(userIds);
} catch (error) { } catch (error) {
logger.error(`[IdentityService] Error finding related users:`, error); logger.error(`[IdentityService] Error finding related users:`, error);
@@ -408,4 +454,4 @@ class IdentityService {
} }
} }
module.exports = new IdentityService(); module.exports = new IdentityService();

View File

@@ -31,5 +31,5 @@ module.exports = {
getConversationHistory: aiAssistant.getConversationHistory, getConversationHistory: aiAssistant.getConversationHistory,
telegramBot, telegramBot,
aiAssistant aiAssistant,
}; };

View File

@@ -14,7 +14,7 @@ class SessionService {
async saveSession(session) { async saveSession(session) {
try { try {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
session.save(err => { session.save((err) => {
if (err) { if (err) {
logger.error('Error saving session:', err); logger.error('Error saving session:', err);
reject(err); reject(err);
@@ -38,7 +38,9 @@ class SessionService {
*/ */
async linkGuestMessages(session, userId) { async linkGuestMessages(session, userId) {
try { try {
logger.info(`[linkGuestMessages] Starting for user ${userId} with guestId=${session.guestId}, previousGuestId=${session.previousGuestId}`); logger.info(
`[linkGuestMessages] Starting for user ${userId} with guestId=${session.guestId}, previousGuestId=${session.previousGuestId}`
);
// Инициализируем массив обработанных гостевых ID, если его нет // Инициализируем массив обработанных гостевых ID, если его нет
if (!session.processedGuestIds) { if (!session.processedGuestIds) {
@@ -50,15 +52,15 @@ class SessionService {
'SELECT guest_id FROM guest_user_mapping WHERE user_id = $1', 'SELECT guest_id FROM guest_user_mapping WHERE user_id = $1',
[userId] [userId]
); );
const userGuestIds = guestIdsResult.rows.map(row => row.guest_id); const userGuestIds = guestIdsResult.rows.map((row) => row.guest_id);
// Собираем все гостевые ID, которые нужно обработать // Собираем все гостевые ID, которые нужно обработать
const guestIdsToProcess = new Set(); const guestIdsToProcess = new Set();
// Добавляем текущий гостевой ID // Добавляем текущий гостевой ID
if (session.guestId && !session.processedGuestIds.includes(session.guestId)) { if (session.guestId && !session.processedGuestIds.includes(session.guestId)) {
guestIdsToProcess.add(session.guestId); guestIdsToProcess.add(session.guestId);
// Записываем связь с пользователем в новую таблицу // Записываем связь с пользователем в новую таблицу
await db.query( await db.query(
'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',
@@ -69,7 +71,7 @@ class SessionService {
// Добавляем предыдущий гостевой ID // Добавляем предыдущий гостевой ID
if (session.previousGuestId && !session.processedGuestIds.includes(session.previousGuestId)) { if (session.previousGuestId && !session.processedGuestIds.includes(session.previousGuestId)) {
guestIdsToProcess.add(session.previousGuestId); guestIdsToProcess.add(session.previousGuestId);
// Записываем связь с пользователем в новую таблицу // Записываем связь с пользователем в новую таблицу
await db.query( await db.query(
'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',
@@ -88,12 +90,11 @@ class SessionService {
for (const guestId of guestIdsToProcess) { for (const guestId of guestIdsToProcess) {
await this.processGuestMessagesWrapper(userId, guestId); await this.processGuestMessagesWrapper(userId, guestId);
session.processedGuestIds.push(guestId); session.processedGuestIds.push(guestId);
// Помечаем guestId как обработанный в базе данных // Помечаем guestId как обработанный в базе данных
await db.query( await db.query('UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', [
'UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', guestId,
[guestId] ]);
);
} }
// Сохраняем сессию // Сохраняем сессию
@@ -114,7 +115,9 @@ class SessionService {
*/ */
async processGuestMessagesWrapper(userId, guestId) { async processGuestMessagesWrapper(userId, guestId) {
try { try {
logger.info(`[processGuestMessagesWrapper] Processing messages: userId=${userId}, guestId=${guestId}`); logger.info(
`[processGuestMessagesWrapper] Processing messages: userId=${userId}, guestId=${guestId}`
);
return await processGuestMessages(userId, guestId); return await processGuestMessages(userId, guestId);
} catch (error) { } catch (error) {
logger.error(`[processGuestMessagesWrapper] Error: ${error.message}`, error); logger.error(`[processGuestMessagesWrapper] Error: ${error.message}`, error);
@@ -146,7 +149,7 @@ class SessionService {
async destroySession(session) { async destroySession(session) {
try { try {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
session.destroy(err => { session.destroy((err) => {
if (err) { if (err) {
logger.error('Error destroying session:', err); logger.error('Error destroying session:', err);
reject(err); reject(err);
@@ -173,29 +176,26 @@ class SessionService {
logger.warn('[SessionService] Cannot restore session without sessionId'); logger.warn('[SessionService] Cannot restore session without sessionId');
return null; return null;
} }
logger.info(`[SessionService] Attempting to retrieve session ${sessionId}`); logger.info(`[SessionService] Attempting to retrieve session ${sessionId}`);
const result = await db.query( const result = await db.query('SELECT sess FROM session WHERE sid = $1', [sessionId]);
'SELECT sess FROM session WHERE sid = $1',
[sessionId]
);
if (result.rows.length === 0) { if (result.rows.length === 0) {
logger.info(`[SessionService] No session found with ID ${sessionId}`); logger.info(`[SessionService] No session found with ID ${sessionId}`);
return null; return null;
} }
const sessionData = result.rows[0].sess; const sessionData = result.rows[0].sess;
logger.info(`[SessionService] Retrieved session data for ${sessionId}`); logger.info(`[SessionService] Retrieved session data for ${sessionId}`);
return sessionData; return sessionData;
} catch (error) { } catch (error) {
logger.error(`[SessionService] Error retrieving session ${sessionId}:`, error); logger.error(`[SessionService] Error retrieving session ${sessionId}:`, error);
return null; return null;
} }
} }
/** /**
* Обновляет данные аутентификации в сессии * Обновляет данные аутентификации в сессии
* @param {object} session - Объект сессии * @param {object} session - Объект сессии
@@ -208,23 +208,23 @@ class SessionService {
logger.warn('[SessionService] Missing parameters for updateAuthData'); logger.warn('[SessionService] Missing parameters for updateAuthData');
return false; return false;
} }
const { userId, authType, isAdmin, ...otherData } = authData; const { userId, authType, isAdmin, ...otherData } = authData;
if (!userId || !authType) { if (!userId || !authType) {
logger.warn('[SessionService] Missing userId or authType in authData'); logger.warn('[SessionService] Missing userId or authType in authData');
return false; return false;
} }
// Обновляем основные поля аутентификации // Обновляем основные поля аутентификации
session.userId = userId; session.userId = userId;
session.authType = authType; session.authType = authType;
session.authenticated = true; session.authenticated = true;
if (isAdmin !== undefined) { if (isAdmin !== undefined) {
session.isAdmin = isAdmin; session.isAdmin = isAdmin;
} }
// Обновляем дополнительные данные в зависимости от типа аутентификации // Обновляем дополнительные данные в зависимости от типа аутентификации
if (authType === 'wallet' && otherData.address) { if (authType === 'wallet' && otherData.address) {
session.address = otherData.address.toLowerCase(); session.address = otherData.address.toLowerCase();
@@ -235,16 +235,16 @@ class SessionService {
if (otherData.telegramUsername) session.telegramUsername = otherData.telegramUsername; if (otherData.telegramUsername) session.telegramUsername = otherData.telegramUsername;
if (otherData.telegramFirstName) session.telegramFirstName = otherData.telegramFirstName; if (otherData.telegramFirstName) session.telegramFirstName = otherData.telegramFirstName;
} }
// Сохраняем гостевые ID, если они предоставлены и не были ранее в сессии // Сохраняем гостевые ID, если они предоставлены и не были ранее в сессии
if (otherData.guestId && !session.guestId) { if (otherData.guestId && !session.guestId) {
session.guestId = otherData.guestId; session.guestId = otherData.guestId;
} }
if (otherData.previousGuestId && !session.previousGuestId) { if (otherData.previousGuestId && !session.previousGuestId) {
session.previousGuestId = otherData.previousGuestId; session.previousGuestId = otherData.previousGuestId;
} }
// Сохраняем обновленную сессию // Сохраняем обновленную сессию
return await this.saveSession(session, 'updateAuthData'); return await this.saveSession(session, 'updateAuthData');
} catch (error) { } catch (error) {
@@ -252,7 +252,7 @@ class SessionService {
return false; return false;
} }
} }
/** /**
* Очищает данные аутентификации в сессии * Очищает данные аутентификации в сессии
* @param {object} session - Объект сессии * @param {object} session - Объект сессии
@@ -264,10 +264,10 @@ class SessionService {
logger.warn('[SessionService] Cannot logout null session'); logger.warn('[SessionService] Cannot logout null session');
return false; return false;
} }
// Сохраняем гостевые ID перед очисткой // Сохраняем гостевые ID перед очисткой
const guestId = session.guestId; const guestId = session.guestId;
// Удаляем данные аутентификации // Удаляем данные аутентификации
delete session.userId; delete session.userId;
delete session.authenticated; delete session.authenticated;
@@ -278,12 +278,12 @@ class SessionService {
delete session.telegramId; delete session.telegramId;
delete session.telegramUsername; delete session.telegramUsername;
delete session.telegramFirstName; delete session.telegramFirstName;
// Восстанавливаем гостевой ID для продолжения работы // Восстанавливаем гостевой ID для продолжения работы
if (guestId) { if (guestId) {
session.guestId = guestId; session.guestId = guestId;
} }
// Сохраняем обновленную сессию // Сохраняем обновленную сессию
return await this.saveSession(session, 'logout'); return await this.saveSession(session, 'logout');
} catch (error) { } catch (error) {
@@ -294,4 +294,4 @@ class SessionService {
} }
const sessionService = new SessionService(); const sessionService = new SessionService();
module.exports = sessionService; module.exports = sessionService;

View File

@@ -20,7 +20,7 @@ async function getBot() {
// Обработка кодов верификации // Обработка кодов верификации
botInstance.on('text', async (ctx) => { botInstance.on('text', async (ctx) => {
const code = ctx.message.text.trim(); const code = ctx.message.text.trim();
try { try {
// Получаем код верификации для всех активных кодов с провайдером telegram // Получаем код верификации для всех активных кодов с провайдером telegram
const codeResult = await db.query( const codeResult = await db.query(
@@ -31,25 +31,24 @@ async function getBot() {
AND expires_at > NOW()`, AND expires_at > NOW()`,
[code] [code]
); );
if (codeResult.rows.length === 0) { if (codeResult.rows.length === 0) {
ctx.reply('Неверный код подтверждения'); ctx.reply('Неверный код подтверждения');
return; return;
} }
const verification = codeResult.rows[0]; const verification = codeResult.rows[0];
const providerId = verification.provider_id; const providerId = verification.provider_id;
const linkedUserId = verification.user_id; // Получаем связанный userId если он есть const linkedUserId = verification.user_id; // Получаем связанный userId если он есть
let userId; let userId;
// Отмечаем код как использованный // Отмечаем код как использованный
await db.query( await db.query('UPDATE verification_codes SET used = true WHERE id = $1', [
'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.query(
`SELECT ui.user_id `SELECT ui.user_id
@@ -57,7 +56,7 @@ async function getBot() {
WHERE ui.provider = 'telegram' AND ui.provider_id = $1`, WHERE ui.provider = 'telegram' AND ui.provider_id = $1`,
[ctx.from.id.toString()] [ctx.from.id.toString()]
); );
if (existingTelegramUser.rows.length > 0) { if (existingTelegramUser.rows.length > 0) {
// Если пользователь с таким Telegram ID уже существует, используем его // Если пользователь с таким Telegram ID уже существует, используем его
userId = existingTelegramUser.rows[0].user_id; userId = existingTelegramUser.rows[0].user_id;
@@ -74,7 +73,9 @@ async function getBot() {
VALUES ($1, $2, $3, NOW())`, VALUES ($1, $2, $3, NOW())`,
[userId, 'telegram', ctx.from.id.toString()] [userId, 'telegram', ctx.from.id.toString()]
); );
logger.info(`Linked Telegram account ${ctx.from.id} to pre-authenticated user ${userId}`); logger.info(
`Linked Telegram account ${ctx.from.id} to pre-authenticated user ${userId}`
);
} else { } else {
// Проверяем, есть ли пользователь, связанный с гостевым идентификатором // Проверяем, есть ли пользователь, связанный с гостевым идентификатором
let existingUserWithGuestId = null; let existingUserWithGuestId = null;
@@ -85,10 +86,12 @@ async function getBot() {
); );
if (guestUserResult.rows.length > 0) { if (guestUserResult.rows.length > 0) {
existingUserWithGuestId = guestUserResult.rows[0].user_id; existingUserWithGuestId = guestUserResult.rows[0].user_id;
logger.info(`Found existing user ${existingUserWithGuestId} by guest ID ${providerId}`); logger.info(
`Found existing user ${existingUserWithGuestId} by guest ID ${providerId}`
);
} }
} }
if (existingUserWithGuestId) { if (existingUserWithGuestId) {
// Используем существующего пользователя и добавляем ему Telegram идентификатор // Используем существующего пользователя и добавляем ему Telegram идентификатор
userId = existingUserWithGuestId; userId = existingUserWithGuestId;
@@ -106,7 +109,7 @@ async function getBot() {
['user'] ['user']
); );
userId = userResult.rows[0].id; userId = userResult.rows[0].id;
// Связываем Telegram с новым пользователем // Связываем Telegram с новым пользователем
await db.query( await db.query(
`INSERT INTO user_identities `INSERT INTO user_identities
@@ -114,7 +117,7 @@ async function getBot() {
VALUES ($1, $2, $3, NOW())`, VALUES ($1, $2, $3, NOW())`,
[userId, 'telegram', ctx.from.id.toString()] [userId, 'telegram', ctx.from.id.toString()]
); );
// Если был гостевой ID, связываем его с новым пользователем // Если был гостевой ID, связываем его с новым пользователем
if (providerId) { if (providerId) {
await db.query( await db.query(
@@ -125,12 +128,12 @@ async function getBot() {
[userId, providerId] [userId, providerId]
); );
} }
logger.info(`Created new user ${userId} with Telegram account ${ctx.from.id}`); logger.info(`Created new user ${userId} with Telegram account ${ctx.from.id}`);
} }
} }
} }
// Обновляем сессию в базе данных // Обновляем сессию в базе данных
await db.query( await db.query(
`UPDATE session `UPDATE session
@@ -140,23 +143,22 @@ async function getBot() {
JSON.stringify({ JSON.stringify({
userId: userId.toString(), userId: userId.toString(),
authenticated: true, authenticated: true,
authType: "telegram", authType: 'telegram',
telegramId: ctx.from.id.toString() telegramId: ctx.from.id.toString(),
}), }),
JSON.stringify({guestId: providerId}) JSON.stringify({ guestId: providerId }),
] ]
); );
// Отправляем сообщение об успешной аутентификации // Отправляем сообщение об успешной аутентификации
await ctx.reply('Аутентификация успешна! Можете вернуться в приложение.'); await ctx.reply('Аутентификация успешна! Можете вернуться в приложение.');
// Удаляем сообщение с кодом // Удаляем сообщение с кодом
try { try {
await ctx.deleteMessage(ctx.message.message_id); await ctx.deleteMessage(ctx.message.message_id);
} catch (error) { } catch (error) {
logger.warn('Could not delete code message:', error); logger.warn('Could not delete code message:', error);
} }
} catch (error) { } catch (error) {
logger.error('Error in Telegram auth:', error); logger.error('Error in Telegram auth:', error);
await ctx.reply('Произошла ошибка при аутентификации. Попробуйте позже.'); await ctx.reply('Произошла ошибка при аутентификации. Попробуйте позже.');
@@ -166,7 +168,7 @@ async function getBot() {
// Запускаем бота // Запускаем бота
await botInstance.launch(); await botInstance.launch();
} }
return botInstance; return botInstance;
} }
@@ -190,12 +192,12 @@ async function initTelegramAuth(session) {
// Используем временный идентификатор для создания кода верификации // Используем временный идентификатор для создания кода верификации
// Реальный пользователь будет создан или найден при проверке кода через бота // Реальный пользователь будет создан или найден при проверке кода через бота
const tempId = crypto.randomBytes(16).toString('hex'); const tempId = crypto.randomBytes(16).toString('hex');
// Если пользователь уже авторизован, сохраняем его userId в guest_user_mapping // Если пользователь уже авторизован, сохраняем его userId в guest_user_mapping
// чтобы потом при авторизации через бота этот пользователь был найден // чтобы потом при авторизации через бота этот пользователь был найден
if (session && session.authenticated && session.userId) { if (session && session.authenticated && session.userId) {
const guestId = session.guestId || tempId; const guestId = session.guestId || tempId;
// Связываем гостевой ID с текущим пользователем // Связываем гостевой ID с текущим пользователем
await db.query( await db.query(
`INSERT INTO guest_user_mapping (user_id, guest_id) `INSERT INTO guest_user_mapping (user_id, guest_id)
@@ -203,22 +205,26 @@ async function initTelegramAuth(session) {
ON CONFLICT (guest_id) DO UPDATE SET user_id = $1`, ON CONFLICT (guest_id) DO UPDATE SET user_id = $1`,
[session.userId, guestId] [session.userId, guestId]
); );
logger.info(`[initTelegramAuth] Linked guestId ${guestId} to authenticated user ${session.userId}`); logger.info(
`[initTelegramAuth] Linked guestId ${guestId} to authenticated user ${session.userId}`
);
} }
// Создаем код через сервис верификации с идентификатором // Создаем код через сервис верификации с идентификатором
const code = await verificationService.createVerificationCode( const code = await verificationService.createVerificationCode(
'telegram', 'telegram',
session.guestId || tempId, session.guestId || tempId,
session.authenticated ? session.userId : null session.authenticated ? session.userId : null
); );
logger.info(`[initTelegramAuth] Created verification code for guestId: ${session.guestId || tempId}${session.authenticated ? `, userId: ${session.userId}` : ''}`); logger.info(
`[initTelegramAuth] Created verification code for guestId: ${session.guestId || tempId}${session.authenticated ? `, userId: ${session.userId}` : ''}`
);
return { return {
verificationCode: code, verificationCode: code,
botLink: `https://t.me/${process.env.TELEGRAM_BOT_USERNAME}` botLink: `https://t.me/${process.env.TELEGRAM_BOT_USERNAME}`,
}; };
} catch (error) { } catch (error) {
logger.error('Error initializing Telegram auth:', error); logger.error('Error initializing Telegram auth:', error);
@@ -229,5 +235,5 @@ async function initTelegramAuth(session) {
module.exports = { module.exports = {
getBot, getBot,
stopBot, stopBot,
initTelegramAuth initTelegramAuth,
}; };

View File

@@ -9,7 +9,10 @@ class VerificationService {
// Генерация кода // Генерация кода
generateCode() { generateCode() {
const code = Math.random().toString(36).substring(2, 2 + this.codeLength).toUpperCase(); const code = Math.random()
.toString(36)
.substring(2, 2 + this.codeLength)
.toUpperCase();
logger.info(`Generated verification code: ${code}`); logger.info(`Generated verification code: ${code}`);
return code; return code;
} }
@@ -20,8 +23,10 @@ class VerificationService {
const expiresAt = new Date(Date.now() + this.expirationMinutes * 60 * 1000); const expiresAt = new Date(Date.now() + this.expirationMinutes * 60 * 1000);
try { try {
logger.info(`Creating verification code for ${provider}:${providerId}, userId: ${userId || 'null'}`); logger.info(
`Creating verification code for ${provider}:${providerId}, userId: ${userId || 'null'}`
);
// Если userId не указан, добавляем запись без ссылки на пользователя // Если userId не указан, добавляем запись без ссылки на пользователя
if (userId === null || userId === undefined) { if (userId === null || userId === undefined) {
await db.query( await db.query(
@@ -46,7 +51,7 @@ class VerificationService {
error: error.message, error: error.message,
provider, provider,
providerId, providerId,
userId userId,
}); });
throw error; throw error;
} }
@@ -56,11 +61,11 @@ class VerificationService {
async verifyCode(code, provider, providerId) { async verifyCode(code, provider, providerId) {
try { try {
logger.info(`Verifying code for ${provider}:${providerId}`); logger.info(`Verifying code for ${provider}:${providerId}`);
// Преобразуем код в верхний регистр для сравнения // Преобразуем код в верхний регистр для сравнения
const normalizedCode = code.toUpperCase(); const normalizedCode = code.toUpperCase();
logger.info(`Normalized code: ${normalizedCode}`); logger.info(`Normalized code: ${normalizedCode}`);
// Проверим, есть ли такой код в базе (для отладки) // Проверим, есть ли такой код в базе (для отладки)
const checkResult = await db.query( const checkResult = await db.query(
`SELECT code FROM verification_codes `SELECT code FROM verification_codes
@@ -70,13 +75,15 @@ class VerificationService {
AND expires_at > NOW()`, AND expires_at > NOW()`,
[provider, providerId] [provider, providerId]
); );
if (checkResult.rows.length > 0) { if (checkResult.rows.length > 0) {
logger.info(`Found codes for ${provider}:${providerId}: ${JSON.stringify(checkResult.rows.map(r => r.code))}`); logger.info(
`Found codes for ${provider}:${providerId}: ${JSON.stringify(checkResult.rows.map((r) => r.code))}`
);
} else { } else {
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.query(
`SELECT * FROM verification_codes `SELECT * FROM verification_codes
WHERE code = $1 WHERE code = $1
@@ -88,30 +95,29 @@ class VerificationService {
); );
if (result.rows.length === 0) { if (result.rows.length === 0) {
logger.warn(`Invalid or expired code for ${provider}:${providerId}. Input: ${normalizedCode}`); logger.warn(
`Invalid or expired code for ${provider}:${providerId}. Input: ${normalizedCode}`
);
return { success: false, error: 'Неверный или истекший код' }; return { success: false, error: 'Неверный или истекший код' };
} }
const verification = result.rows[0]; const verification = result.rows[0];
// Отмечаем код как использованный // Отмечаем код как использованный
await db.query( await db.query('UPDATE verification_codes SET used = true WHERE id = $1', [verification.id]);
'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 {
success: true, success: true,
userId: verification.user_id, userId: verification.user_id,
providerId: verification.provider_id providerId: verification.provider_id,
}; };
} catch (error) { } catch (error) {
logger.error('Error verifying code:', { logger.error('Error verifying code:', {
error: error.message, error: error.message,
code, code,
provider, provider,
providerId providerId,
}); });
throw error; throw error;
} }
@@ -131,4 +137,4 @@ class VerificationService {
} }
const verificationService = new VerificationService(); const verificationService = new VerificationService();
module.exports = verificationService; module.exports = verificationService;

View File

@@ -1,28 +1,28 @@
// Роли пользователей // Роли пользователей
const USER_ROLES = { const USER_ROLES = {
USER: 1, USER: 1,
ADMIN: 2 ADMIN: 2,
}; };
// Типы идентификаторов // Типы идентификаторов
const IDENTITY_TYPES = { const IDENTITY_TYPES = {
WALLET: 'wallet', WALLET: 'wallet',
EMAIL: 'email', EMAIL: 'email',
TELEGRAM: 'telegram' TELEGRAM: 'telegram',
}; };
// Каналы сообщений // Каналы сообщений
const MESSAGE_CHANNELS = { const MESSAGE_CHANNELS = {
WEB: 'web', WEB: 'web',
TELEGRAM: 'telegram', TELEGRAM: 'telegram',
EMAIL: 'email' EMAIL: 'email',
}; };
// Типы отправителей сообщений // Типы отправителей сообщений
const SENDER_TYPES = { const SENDER_TYPES = {
USER: 'user', USER: 'user',
AI: 'ai', AI: 'ai',
ADMIN: 'admin' ADMIN: 'admin',
}; };
// Коды ошибок // Коды ошибок
@@ -31,20 +31,20 @@ const ERROR_CODES = {
FORBIDDEN: 'forbidden', FORBIDDEN: 'forbidden',
NOT_FOUND: 'not_found', NOT_FOUND: 'not_found',
INTERNAL_ERROR: 'internal_error', INTERNAL_ERROR: 'internal_error',
BAD_REQUEST: 'bad_request' BAD_REQUEST: 'bad_request',
}; };
// Настройки сессии // Настройки сессии
const SESSION_CONFIG = { const SESSION_CONFIG = {
COOKIE_MAX_AGE: 24 * 60 * 60 * 1000, // 24 часа COOKIE_MAX_AGE: 24 * 60 * 60 * 1000, // 24 часа
COOKIE_SECURE: process.env.NODE_ENV === 'production', COOKIE_SECURE: process.env.NODE_ENV === 'production',
COOKIE_SAME_SITE: 'lax' COOKIE_SAME_SITE: 'lax',
}; };
// Настройки API // Настройки API
const API_CONFIG = { const API_CONFIG = {
RATE_LIMIT: 100, // запросов в минуту RATE_LIMIT: 100, // запросов в минуту
TIMEOUT: 30000 // 30 секунд TIMEOUT: 30000, // 30 секунд
}; };
module.exports = { module.exports = {
@@ -54,5 +54,5 @@ module.exports = {
SENDER_TYPES, SENDER_TYPES,
ERROR_CODES, ERROR_CODES,
SESSION_CONFIG, SESSION_CONFIG,
API_CONFIG API_CONFIG,
}; };

View File

@@ -10,4 +10,4 @@ function createError(message, statusCode = 500) {
return error; return error;
} }
module.exports = { createError }; module.exports = { createError };

View File

@@ -37,7 +37,8 @@ async function addUserIdentity(userId, provider, providerId) {
); );
return true; return true;
} catch (error) { } catch (error) {
if (error.code === '23505') { // Уникальное ограничение нарушено if (error.code === '23505') {
// Уникальное ограничение нарушено
return false; return false;
} }
throw error; throw error;
@@ -49,5 +50,5 @@ module.exports = {
isValidEmail, isValidEmail,
generateVerificationCode, generateVerificationCode,
checkUserIdentity, checkUserIdentity,
addUserIdentity addUserIdentity,
}; };

4936
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,8 @@
"preview": "vite preview", "preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --ignore-pattern 'node_modules/'", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --ignore-pattern 'node_modules/'",
"lint:fix": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-pattern 'node_modules/'", "lint:fix": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-pattern 'node_modules/'",
"lint:style": "stylelint \"**/*.{vue,css}\"",
"lint:style:fix": "stylelint \"**/*.{vue,css}\" --fix",
"format": "prettier --write \"**/*.{js,vue,json,md}\"", "format": "prettier --write \"**/*.{js,vue,json,md}\"",
"format:check": "prettier --check \"**/*.{js,vue,json,md}\"" "format:check": "prettier --check \"**/*.{js,vue,json,md}\""
}, },
@@ -34,9 +36,13 @@
"eslint-plugin-prettier": "^5.2.3", "eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-vue": "^9.32.0", "eslint-plugin-vue": "^9.32.0",
"globals": "^16.0.0", "globals": "^16.0.0",
"postcss-html": "^1.8.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"rollup": "^3.29.4", "rollup": "^3.29.4",
"rollup-plugin-polyfill-node": "^0.12.0", "rollup-plugin-polyfill-node": "^0.12.0",
"stylelint": "^14.16.1",
"stylelint-config-prettier": "^9.0.5",
"stylelint-config-standard-vue": "^1.0.0",
"vite": "^6.2.3" "vite": "^6.2.3"
} }
} }

View File

@@ -634,11 +634,17 @@
const loadMessages = async (options = {}) => { const loadMessages = async (options = {}) => {
const { silent = false, initial = false, authType = null } = options; const { silent = false, initial = false, authType = null } = options;
// Усиленная проверка для предотвращения параллельного выполнения
if (messageLoading.value.isInProgress) {
console.warn('[loadMessages] Выполнение уже идет, пропуск вызова.');
return;
}
messageLoading.value.isInProgress = true; // Устанавливаем флаг НЕМЕДЛЕННО
if (messageLoading.value.isLoadingMore && !initial) return; if (messageLoading.value.isLoadingMore && !initial) return;
try { try {
messageLoading.value.isLoadingMore = true; messageLoading.value.isLoadingMore = true;
messageLoading.value.isInProgress = true;
if (!silent) isLoading.value = true; if (!silent) isLoading.value = true;
console.log( console.log(
@@ -732,8 +738,8 @@
removeFromStorage('guestId'); removeFromStorage('guestId');
} }
} else if (response.data.messages && response.data.messages.length) { } else if (response.data.messages && response.data.messages.length) {
// Иначе добавляем к существующим // Иначе добавляем к существующим (В НАЧАЛО МАССИВА)
messages.value = [...messages.value, ...response.data.messages]; messages.value = [...response.data.messages, ...messages.value];
} }
console.log(`Загружено ${messages.value.length} сообщений из истории`); console.log(`Загружено ${messages.value.length} сообщений из истории`);

View File

@@ -0,0 +1,15 @@
module.exports = {
extends: [
// Используем стандартную конфигурацию для Vue (включает CSS и <style> в .vue)
'stylelint-config-standard-vue',
// Отключает правила Stylelint, конфликтующие с Prettier
'stylelint-config-prettier',
],
// Здесь можно добавить или переопределить правила
rules: {
// Пример: можно отключить правило о пустой строке перед комментариями
// 'comment-empty-line-before': null,
// Пример: требовать нижний регистр для имен анимаций
// 'keyframes-name-pattern': '^[a-z][a-z0-9-]*$',
},
};

File diff suppressed because it is too large Load Diff