nakama/backend/middleware/auth.js

248 lines
8.3 KiB
JavaScript
Raw Normal View History

2025-11-03 20:35:01 +00:00
const crypto = require('crypto');
const User = require('../models/User');
2025-11-04 21:51:05 +00:00
const { validateTelegramId } = require('./validator');
const { logSecurityEvent } = require('./logger');
const config = require('../config');
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-11-03 20:35:01 +00:00
// Проверка Telegram Init Data
function validateTelegramWebAppData(initData, botToken) {
const urlParams = new URLSearchParams(initData);
const hash = urlParams.get('hash');
urlParams.delete('hash');
const dataCheckString = Array.from(urlParams.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${key}=${value}`)
.join('\n');
const secretKey = crypto
.createHmac('sha256', 'WebAppData')
.update(botToken)
.digest();
const calculatedHash = crypto
.createHmac('sha256', secretKey)
.update(dataCheckString)
.digest('hex');
return calculatedHash === hash;
}
// Middleware для проверки авторизации
const authenticate = async (req, res, next) => {
try {
const initData = req.headers['x-telegram-init-data'];
2025-11-04 22:41:35 +00:00
const telegramUserId = req.headers['x-telegram-user-id'];
// Если нет initData, но есть telegramUserId (сохраненная OAuth сессия)
if (!initData && telegramUserId) {
try {
// Найти пользователя по telegramId
const user = await User.findOne({ telegramId: telegramUserId.toString() });
if (!user) {
2025-11-10 20:13:22 +00:00
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
2025-11-04 22:41:35 +00:00
}
if (user.banned) {
return res.status(403).json({ error: 'Пользователь заблокирован' });
}
req.user = user;
req.telegramUser = { id: user.telegramId };
return next();
} catch (error) {
console.error('❌ Ошибка авторизации по сохраненной сессии:', error);
return res.status(401).json({ error: 'Ошибка авторизации' });
}
}
2025-11-03 20:35:01 +00:00
if (!initData) {
2025-11-03 22:41:34 +00:00
console.warn('⚠️ Нет x-telegram-init-data заголовка');
2025-11-10 20:13:22 +00:00
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
2025-11-03 20:35:01 +00:00
}
2025-11-03 21:29:00 +00:00
// Получаем user из initData
2025-11-03 22:41:34 +00:00
let urlParams;
try {
urlParams = new URLSearchParams(initData);
} catch (e) {
// Если initData не URLSearchParams, попробуем как JSON
try {
const parsed = JSON.parse(initData);
if (parsed.user) {
req.telegramUser = parsed.user;
// Найти или создать пользователя
let user = await User.findOne({ telegramId: parsed.user.id.toString() });
if (!user) {
user = new User({
telegramId: parsed.user.id.toString(),
username: parsed.user.username || parsed.user.first_name,
firstName: parsed.user.first_name,
lastName: parsed.user.last_name,
photoUrl: parsed.user.photo_url
});
await user.save();
console.log(`✅ Создан новый пользователь: ${user.username}`);
2025-11-10 20:13:22 +00:00
} else {
user.username = parsed.user.username || parsed.user.first_name;
user.firstName = parsed.user.first_name;
user.lastName = parsed.user.last_name;
if (parsed.user.photo_url) {
user.photoUrl = parsed.user.photo_url;
}
await user.save();
2025-11-03 22:41:34 +00:00
}
2025-11-10 20:13:22 +00:00
await ensureUserSettings(user);
await touchUserActivity(user);
2025-11-03 22:41:34 +00:00
req.user = user;
return next();
}
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
} catch (e2) {
console.error('❌ Ошибка парсинга initData:', e2.message);
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-03 21:29:00 +00:00
const userParam = urlParams.get('user');
2025-11-03 20:35:01 +00:00
2025-11-03 21:29:00 +00:00
if (!userParam) {
2025-11-03 22:41:34 +00:00
console.warn('⚠️ Нет user параметра в initData');
2025-11-10 20:13:22 +00:00
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
2025-11-03 20:35:01 +00:00
}
2025-11-03 22:41:34 +00:00
let telegramUser;
try {
telegramUser = JSON.parse(userParam);
} catch (e) {
console.error('❌ Ошибка парсинга user:', e.message);
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-03 20:35:01 +00:00
req.telegramUser = telegramUser;
2025-11-04 21:51:05 +00:00
// Валидация Telegram ID
if (!validateTelegramId(telegramUser.id)) {
logSecurityEvent('INVALID_TELEGRAM_ID', req, { telegramId: telegramUser.id });
return res.status(401).json({ error: 'Неверный ID пользователя' });
}
// Проверка подписи Telegram (строгая проверка в production)
if (config.telegramBotToken) {
const isValid = validateTelegramWebAppData(initData, config.telegramBotToken);
2025-11-03 21:29:00 +00:00
if (!isValid) {
2025-11-04 21:51:05 +00:00
logSecurityEvent('INVALID_TELEGRAM_SIGNATURE', req, {
telegramId: telegramUser.id,
hasToken: !!config.telegramBotToken
});
// В production строгая проверка
if (config.isProduction()) {
return res.status(401).json({ error: 'Неверные данные авторизации' });
}
2025-11-03 21:29:00 +00:00
}
2025-11-04 21:51:05 +00:00
} else if (config.isProduction()) {
logSecurityEvent('MISSING_BOT_TOKEN', req);
2025-11-03 21:29:00 +00:00
console.warn('⚠️ TELEGRAM_BOT_TOKEN не установлен, проверка подписи пропущена');
}
2025-11-03 20:35:01 +00:00
// Найти или создать пользователя
let user = await User.findOne({ telegramId: telegramUser.id.toString() });
if (!user) {
user = new User({
telegramId: telegramUser.id.toString(),
username: telegramUser.username || telegramUser.first_name,
firstName: telegramUser.first_name,
lastName: telegramUser.last_name,
photoUrl: telegramUser.photo_url
});
await user.save();
2025-11-03 21:29:00 +00:00
console.log(`✅ Создан новый пользователь: ${user.username}`);
2025-11-10 20:13:22 +00:00
} else {
user.username = telegramUser.username || telegramUser.first_name;
user.firstName = telegramUser.first_name;
user.lastName = telegramUser.last_name;
if (telegramUser.photo_url) {
user.photoUrl = telegramUser.photo_url;
}
await user.save();
2025-11-03 20:35:01 +00:00
}
2025-11-10 20:13:22 +00:00
await ensureUserSettings(user);
await touchUserActivity(user);
2025-11-03 20:35:01 +00:00
req.user = user;
next();
} catch (error) {
2025-11-03 21:29:00 +00:00
console.error('❌ Ошибка авторизации:', error);
2025-11-03 20:35:01 +00:00
res.status(401).json({ error: 'Ошибка авторизации' });
}
};
// 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();
};
module.exports = {
authenticate,
requireModerator,
requireAdmin
};