2025-11-03 20:35:01 +00:00
|
|
|
|
const User = require('../models/User');
|
2025-11-04 21:51:05 +00:00
|
|
|
|
const { validateTelegramId } = require('./validator');
|
|
|
|
|
|
const { logSecurityEvent } = require('./logger');
|
2025-11-10 22:37:25 +00:00
|
|
|
|
const { validateAndParseInitData } = require('../utils/telegram');
|
2025-12-01 00:51:23 +00:00
|
|
|
|
const { fetchLatestAvatar } = require('../jobs/avatarUpdater');
|
2025-11-03 20:35:01 +00:00
|
|
|
|
|
2025-11-10 20:13:22 +00:00
|
|
|
|
const OFFICIAL_CLIENT_MESSAGE = 'Используйте официальный клиент. Сообщите об ошибке в https://t.me/NakamaReportbot';
|
|
|
|
|
|
const ALLOWED_SEARCH_PREFERENCES = ['furry', 'anime'];
|
|
|
|
|
|
|
|
|
|
|
|
const touchUserActivity = async (user) => {
|
|
|
|
|
|
if (!user) return;
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
const shouldUpdate =
|
|
|
|
|
|
!user.lastActiveAt ||
|
|
|
|
|
|
Math.abs(now - new Date(user.lastActiveAt).getTime()) > 5 * 60 * 1000;
|
|
|
|
|
|
|
|
|
|
|
|
if (shouldUpdate) {
|
|
|
|
|
|
user.lastActiveAt = new Date(now);
|
|
|
|
|
|
await user.save();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const ensureUserSettings = async (user) => {
|
|
|
|
|
|
if (!user) return;
|
|
|
|
|
|
let updated = false;
|
|
|
|
|
|
|
|
|
|
|
|
if (!user.settings) {
|
|
|
|
|
|
user.settings = {};
|
|
|
|
|
|
updated = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!ALLOWED_SEARCH_PREFERENCES.includes(user.settings.searchPreference)) {
|
|
|
|
|
|
user.settings.searchPreference = 'furry';
|
|
|
|
|
|
updated = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!user.settings.whitelist) {
|
2025-12-01 14:26:18 +00:00
|
|
|
|
user.settings.whitelist = { noNSFW: true, noHomo: true };
|
2025-11-10 20:13:22 +00:00
|
|
|
|
updated = true;
|
2025-12-01 14:26:18 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
if (user.settings.whitelist.noNSFW === undefined) {
|
2025-12-01 14:48:54 +00:00
|
|
|
|
user.settings.whitelist.noNSFW = true;
|
|
|
|
|
|
updated = true;
|
2025-12-01 14:26:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
if (user.settings.whitelist.noHomo === undefined) {
|
|
|
|
|
|
user.settings.whitelist.noHomo = true;
|
|
|
|
|
|
updated = true;
|
|
|
|
|
|
}
|
2025-11-10 20:13:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (updated) {
|
|
|
|
|
|
user.markModified('settings');
|
|
|
|
|
|
await user.save();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-01 01:02:46 +00:00
|
|
|
|
// Нормализовать данные пользователя из Telegram (поддержка camelCase и snake_case)
|
|
|
|
|
|
const normalizeTelegramUser = (telegramUser) => {
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: telegramUser.id,
|
|
|
|
|
|
username: telegramUser.username || telegramUser.userName,
|
|
|
|
|
|
firstName: telegramUser.firstName || telegramUser.first_name || '',
|
|
|
|
|
|
lastName: telegramUser.lastName || telegramUser.last_name || '',
|
|
|
|
|
|
photoUrl: telegramUser.photoUrl || telegramUser.photo_url || null
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-01 00:51:23 +00:00
|
|
|
|
// Подтянуть отсутствующие данные пользователя из Telegram
|
|
|
|
|
|
const ensureUserData = async (user, telegramUser) => {
|
|
|
|
|
|
if (!user || !telegramUser) return;
|
|
|
|
|
|
|
2025-12-01 01:02:46 +00:00
|
|
|
|
// Нормализовать данные (поддержка camelCase и snake_case)
|
|
|
|
|
|
const normalized = normalizeTelegramUser(telegramUser);
|
|
|
|
|
|
|
2025-12-01 00:51:23 +00:00
|
|
|
|
let updated = false;
|
|
|
|
|
|
|
|
|
|
|
|
// Обновить username, если отсутствует или пустой
|
|
|
|
|
|
if (!user.username || user.username.trim() === '') {
|
2025-12-01 01:02:46 +00:00
|
|
|
|
if (normalized.username) {
|
|
|
|
|
|
user.username = normalized.username;
|
2025-12-01 00:51:23 +00:00
|
|
|
|
updated = true;
|
2025-12-01 01:02:46 +00:00
|
|
|
|
} else if (normalized.firstName) {
|
|
|
|
|
|
user.username = normalized.firstName;
|
2025-12-01 00:51:23 +00:00
|
|
|
|
updated = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Обновить firstName, если отсутствует
|
2025-12-01 01:02:46 +00:00
|
|
|
|
if (!user.firstName && normalized.firstName) {
|
|
|
|
|
|
user.firstName = normalized.firstName;
|
2025-12-01 00:51:23 +00:00
|
|
|
|
updated = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Обновить lastName, если отсутствует
|
|
|
|
|
|
if (user.lastName === undefined || user.lastName === null) {
|
2025-12-01 01:02:46 +00:00
|
|
|
|
user.lastName = normalized.lastName;
|
2025-12-01 00:51:23 +00:00
|
|
|
|
updated = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Обновить аватарку, если отсутствует
|
|
|
|
|
|
if (!user.photoUrl) {
|
2025-12-01 01:02:46 +00:00
|
|
|
|
// Сначала проверить photoUrl из initData
|
|
|
|
|
|
if (normalized.photoUrl) {
|
|
|
|
|
|
user.photoUrl = normalized.photoUrl;
|
2025-12-01 00:51:23 +00:00
|
|
|
|
updated = true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Если нет в initData, попробовать получить через Bot API
|
|
|
|
|
|
try {
|
|
|
|
|
|
const avatarUrl = await fetchLatestAvatar(user.telegramId);
|
|
|
|
|
|
if (avatarUrl) {
|
|
|
|
|
|
user.photoUrl = avatarUrl;
|
|
|
|
|
|
updated = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// Игнорируем ошибки получения аватарки
|
|
|
|
|
|
console.log('Не удалось получить аватарку через Bot API:', error.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (updated) {
|
|
|
|
|
|
await user.save();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-03 20:35:01 +00:00
|
|
|
|
const authenticate = async (req, res, next) => {
|
|
|
|
|
|
try {
|
2025-11-10 22:37:25 +00:00
|
|
|
|
const authHeader = req.headers.authorization || '';
|
2025-11-10 22:48:18 +00:00
|
|
|
|
let initDataRaw = null;
|
2025-11-10 21:22:58 +00:00
|
|
|
|
|
2025-11-10 22:48:18 +00:00
|
|
|
|
if (authHeader.startsWith('tma ')) {
|
|
|
|
|
|
initDataRaw = authHeader.slice(4).trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!initDataRaw) {
|
|
|
|
|
|
const headerInitData = req.headers['x-telegram-init-data'];
|
|
|
|
|
|
if (headerInitData && typeof headerInitData === 'string') {
|
|
|
|
|
|
initDataRaw = headerInitData.trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!initDataRaw) {
|
2025-11-10 22:37:25 +00:00
|
|
|
|
logSecurityEvent('AUTH_TOKEN_MISSING', req);
|
|
|
|
|
|
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
|
2025-11-03 20:35:01 +00:00
|
|
|
|
}
|
2025-11-10 21:22:58 +00:00
|
|
|
|
|
2025-11-10 22:37:25 +00:00
|
|
|
|
if (!initDataRaw) {
|
|
|
|
|
|
logSecurityEvent('EMPTY_INITDATA', req);
|
2025-11-10 20:13:22 +00:00
|
|
|
|
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
|
2025-11-03 22:41:34 +00:00
|
|
|
|
}
|
2025-11-10 21:22:58 +00:00
|
|
|
|
|
2025-11-10 22:37:25 +00:00
|
|
|
|
let payload;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
payload = validateAndParseInitData(initDataRaw);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
logSecurityEvent('INVALID_INITDATA', req, { reason: error.message });
|
|
|
|
|
|
return res.status(401).json({ error: `${error.message}. ${OFFICIAL_CLIENT_MESSAGE}` });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const telegramUser = payload.user;
|
|
|
|
|
|
|
|
|
|
|
|
if (!validateTelegramId(telegramUser.id)) {
|
|
|
|
|
|
logSecurityEvent('INVALID_TELEGRAM_ID', req, { telegramId: telegramUser.id });
|
2025-11-04 21:51:05 +00:00
|
|
|
|
return res.status(401).json({ error: 'Неверный ID пользователя' });
|
|
|
|
|
|
}
|
2025-11-10 21:56:36 +00:00
|
|
|
|
|
2025-12-01 01:02:46 +00:00
|
|
|
|
// Нормализовать данные пользователя (библиотека возвращает camelCase, но может быть и snake_case)
|
|
|
|
|
|
const normalizedUser = normalizeTelegramUser(telegramUser);
|
|
|
|
|
|
|
|
|
|
|
|
let user = await User.findOne({ telegramId: normalizedUser.id.toString() });
|
2025-11-10 22:37:25 +00:00
|
|
|
|
|
2025-11-03 20:35:01 +00:00
|
|
|
|
if (!user) {
|
2025-11-10 22:37:25 +00:00
|
|
|
|
user = new User({
|
2025-12-01 01:02:46 +00:00
|
|
|
|
telegramId: normalizedUser.id.toString(),
|
|
|
|
|
|
username: normalizedUser.username || normalizedUser.firstName || 'user',
|
|
|
|
|
|
firstName: normalizedUser.firstName,
|
|
|
|
|
|
lastName: normalizedUser.lastName,
|
|
|
|
|
|
photoUrl: normalizedUser.photoUrl
|
2025-11-10 22:37:25 +00:00
|
|
|
|
});
|
|
|
|
|
|
await user.save();
|
|
|
|
|
|
} else {
|
2025-12-01 00:51:23 +00:00
|
|
|
|
// Обновлять только если есть новые данные, не перезаписывать существующие пустыми значениями
|
2025-12-01 01:02:46 +00:00
|
|
|
|
if (normalizedUser.username) {
|
|
|
|
|
|
user.username = normalizedUser.username;
|
|
|
|
|
|
} else if (!user.username && normalizedUser.firstName) {
|
|
|
|
|
|
// Если username пустой, использовать firstName как fallback
|
|
|
|
|
|
user.username = normalizedUser.firstName;
|
2025-12-01 00:51:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-01 01:02:46 +00:00
|
|
|
|
if (normalizedUser.firstName) {
|
|
|
|
|
|
user.firstName = normalizedUser.firstName;
|
2025-12-01 00:51:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-01 01:02:46 +00:00
|
|
|
|
if (normalizedUser.lastName !== undefined) {
|
|
|
|
|
|
user.lastName = normalizedUser.lastName;
|
2025-12-01 00:51:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Обновлять аватарку только если есть новая
|
2025-12-01 01:02:46 +00:00
|
|
|
|
if (normalizedUser.photoUrl) {
|
|
|
|
|
|
user.photoUrl = normalizedUser.photoUrl;
|
2025-11-10 22:37:25 +00:00
|
|
|
|
}
|
2025-12-01 00:51:23 +00:00
|
|
|
|
|
2025-11-10 22:37:25 +00:00
|
|
|
|
await user.save();
|
2025-11-03 20:35:01 +00:00
|
|
|
|
}
|
2025-11-10 21:56:36 +00:00
|
|
|
|
|
|
|
|
|
|
if (user.banned) {
|
|
|
|
|
|
return res.status(403).json({ error: 'Пользователь заблокирован' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-01 01:02:46 +00:00
|
|
|
|
// Подтянуть отсутствующие данные из Telegram (используем нормализованные данные)
|
|
|
|
|
|
await ensureUserData(user, normalizedUser);
|
2025-11-10 20:13:22 +00:00
|
|
|
|
await ensureUserSettings(user);
|
|
|
|
|
|
await touchUserActivity(user);
|
2025-11-10 22:37:25 +00:00
|
|
|
|
|
2025-11-03 20:35:01 +00:00
|
|
|
|
req.user = user;
|
2025-12-01 01:02:46 +00:00
|
|
|
|
req.telegramUser = normalizedUser;
|
2025-11-03 20:35:01 +00:00
|
|
|
|
next();
|
|
|
|
|
|
} catch (error) {
|
2025-11-03 21:29:00 +00:00
|
|
|
|
console.error('❌ Ошибка авторизации:', error);
|
2025-11-10 21:22:58 +00:00
|
|
|
|
res.status(401).json({ error: `Ошибка авторизации. ${OFFICIAL_CLIENT_MESSAGE}` });
|
2025-11-03 20:35:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Middleware для проверки роли модератора
|
|
|
|
|
|
const requireModerator = (req, res, next) => {
|
|
|
|
|
|
if (req.user.role !== 'moderator' && req.user.role !== 'admin') {
|
|
|
|
|
|
return res.status(403).json({ error: 'Требуются права модератора' });
|
|
|
|
|
|
}
|
|
|
|
|
|
next();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Middleware для проверки роли админа
|
|
|
|
|
|
const requireAdmin = (req, res, next) => {
|
|
|
|
|
|
if (req.user.role !== 'admin') {
|
|
|
|
|
|
return res.status(403).json({ error: 'Требуются права администратора' });
|
|
|
|
|
|
}
|
|
|
|
|
|
next();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-10 23:11:33 +00:00
|
|
|
|
// Middleware для модерации (использует MODERATION_BOT_TOKEN)
|
|
|
|
|
|
const authenticateModeration = async (req, res, next) => {
|
|
|
|
|
|
const config = require('../config');
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const authHeader = req.headers.authorization || '';
|
|
|
|
|
|
let initDataRaw = null;
|
|
|
|
|
|
|
|
|
|
|
|
if (authHeader.startsWith('tma ')) {
|
|
|
|
|
|
initDataRaw = authHeader.slice(4).trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!initDataRaw) {
|
|
|
|
|
|
const headerInitData = req.headers['x-telegram-init-data'];
|
|
|
|
|
|
if (headerInitData && typeof headerInitData === 'string') {
|
|
|
|
|
|
initDataRaw = headerInitData.trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!initDataRaw) {
|
|
|
|
|
|
logSecurityEvent('AUTH_TOKEN_MISSING', req);
|
|
|
|
|
|
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let payload;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Use MODERATION_BOT_TOKEN for validation
|
|
|
|
|
|
payload = validateAndParseInitData(initDataRaw, config.moderationBotToken);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
logSecurityEvent('INVALID_INITDATA', req, { reason: error.message });
|
|
|
|
|
|
return res.status(401).json({ error: `${error.message}. ${OFFICIAL_CLIENT_MESSAGE}` });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const telegramUser = payload.user;
|
|
|
|
|
|
|
|
|
|
|
|
if (!validateTelegramId(telegramUser.id)) {
|
|
|
|
|
|
logSecurityEvent('INVALID_TELEGRAM_ID', req, { telegramId: telegramUser.id });
|
|
|
|
|
|
return res.status(401).json({ error: 'Неверный ID пользователя' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-01 01:02:46 +00:00
|
|
|
|
// Нормализовать данные пользователя (библиотека возвращает camelCase, но может быть и snake_case)
|
|
|
|
|
|
const normalizedUser = normalizeTelegramUser(telegramUser);
|
|
|
|
|
|
|
|
|
|
|
|
let user = await User.findOne({ telegramId: normalizedUser.id.toString() });
|
2025-11-10 23:11:33 +00:00
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
|
user = new User({
|
2025-12-01 01:02:46 +00:00
|
|
|
|
telegramId: normalizedUser.id.toString(),
|
|
|
|
|
|
username: normalizedUser.username || normalizedUser.firstName || 'user',
|
|
|
|
|
|
firstName: normalizedUser.firstName,
|
|
|
|
|
|
lastName: normalizedUser.lastName,
|
|
|
|
|
|
photoUrl: normalizedUser.photoUrl
|
2025-11-10 23:11:33 +00:00
|
|
|
|
});
|
|
|
|
|
|
await user.save();
|
|
|
|
|
|
} else {
|
2025-12-01 00:51:23 +00:00
|
|
|
|
// Обновлять только если есть новые данные, не перезаписывать существующие пустыми значениями
|
2025-12-01 01:02:46 +00:00
|
|
|
|
if (normalizedUser.username) {
|
|
|
|
|
|
user.username = normalizedUser.username;
|
|
|
|
|
|
} else if (!user.username && normalizedUser.firstName) {
|
|
|
|
|
|
// Если username пустой, использовать firstName как fallback
|
|
|
|
|
|
user.username = normalizedUser.firstName;
|
2025-12-01 00:51:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-01 01:02:46 +00:00
|
|
|
|
if (normalizedUser.firstName) {
|
|
|
|
|
|
user.firstName = normalizedUser.firstName;
|
2025-12-01 00:51:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-01 01:02:46 +00:00
|
|
|
|
if (normalizedUser.lastName !== undefined) {
|
|
|
|
|
|
user.lastName = normalizedUser.lastName;
|
2025-12-01 00:51:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Обновлять аватарку только если есть новая
|
2025-12-01 01:02:46 +00:00
|
|
|
|
if (normalizedUser.photoUrl) {
|
|
|
|
|
|
user.photoUrl = normalizedUser.photoUrl;
|
2025-11-10 23:11:33 +00:00
|
|
|
|
}
|
2025-12-01 00:51:23 +00:00
|
|
|
|
|
2025-11-10 23:11:33 +00:00
|
|
|
|
await user.save();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (user.banned) {
|
|
|
|
|
|
return res.status(403).json({ error: 'Пользователь заблокирован' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-01 01:02:46 +00:00
|
|
|
|
// Подтянуть отсутствующие данные из Telegram (используем нормализованные данные)
|
|
|
|
|
|
await ensureUserData(user, normalizedUser);
|
2025-11-10 23:11:33 +00:00
|
|
|
|
await ensureUserSettings(user);
|
|
|
|
|
|
await touchUserActivity(user);
|
|
|
|
|
|
|
|
|
|
|
|
req.user = user;
|
2025-12-01 01:02:46 +00:00
|
|
|
|
req.telegramUser = normalizedUser;
|
2025-11-10 23:11:33 +00:00
|
|
|
|
next();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('❌ Ошибка авторизации модерации:', error);
|
|
|
|
|
|
res.status(401).json({ error: `Ошибка авторизации. ${OFFICIAL_CLIENT_MESSAGE}` });
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-03 20:35:01 +00:00
|
|
|
|
module.exports = {
|
|
|
|
|
|
authenticate,
|
2025-11-10 23:11:33 +00:00
|
|
|
|
authenticateModeration,
|
2025-11-03 20:35:01 +00:00
|
|
|
|
requireModerator,
|
2025-11-10 21:56:36 +00:00
|
|
|
|
requireAdmin,
|
|
|
|
|
|
touchUserActivity,
|
2025-12-01 00:51:23 +00:00
|
|
|
|
ensureUserSettings,
|
|
|
|
|
|
ensureUserData
|
2025-11-03 20:35:01 +00:00
|
|
|
|
};
|