nakama/backend/middleware/auth.js

320 lines
10 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 User = require('../models/User');
const { validateTelegramId } = require('./validator');
const { logSecurityEvent } = require('./logger');
const { validateAndParseInitData } = require('../utils/telegram');
const { fetchLatestAvatar } = require('../jobs/avatarUpdater');
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
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();
}
};
const authenticate = async (req, res, next) => {
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 });
}
if (!initDataRaw) {
logSecurityEvent('EMPTY_INITDATA', req);
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
}
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 });
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 || 'user',
firstName: telegramUser.first_name || '',
lastName: telegramUser.last_name || '',
photoUrl: telegramUser.photo_url || null
});
await user.save();
} else {
// Обновлять только если есть новые данные, не перезаписывать существующие пустыми значениями
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 || '';
}
// Обновлять аватарку только если есть новая
if (telegramUser.photo_url) {
user.photoUrl = telegramUser.photo_url;
}
await user.save();
}
if (user.banned) {
return res.status(403).json({ error: 'Пользователь заблокирован' });
}
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}` });
}
};
// 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();
};
// 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(),
username: telegramUser.username || telegramUser.first_name || 'user',
firstName: telegramUser.first_name || '',
lastName: telegramUser.last_name || '',
photoUrl: telegramUser.photo_url || null
});
await user.save();
} else {
// Обновлять только если есть новые данные, не перезаписывать существующие пустыми значениями
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 || '';
}
// Обновлять аватарку только если есть новая
if (telegramUser.photo_url) {
user.photoUrl = telegramUser.photo_url;
}
await user.save();
}
if (user.banned) {
return res.status(403).json({ error: 'Пользователь заблокирован' });
}
// Подтянуть отсутствующие данные из Telegram
await ensureUserData(user, telegramUser);
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}` });
}
};
module.exports = {
authenticate,
authenticateModeration,
requireModerator,
requireAdmin,
touchUserActivity,
ensureUserSettings,
ensureUserData
};