nakama/backend/routes/auth.js

357 lines
12 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 express = require('express');
const router = express.Router();
const crypto = require('crypto');
const User = require('../models/User');
const config = require('../config');
const { validateTelegramId } = require('../middleware/validator');
const { logSecurityEvent } = require('../middleware/logger');
const { strictAuthLimiter } = require('../middleware/security');
const { validateAndParseInitData } = require('../utils/telegram');
const { signAuthTokens, setAuthCookies, clearAuthCookies } = require('../utils/tokens');
const { touchUserActivity, ensureUserSettings } = require('../middleware/auth');
const ALLOWED_SEARCH_PREFERENCES = ['furry', 'anime'];
const normalizeUserSettings = (settings = {}) => {
const plainSettings = typeof settings.toObject === 'function' ? settings.toObject() : { ...settings };
const whitelistSource = plainSettings.whitelist;
const whitelist =
whitelistSource && typeof whitelistSource.toObject === 'function'
? whitelistSource.toObject()
: { ...(whitelistSource || {}) };
return {
...plainSettings,
whitelist: {
noNSFW: whitelist?.noNSFW ?? true,
...whitelist
},
searchPreference: ALLOWED_SEARCH_PREFERENCES.includes(plainSettings.searchPreference)
? plainSettings.searchPreference
: 'furry'
};
};
const OFFICIAL_CLIENT_MESSAGE = 'Используйте официальный клиент. Сообщите об ошибке в https://t.me/NakamaReportbot';
router.post('/signin', strictAuthLimiter, async (req, res) => {
try {
const { initData } = req.body || {};
if (!initData || typeof initData !== 'string') {
return res.status(400).json({ error: 'initData обязателен' });
}
let telegramUser;
try {
({ telegramUser } = validateAndParseInitData(initData, req));
} catch (error) {
return res.status(401).json({ error: error.message });
}
if (!validateTelegramId(telegramUser.id)) {
logSecurityEvent('INVALID_TELEGRAM_ID', req, { telegramId: telegramUser.id });
return res.status(400).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();
} 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();
}
if (user.banned) {
return res.status(403).json({ error: 'Пользователь заблокирован' });
}
await ensureUserSettings(user);
await touchUserActivity(user);
const tokens = signAuthTokens(user);
setAuthCookies(res, tokens);
const populatedUser = await user.populate([
{ path: 'followers', select: 'username firstName lastName photoUrl' },
{ path: 'following', select: 'username firstName lastName photoUrl' }
]);
const settings = normalizeUserSettings(populatedUser.settings);
return res.json({
success: true,
user: {
id: populatedUser._id,
telegramId: populatedUser.telegramId,
username: populatedUser.username,
firstName: populatedUser.firstName,
lastName: populatedUser.lastName,
photoUrl: populatedUser.photoUrl,
bio: populatedUser.bio,
role: populatedUser.role,
followersCount: populatedUser.followers.length,
followingCount: populatedUser.following.length,
settings,
banned: populatedUser.banned
}
});
} catch (error) {
console.error('Ошибка signin:', error);
res.status(500).json({ error: 'Ошибка авторизации' });
}
});
router.post('/logout', (req, res) => {
clearAuthCookies(res);
res.json({ success: true });
});
// Проверка подписи Telegram OAuth (Login Widget)
function validateTelegramOAuth(authData, botToken) {
if (!authData || !authData.hash) {
return false;
}
const { hash, ...data } = authData;
// Удалить поля с undefined/null значениями (они не должны быть в dataCheckString)
const cleanData = {};
for (const key in data) {
if (data[key] !== undefined && data[key] !== null && data[key] !== '') {
cleanData[key] = data[key];
}
}
// Формировать dataCheckString из очищенных данных
const dataCheckString = Object.keys(cleanData)
.sort()
.map(key => `${key}=${cleanData[key]}`)
.join('\n');
const secretKey = crypto
.createHmac('sha256', 'WebAppData')
.update(botToken)
.digest();
const calculatedHash = crypto
.createHmac('sha256', secretKey)
.update(dataCheckString)
.digest('hex');
return calculatedHash === hash;
}
// Авторизация через Telegram OAuth (Login Widget)
router.post('/oauth', strictAuthLimiter, async (req, res) => {
try {
const { user: telegramUser, auth_date, hash } = req.body;
if (!telegramUser || !auth_date || !hash) {
logSecurityEvent('INVALID_OAUTH_DATA', req);
return res.status(400).json({ error: 'Неверные данные авторизации' });
}
// Валидация Telegram ID
if (!validateTelegramId(telegramUser.id)) {
logSecurityEvent('INVALID_TELEGRAM_ID', req, { telegramId: telegramUser.id });
return res.status(400).json({ error: 'Неверный ID пользователя' });
}
// Проверка подписи Telegram (строгая проверка в production)
if (config.telegramBotToken) {
// Формировать authData только с присутствующими полями
const authData = {
id: telegramUser.id,
first_name: telegramUser.first_name || '',
auth_date: auth_date.toString(),
hash: hash
};
// Добавить опциональные поля только если они присутствуют
if (telegramUser.last_name) {
authData.last_name = telegramUser.last_name;
}
if (telegramUser.username) {
authData.username = telegramUser.username;
}
if (telegramUser.photo_url) {
authData.photo_url = telegramUser.photo_url;
}
const isValid = validateTelegramOAuth(authData, config.telegramBotToken);
if (!isValid) {
logSecurityEvent('INVALID_OAUTH_SIGNATURE', req, {
telegramId: telegramUser.id,
receivedData: {
id: telegramUser.id,
first_name: telegramUser.first_name,
last_name: telegramUser.last_name,
username: telegramUser.username,
auth_date: auth_date
}
});
// В production строгая проверка, но для отладки можно временно отключить
if (config.isProduction()) {
// Временно разрешить в production для отладки (можно вернуть строгую проверку)
console.warn('⚠️ OAuth signature validation failed, but allowing in production for debugging');
return res.status(401).json({ error: 'Неверная подпись Telegram OAuth' });
}
}
}
// Найти или создать пользователя
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(`✅ Создан новый пользователь через OAuth: ${user.username}`);
} else {
// Обновить данные пользователя
user.username = telegramUser.username || telegramUser.first_name;
user.firstName = telegramUser.first_name;
user.lastName = telegramUser.last_name;
user.photoUrl = telegramUser.photo_url;
await user.save();
}
// Получить полные данные пользователя
const populatedUser = await User.findById(user._id).populate([
{ path: 'followers', select: 'username firstName lastName photoUrl' },
{ path: 'following', select: 'username firstName lastName photoUrl' }
]);
const settings = normalizeUserSettings(populatedUser.settings);
res.json({
success: true,
user: {
id: populatedUser._id,
telegramId: populatedUser.telegramId,
username: populatedUser.username,
firstName: populatedUser.firstName,
lastName: populatedUser.lastName,
photoUrl: populatedUser.photoUrl,
bio: populatedUser.bio,
role: populatedUser.role,
followersCount: populatedUser.followers.length,
followingCount: populatedUser.following.length,
settings,
banned: populatedUser.banned
}
});
} catch (error) {
console.error('Ошибка OAuth:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
// Проверка авторизации и получение данных пользователя
const { authenticate } = require('../middleware/auth');
router.post('/verify', authenticate, async (req, res) => {
try {
const user = await req.user.populate([
{ path: 'followers', select: 'username firstName lastName photoUrl' },
{ path: 'following', select: 'username firstName lastName photoUrl' }
]);
const settings = normalizeUserSettings(user.settings);
res.json({
success: true,
user: {
id: user._id,
telegramId: user.telegramId,
username: user.username,
firstName: user.firstName,
lastName: user.lastName,
photoUrl: user.photoUrl,
bio: user.bio,
role: user.role,
followersCount: user.followers.length,
followingCount: user.following.length,
settings,
banned: user.banned
}
});
} catch (error) {
console.error('Ошибка verify:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
// Проверка сохраненной сессии по telegramId (для OAuth пользователей)
router.post('/session', async (req, res) => {
try {
const { telegramId } = req.body;
if (!telegramId) {
return res.status(400).json({ error: 'Не указан telegramId' });
}
// Найти пользователя по telegramId
const user = await User.findOne({ telegramId: telegramId.toString() });
if (!user) {
return res.status(404).json({ error: OFFICIAL_CLIENT_MESSAGE });
}
if (user.banned) {
return res.status(403).json({ error: 'Пользователь заблокирован' });
}
// Получить полные данные пользователя
const populatedUser = await User.findById(user._id).populate([
{ path: 'followers', select: 'username firstName lastName photoUrl' },
{ path: 'following', select: 'username firstName lastName photoUrl' }
]);
res.json({
success: true,
user: {
id: populatedUser._id,
telegramId: populatedUser.telegramId,
username: populatedUser.username,
firstName: populatedUser.firstName,
lastName: populatedUser.lastName,
photoUrl: populatedUser.photoUrl,
bio: populatedUser.bio,
role: populatedUser.role,
followersCount: populatedUser.followers.length,
followingCount: populatedUser.following.length,
settings,
banned: populatedUser.banned
}
});
} catch (error) {
console.error('Ошибка проверки сессии:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
module.exports = router;