nakama/backend/middleware/auth.js

198 lines
6.2 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 MAX_AUTH_AGE_SECONDS = 5 * 60; // 5 минут
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();
}
};
function validateTelegramWebAppData(initData, botToken) {
if (!botToken) {
throw new Error('TELEGRAM_BOT_TOKEN не настроен');
}
const params = new URLSearchParams(initData);
const hash = params.get('hash');
const authDate = Number(params.get('auth_date'));
if (!hash) {
throw new Error('Отсутствует hash в initData');
}
if (!authDate) {
throw new Error('Отсутствует auth_date в initData');
}
const dataCheck = [];
for (const [key, value] of params.entries()) {
if (key === 'hash') continue;
dataCheck.push(`${key}=${value}`);
}
dataCheck.sort((a, b) => a.localeCompare(b));
const dataCheckString = dataCheck.join('\n');
const secretKey = crypto.createHmac('sha256', 'WebAppData').update(botToken).digest();
const calculatedHash = crypto.createHmac('sha256', secretKey).update(dataCheckString).digest('hex');
if (calculatedHash !== hash) {
throw new Error('Неверная подпись initData');
}
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - authDate) > MAX_AUTH_AGE_SECONDS) {
throw new Error('Данные авторизации устарели');
}
return params;
}
// Middleware для проверки авторизации
const authenticate = async (req, res, next) => {
try {
const initData = req.headers['x-telegram-init-data'];
if (!initData) {
logSecurityEvent('MISSING_INITDATA', req);
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
}
if (!config.telegramBotToken) {
logSecurityEvent('MISSING_BOT_TOKEN', req);
if (config.isProduction()) {
return res.status(500).json({ error: 'Сервер некорректно настроен для авторизации через Telegram' });
}
console.warn('⚠️ TELEGRAM_BOT_TOKEN не установлен, пропускаем проверку подписи (dev режим)');
}
let params;
try {
if (config.telegramBotToken) {
params = validateTelegramWebAppData(initData, config.telegramBotToken);
} else {
params = new URLSearchParams(initData);
}
} catch (validationError) {
logSecurityEvent('INVALID_INITDATA_SIGNATURE', req, { reason: validationError.message });
return res.status(401).json({ error: `${validationError.message}. ${OFFICIAL_CLIENT_MESSAGE}` });
}
const userParam = params.get('user');
if (!userParam) {
logSecurityEvent('MISSING_INITDATA_USER', req);
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
}
let telegramUser;
try {
telegramUser = JSON.parse(userParam);
} catch (parseError) {
logSecurityEvent('INVALID_INITDATA_USER_JSON', req, { error: parseError.message });
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
}
req.telegramUser = telegramUser;
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(),
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: `Ошибка авторизации. ${OFFICIAL_CLIENT_MESSAGE}` });
}
};
// 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
};