nakama/backend/middleware/auth.js

320 lines
10 KiB
JavaScript
Raw Normal View History

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) {
user.settings.whitelist = { noNSFW: true };
updated = true;
} else if (user.settings.whitelist.noNSFW === undefined) {
user.settings.whitelist.noNSFW = true;
updated = true;
}
if (updated) {
user.markModified('settings');
await user.save();
}
};
2025-12-01 00:51:23 +00:00
// Подтянуть отсутствующие данные пользователя из Telegram
const ensureUserData = async (user, telegramUser) => {
if (!user || !telegramUser) return;
let updated = false;
// Обновить username, если отсутствует или пустой
if (!user.username || user.username.trim() === '') {
if (telegramUser.username) {
user.username = telegramUser.username;
updated = true;
} else if (telegramUser.first_name) {
user.username = telegramUser.first_name;
updated = true;
}
}
// Обновить firstName, если отсутствует
if (!user.firstName && telegramUser.first_name) {
user.firstName = telegramUser.first_name;
updated = true;
}
// Обновить lastName, если отсутствует
if (user.lastName === undefined || user.lastName === null) {
user.lastName = telegramUser.last_name || '';
updated = true;
}
// Обновить аватарку, если отсутствует
if (!user.photoUrl) {
// Сначала проверить photo_url из initData
if (telegramUser.photo_url) {
user.photoUrl = telegramUser.photo_url;
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-11-10 22:37:25 +00:00
let user = await User.findOne({ telegramId: telegramUser.id.toString() });
2025-11-03 20:35:01 +00:00
if (!user) {
2025-11-10 22:37:25 +00:00
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-10 22:37:25 +00:00
});
await user.save();
} 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) {
user.firstName = telegramUser.first_name;
}
if (telegramUser.last_name !== undefined) {
user.lastName = telegramUser.last_name || '';
}
// Обновлять аватарку только если есть новая
2025-11-10 22:37:25 +00:00
if (telegramUser.photo_url) {
user.photoUrl = telegramUser.photo_url;
}
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-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-11-10 22:37:25 +00:00
req.telegramUser = telegramUser;
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 пользователя' });
}
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-10 23:11:33 +00:00
});
await user.save();
} 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) {
user.firstName = telegramUser.first_name;
}
if (telegramUser.last_name !== undefined) {
user.lastName = telegramUser.last_name || '';
}
// Обновлять аватарку только если есть новая
2025-11-10 23:11:33 +00:00
if (telegramUser.photo_url) {
user.photoUrl = telegramUser.photo_url;
}
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 00:51:23 +00:00
// Подтянуть отсутствующие данные из Telegram
await ensureUserData(user, telegramUser);
2025-11-10 23:11:33 +00:00
await ensureUserSettings(user);
await touchUserActivity(user);
req.user = user;
req.telegramUser = telegramUser;
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
};