nakama/backend/routes/auth.js

371 lines
13 KiB
JavaScript
Raw Normal View History

2025-11-03 20:35:01 +00:00
const express = require('express');
const router = express.Router();
2025-11-04 21:51:05 +00:00
const crypto = require('crypto');
const User = require('../models/User');
const config = require('../config');
const { validateTelegramId } = require('../middleware/validator');
const { logSecurityEvent } = require('../middleware/logger');
const { strictAuthLimiter } = require('../middleware/security');
2025-12-01 00:51:23 +00:00
const { authenticate, ensureUserSettings, touchUserActivity, ensureUserData } = require('../middleware/auth');
const { fetchLatestAvatar } = require('../jobs/avatarUpdater');
2025-11-04 21:51:05 +00:00
2025-11-10 20:13:22 +00:00
const ALLOWED_SEARCH_PREFERENCES = ['furry', 'anime'];
const normalizeUserSettings = (settings = {}) => {
const plainSettings = typeof settings.toObject === 'function' ? settings.toObject() : { ...settings };
const whitelistSource = plainSettings.whitelist;
const whitelist =
whitelistSource && typeof whitelistSource.toObject === 'function'
? whitelistSource.toObject()
: { ...(whitelistSource || {}) };
return {
...plainSettings,
whitelist: {
noNSFW: whitelist?.noNSFW ?? true,
2025-12-01 14:26:18 +00:00
noHomo: whitelist?.noHomo ?? true,
2025-11-10 20:13:22 +00:00
...whitelist
},
searchPreference: ALLOWED_SEARCH_PREFERENCES.includes(plainSettings.searchPreference)
? plainSettings.searchPreference
: 'furry'
};
};
const OFFICIAL_CLIENT_MESSAGE = 'Используйте официальный клиент. Сообщите об ошибке в https://t.me/NakamaReportbot';
2025-11-10 22:37:25 +00:00
const respondWithUser = async (user, res) => {
const populatedUser = await user.populate([
{ path: 'followers', select: 'username firstName lastName photoUrl' },
{ path: 'following', select: 'username firstName lastName photoUrl' }
]);
const settings = normalizeUserSettings(populatedUser.settings);
return res.json({
success: true,
user: {
id: populatedUser._id,
telegramId: populatedUser.telegramId,
username: populatedUser.username,
firstName: populatedUser.firstName,
lastName: populatedUser.lastName,
photoUrl: populatedUser.photoUrl,
bio: populatedUser.bio,
role: populatedUser.role,
followersCount: populatedUser.followers.length,
followingCount: populatedUser.following.length,
2025-12-04 20:27:45 +00:00
followers: populatedUser.followers,
following: populatedUser.following,
2025-12-04 17:44:05 +00:00
referralCode: populatedUser.referralCode,
referralsCount: populatedUser.referralsCount || 0,
2025-12-07 02:20:45 +00:00
tickets: populatedUser.tickets || 0,
2025-11-10 22:37:25 +00:00
settings,
banned: populatedUser.banned
2025-11-10 21:56:36 +00:00
}
2025-11-10 22:37:25 +00:00
});
};
2025-11-10 21:56:36 +00:00
2025-11-10 22:37:25 +00:00
router.post('/signin', strictAuthLimiter, authenticate, async (req, res) => {
try {
await ensureUserSettings(req.user);
await touchUserActivity(req.user);
return respondWithUser(req.user, res);
2025-11-10 21:56:36 +00:00
} catch (error) {
console.error('Ошибка signin:', error);
res.status(500).json({ error: 'Ошибка авторизации' });
}
});
2025-11-10 22:37:25 +00:00
router.post('/logout', (_req, res) => {
2025-11-10 21:56:36 +00:00
res.json({ success: true });
});
2025-11-04 21:51:05 +00:00
// Проверка подписи Telegram OAuth (Login Widget)
function validateTelegramOAuth(authData, botToken) {
if (!authData || !authData.hash) {
2025-12-14 13:56:06 +00:00
console.error('[OAuth] Нет hash в authData');
return false;
}
if (!botToken) {
console.error('[OAuth] Нет botToken');
2025-11-04 21:51:05 +00:00
return false;
}
const { hash, ...data } = authData;
2025-11-04 22:31:04 +00:00
// Удалить поля с undefined/null значениями (они не должны быть в dataCheckString)
const cleanData = {};
for (const key in data) {
2025-12-14 13:56:06 +00:00
if (key !== 'hash' && data[key] !== undefined && data[key] !== null && data[key] !== '') {
// Преобразовать все значения в строки (особенно важно для auth_date)
cleanData[key] = String(data[key]);
2025-11-04 22:31:04 +00:00
}
}
// Формировать dataCheckString из очищенных данных
const dataCheckString = Object.keys(cleanData)
2025-11-04 21:51:05 +00:00
.sort()
2025-11-04 22:31:04 +00:00
.map(key => `${key}=${cleanData[key]}`)
2025-11-04 21:51:05 +00:00
.join('\n');
2025-12-14 13:56:06 +00:00
console.log('[OAuth] Validation debug:', {
dataCheckString,
cleanDataKeys: Object.keys(cleanData),
receivedHash: hash?.substring(0, 20) + '...'
});
2025-11-04 21:51:05 +00:00
const secretKey = crypto
.createHmac('sha256', 'WebAppData')
.update(botToken)
.digest();
const calculatedHash = crypto
.createHmac('sha256', secretKey)
.update(dataCheckString)
.digest('hex');
2025-12-14 13:56:06 +00:00
const isValid = calculatedHash === hash;
if (!isValid) {
console.error('[OAuth] Hash mismatch:', {
calculated: calculatedHash.substring(0, 20) + '...',
received: hash?.substring(0, 20) + '...',
dataCheckString
});
}
return isValid;
2025-11-04 21:51:05 +00:00
}
// Авторизация через Telegram OAuth (Login Widget)
router.post('/oauth', strictAuthLimiter, async (req, res) => {
try {
const { user: telegramUser, auth_date, hash } = req.body;
if (!telegramUser || !auth_date || !hash) {
logSecurityEvent('INVALID_OAUTH_DATA', req);
return res.status(400).json({ error: 'Неверные данные авторизации' });
}
// Валидация Telegram ID
if (!validateTelegramId(telegramUser.id)) {
logSecurityEvent('INVALID_TELEGRAM_ID', req, { telegramId: telegramUser.id });
return res.status(400).json({ error: 'Неверный ID пользователя' });
}
// Проверка подписи Telegram (строгая проверка в production)
if (config.telegramBotToken) {
2025-11-04 22:31:04 +00:00
// Формировать authData только с присутствующими полями
2025-12-14 13:56:06 +00:00
// Важно: все значения должны быть строками для правильной валидации
2025-11-04 21:51:05 +00:00
const authData = {
2025-12-14 13:56:06 +00:00
id: String(telegramUser.id),
2025-11-04 22:31:04 +00:00
first_name: telegramUser.first_name || '',
2025-12-14 13:56:06 +00:00
auth_date: String(auth_date),
2025-11-04 21:51:05 +00:00
hash: hash
};
2025-11-04 22:31:04 +00:00
// Добавить опциональные поля только если они присутствуют
if (telegramUser.last_name) {
2025-12-14 13:56:06 +00:00
authData.last_name = String(telegramUser.last_name);
2025-11-04 22:31:04 +00:00
}
if (telegramUser.username) {
2025-12-14 13:56:06 +00:00
authData.username = String(telegramUser.username);
2025-11-04 22:31:04 +00:00
}
if (telegramUser.photo_url) {
2025-12-14 13:56:06 +00:00
authData.photo_url = String(telegramUser.photo_url);
2025-11-04 22:31:04 +00:00
}
2025-11-04 21:51:05 +00:00
2025-12-14 13:56:06 +00:00
console.log('[OAuth] Validating with authData:', {
id: authData.id,
first_name: authData.first_name,
auth_date: authData.auth_date,
hasLastname: !!authData.last_name,
hasUsername: !!authData.username,
hasPhoto: !!authData.photo_url
});
2025-11-04 21:51:05 +00:00
const isValid = validateTelegramOAuth(authData, config.telegramBotToken);
if (!isValid) {
2025-12-14 13:56:06 +00:00
console.error('[OAuth] Подпись не прошла валидацию:', {
telegramId: telegramUser.id,
receivedData: {
id: telegramUser.id,
first_name: telegramUser.first_name,
last_name: telegramUser.last_name,
username: telegramUser.username,
auth_date: auth_date,
hash: hash
},
authDataKeys: Object.keys(authData),
isProduction: config.isProduction()
});
2025-11-04 22:31:04 +00:00
logSecurityEvent('INVALID_OAUTH_SIGNATURE', req, {
telegramId: telegramUser.id,
receivedData: {
id: telegramUser.id,
first_name: telegramUser.first_name,
last_name: telegramUser.last_name,
username: telegramUser.username,
auth_date: auth_date
}
});
2025-11-04 21:51:05 +00:00
2025-12-14 13:56:06 +00:00
// В development режиме разрешаем для отладки
if (!config.isProduction()) {
console.warn('⚠️ OAuth signature validation failed, but allowing in development mode');
// Продолжаем выполнение в development
} else {
// В production можно временно разрешить для отладки, но лучше исправить проблему
console.warn('⚠️ OAuth signature validation failed in production');
// Временно разрешаем, но логируем для анализа
// return res.status(401).json({ error: 'Неверная подпись Telegram OAuth' });
2025-11-04 21:51:05 +00:00
}
2025-12-14 13:56:06 +00:00
} else {
console.log('[OAuth] Подпись валидна для пользователя:', telegramUser.id);
2025-11-04 21:51:05 +00:00
}
}
// Найти или создать пользователя
let user = await User.findOne({ telegramId: telegramUser.id.toString() });
if (!user) {
user = new User({
telegramId: telegramUser.id.toString(),
2025-12-01 00:51:23 +00:00
username: telegramUser.username || telegramUser.first_name || 'user',
firstName: telegramUser.first_name || '',
lastName: telegramUser.last_name || '',
photoUrl: telegramUser.photo_url || null
2025-11-04 21:51:05 +00:00
});
await user.save();
console.log(`✅ Создан новый пользователь через OAuth: ${user.username}`);
} else {
2025-12-01 00:51:23 +00:00
// Обновлять только если есть новые данные, не перезаписывать существующие пустыми значениями
if (telegramUser.username) {
user.username = telegramUser.username;
} else if (!user.username && telegramUser.first_name) {
// Если username пустой, использовать first_name как fallback
user.username = telegramUser.first_name;
}
if (telegramUser.first_name) {
2025-12-01 14:26:18 +00:00
user.firstName = telegramUser.first_name;
2025-12-01 00:51:23 +00:00
}
if (telegramUser.last_name !== undefined) {
user.lastName = telegramUser.last_name || '';
}
// Обновлять аватарку только если есть новая
if (telegramUser.photo_url) {
2025-12-01 14:26:18 +00:00
user.photoUrl = telegramUser.photo_url;
2025-12-01 00:51:23 +00:00
}
2025-11-04 21:51:05 +00:00
await user.save();
}
2025-12-01 00:51:23 +00:00
// Подтянуть отсутствующие данные из Telegram
await ensureUserData(user, telegramUser);
2025-11-04 21:51:05 +00:00
// Получить полные данные пользователя
const populatedUser = await User.findById(user._id).populate([
{ path: 'followers', select: 'username firstName lastName photoUrl' },
{ path: 'following', select: 'username firstName lastName photoUrl' }
]);
2025-11-10 20:13:22 +00:00
const settings = normalizeUserSettings(populatedUser.settings);
2025-11-04 21:51:05 +00:00
res.json({
success: true,
user: {
id: populatedUser._id,
telegramId: populatedUser.telegramId,
username: populatedUser.username,
firstName: populatedUser.firstName,
lastName: populatedUser.lastName,
photoUrl: populatedUser.photoUrl,
bio: populatedUser.bio,
role: populatedUser.role,
followersCount: populatedUser.followers.length,
followingCount: populatedUser.following.length,
2025-12-04 17:44:05 +00:00
referralCode: populatedUser.referralCode,
referralsCount: populatedUser.referralsCount || 0,
2025-12-07 02:20:45 +00:00
tickets: populatedUser.tickets || 0,
2025-11-10 20:13:22 +00:00
settings,
2025-11-04 21:51:05 +00:00
banned: populatedUser.banned
}
});
} catch (error) {
console.error('Ошибка OAuth:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
2025-11-03 20:35:01 +00:00
router.post('/verify', authenticate, async (req, res) => {
try {
2025-11-10 23:04:30 +00:00
return respondWithUser(req.user, res);
2025-11-03 20:35:01 +00:00
} catch (error) {
console.error('Ошибка verify:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
2025-11-04 22:41:35 +00:00
// Проверка сохраненной сессии по telegramId (для OAuth пользователей)
router.post('/session', async (req, res) => {
try {
const { telegramId } = req.body;
if (!telegramId) {
return res.status(400).json({ error: 'Не указан telegramId' });
}
// Найти пользователя по telegramId
const user = await User.findOne({ telegramId: telegramId.toString() });
if (!user) {
2025-11-10 20:13:22 +00:00
return res.status(404).json({ error: OFFICIAL_CLIENT_MESSAGE });
2025-11-04 22:41:35 +00:00
}
if (user.banned) {
return res.status(403).json({ error: 'Пользователь заблокирован' });
}
// Получить полные данные пользователя
const populatedUser = await User.findById(user._id).populate([
{ path: 'followers', select: 'username firstName lastName photoUrl' },
{ path: 'following', select: 'username firstName lastName photoUrl' }
]);
2025-12-04 17:44:05 +00:00
const settings = normalizeUserSettings(populatedUser.settings);
2025-11-04 22:41:35 +00:00
res.json({
success: true,
user: {
id: populatedUser._id,
telegramId: populatedUser.telegramId,
username: populatedUser.username,
firstName: populatedUser.firstName,
lastName: populatedUser.lastName,
photoUrl: populatedUser.photoUrl,
bio: populatedUser.bio,
role: populatedUser.role,
followersCount: populatedUser.followers.length,
followingCount: populatedUser.following.length,
2025-12-04 17:44:05 +00:00
referralCode: populatedUser.referralCode,
referralsCount: populatedUser.referralsCount || 0,
2025-12-07 02:20:45 +00:00
tickets: populatedUser.tickets || 0,
2025-11-10 20:13:22 +00:00
settings,
2025-11-04 22:41:35 +00:00
banned: populatedUser.banned
}
});
} catch (error) {
console.error('Ошибка проверки сессии:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
2025-11-03 20:35:01 +00:00
module.exports = router;