nakama/backend/middleware/auth.js

248 lines
8.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const crypto = require('crypto');
const User = require('../models/User');
const { validateTelegramId } = require('./validator');
const { logSecurityEvent } = require('./logger');
const config = require('../config');
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();
}
};
// Проверка 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'];
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) {
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
}
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: 'Ошибка авторизации' });
}
}
if (!initData) {
console.warn('⚠️ Нет x-telegram-init-data заголовка');
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
}
// Получаем user из initData
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}`);
} 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();
}
await ensureUserSettings(user);
await touchUserActivity(user);
req.user = user;
return next();
}
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
} catch (e2) {
console.error('❌ Ошибка парсинга initData:', e2.message);
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
}
}
const userParam = urlParams.get('user');
if (!userParam) {
console.warn('⚠️ Нет user параметра в initData');
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
}
let telegramUser;
try {
telegramUser = JSON.parse(userParam);
} catch (e) {
console.error('❌ Ошибка парсинга user:', e.message);
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
}
req.telegramUser = telegramUser;
// Валидация 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);
if (!isValid) {
logSecurityEvent('INVALID_TELEGRAM_SIGNATURE', req, {
telegramId: telegramUser.id,
hasToken: !!config.telegramBotToken
});
// В production строгая проверка
if (config.isProduction()) {
return res.status(401).json({ error: 'Неверные данные авторизации' });
}
}
} else if (config.isProduction()) {
logSecurityEvent('MISSING_BOT_TOKEN', req);
console.warn('⚠️ TELEGRAM_BOT_TOKEN не установлен, проверка подписи пропущена');
}
// Найти или создать пользователя
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();
console.log(`✅ Создан новый пользователь: ${user.username}`);
} 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();
}
await ensureUserSettings(user);
await touchUserActivity(user);
req.user = user;
next();
} catch (error) {
console.error('❌ Ошибка авторизации:', error);
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
};