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,
|
|
|
|
|
|
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) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const { hash, ...data } = authData;
|
2025-11-04 22:31:04 +00:00
|
|
|
|
|
|
|
|
|
|
// Удалить поля с undefined/null значениями (они не должны быть в dataCheckString)
|
|
|
|
|
|
const cleanData = {};
|
|
|
|
|
|
for (const key in data) {
|
|
|
|
|
|
if (data[key] !== undefined && data[key] !== null && data[key] !== '') {
|
|
|
|
|
|
cleanData[key] = data[key];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Формировать 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');
|
|
|
|
|
|
|
|
|
|
|
|
const secretKey = crypto
|
|
|
|
|
|
.createHmac('sha256', 'WebAppData')
|
|
|
|
|
|
.update(botToken)
|
|
|
|
|
|
.digest();
|
|
|
|
|
|
|
|
|
|
|
|
const calculatedHash = crypto
|
|
|
|
|
|
.createHmac('sha256', secretKey)
|
|
|
|
|
|
.update(dataCheckString)
|
|
|
|
|
|
.digest('hex');
|
|
|
|
|
|
|
|
|
|
|
|
return calculatedHash === hash;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Авторизация через 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-11-04 21:51:05 +00:00
|
|
|
|
const authData = {
|
|
|
|
|
|
id: telegramUser.id,
|
2025-11-04 22:31:04 +00:00
|
|
|
|
first_name: telegramUser.first_name || '',
|
|
|
|
|
|
auth_date: auth_date.toString(),
|
2025-11-04 21:51:05 +00:00
|
|
|
|
hash: hash
|
|
|
|
|
|
};
|
2025-11-04 22:31:04 +00:00
|
|
|
|
|
|
|
|
|
|
// Добавить опциональные поля только если они присутствуют
|
|
|
|
|
|
if (telegramUser.last_name) {
|
|
|
|
|
|
authData.last_name = telegramUser.last_name;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (telegramUser.username) {
|
|
|
|
|
|
authData.username = telegramUser.username;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (telegramUser.photo_url) {
|
|
|
|
|
|
authData.photo_url = telegramUser.photo_url;
|
|
|
|
|
|
}
|
2025-11-04 21:51:05 +00:00
|
|
|
|
|
|
|
|
|
|
const isValid = validateTelegramOAuth(authData, config.telegramBotToken);
|
|
|
|
|
|
|
|
|
|
|
|
if (!isValid) {
|
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-11-04 22:31:04 +00:00
|
|
|
|
// В production строгая проверка, но для отладки можно временно отключить
|
2025-11-04 21:51:05 +00:00
|
|
|
|
if (config.isProduction()) {
|
2025-11-04 22:31:04 +00:00
|
|
|
|
// Временно разрешить в production для отладки (можно вернуть строгую проверку)
|
|
|
|
|
|
console.warn('⚠️ OAuth signature validation failed, but allowing in production for debugging');
|
2025-11-04 22:41:35 +00:00
|
|
|
|
return res.status(401).json({ error: 'Неверная подпись Telegram OAuth' });
|
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-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' }
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
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-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;
|
|
|
|
|
|
|