nakama/backend/routes/moderationAuth.js

432 lines
15 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 bcrypt = require('bcryptjs');
const User = require('../models/User');
const EmailVerificationCode = require('../models/EmailVerificationCode');
const { sendVerificationCode } = require('../utils/email');
const { signAuthTokens, setAuthCookies, clearAuthCookies, verifyAccessToken } = require('../utils/tokens');
const { logSecurityEvent } = require('../middleware/logger');
const { authenticateModeration } = require('../middleware/auth');
const { isEmail } = require('validator');
// Rate limiting для авторизации
const rateLimit = require('express-rate-limit');
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 минут
max: 5, // 5 попыток
message: 'Слишком много попыток авторизации. Попробуйте позже.'
});
const codeLimiter = rateLimit({
windowMs: 60 * 1000, // 1 минута
max: 1, // 1 запрос в минуту
message: 'Подождите минуту перед следующим запросом кода.'
});
// Отправка кода подтверждения на email
router.post('/send-code', codeLimiter, async (req, res) => {
try {
const { email } = req.body;
if (!email || !isEmail(email)) {
return res.status(400).json({ error: 'Неверный email адрес' });
}
// Проверить, есть ли уже пользователь с этим email (но только если он модератор/админ)
const existingUser = await User.findOne({
email: email.toLowerCase(),
role: { $in: ['moderator', 'admin'] }
});
if (!existingUser) {
return res.status(403).json({
error: 'Регистрация недоступна. Обратитесь к администратору для получения доступа.'
});
}
// Генерировать 6-значный код
const code = crypto.randomInt(100000, 999999).toString();
// Удалить старые коды для этого email
await EmailVerificationCode.deleteMany({
email: email.toLowerCase(),
purpose: 'registration'
});
// Сохранить новый код (действителен 15 минут)
const verificationCode = new EmailVerificationCode({
email: email.toLowerCase(),
code,
purpose: 'registration',
expiresAt: new Date(Date.now() + 15 * 60 * 1000) // 15 минут
});
await verificationCode.save();
// Отправить код на email
try {
await sendVerificationCode(email, code);
res.json({
success: true,
message: 'Код подтверждения отправлен на email'
});
} catch (emailError) {
console.error('Ошибка отправки email:', emailError);
await EmailVerificationCode.deleteOne({ _id: verificationCode._id });
return res.status(500).json({
error: 'Не удалось отправить код на email. Проверьте настройки email сервера.'
});
}
} catch (error) {
console.error('Ошибка отправки кода:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
// Регистрация с кодом подтверждения
router.post('/register', authLimiter, async (req, res) => {
try {
const { email, code, password, username } = req.body;
if (!email || !code || !password || !username) {
return res.status(400).json({ error: 'Все поля обязательны' });
}
if (!isEmail(email)) {
return res.status(400).json({ error: 'Неверный email адрес' });
}
if (password.length < 6) {
return res.status(400).json({ error: 'Пароль должен содержать минимум 6 символов' });
}
// Найти код подтверждения
const verificationCode = await EmailVerificationCode.findOne({
email: email.toLowerCase(),
code,
purpose: 'registration',
verified: false
});
if (!verificationCode) {
return res.status(400).json({ error: 'Неверный или истекший код' });
}
// Проверить срок действия
if (new Date() > verificationCode.expiresAt) {
await EmailVerificationCode.deleteOne({ _id: verificationCode._id });
return res.status(400).json({ error: 'Код истек. Запросите новый.' });
}
// Найти пользователя (должен быть создан администратором)
const user = await User.findOne({
email: email.toLowerCase(),
role: { $in: ['moderator', 'admin'] }
});
if (!user) {
return res.status(403).json({
error: 'Регистрация недоступна. Обратитесь к администратору.'
});
}
// Если у пользователя уже есть пароль - ошибка
if (user.passwordHash) {
return res.status(400).json({
error: 'Аккаунт уже зарегистрирован. Используйте вход по паролю.'
});
}
// Захешировать пароль
const passwordHash = await bcrypt.hash(password, 10);
// Обновить пользователя
user.passwordHash = passwordHash;
user.emailVerified = true;
user.username = username || user.username;
await user.save();
// Пометить код как использованный
verificationCode.verified = true;
await verificationCode.save();
// Генерировать токены
const tokens = signAuthTokens(user);
// Установить cookies
setAuthCookies(res, tokens);
res.json({
success: true,
user: {
id: user._id,
username: user.username,
role: user.role
},
accessToken: tokens.accessToken
});
} catch (error) {
console.error('Ошибка регистрации:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
// Авторизация по email и паролю
router.post('/login', authLimiter, async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email и пароль обязательны' });
}
if (!isEmail(email)) {
return res.status(400).json({ error: 'Неверный email адрес' });
}
// Найти пользователя с паролем (только модераторы и админы)
const user = await User.findOne({
email: email.toLowerCase(),
passwordHash: { $exists: true, $ne: null },
role: { $in: ['moderator', 'admin'] }
}).select('+passwordHash');
if (!user) {
logSecurityEvent('MODERATION_LOGIN_FAILED', req, { email: email.toLowerCase() });
return res.status(401).json({ error: 'Неверный email или пароль' });
}
if (user.banned) {
return res.status(403).json({ error: 'Аккаунт заблокирован' });
}
// Проверить пароль
const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
if (!isPasswordValid) {
logSecurityEvent('MODERATION_LOGIN_FAILED', req, { email: email.toLowerCase(), userId: user._id });
return res.status(401).json({ error: 'Неверный email или пароль' });
}
// Обновить время последней активности
user.lastActiveAt = new Date();
await user.save();
// Генерировать токены
const tokens = signAuthTokens(user);
// Установить cookies
setAuthCookies(res, tokens);
logSecurityEvent('MODERATION_LOGIN_SUCCESS', req, { userId: user._id, email: user.email });
// Email не возвращаем в ответе для безопасности
res.json({
success: true,
user: {
id: user._id,
username: user.username,
role: user.role,
telegramId: user.telegramId
},
accessToken: tokens.accessToken
});
} catch (error) {
console.error('Ошибка авторизации:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
// Авторизация через Telegram Login Widget (для обычного браузера)
router.post('/telegram-widget', authLimiter, async (req, res) => {
try {
const { id, first_name, last_name, username, photo_url, auth_date, hash } = req.body;
if (!id || !hash || !auth_date) {
return res.status(400).json({ error: 'Неполные данные от Telegram Login Widget' });
}
// Проверить подпись (базовая проверка)
// В production нужно проверить hash через Bot API
// Для модерации используем упрощенную проверку - ищем пользователя по telegramId
const user = await User.findOne({ telegramId: id.toString() });
if (!user) {
return res.status(404).json({ error: 'Пользователь не найден. Сначала зарегистрируйтесь через бота.' });
}
if (!['moderator', 'admin'].includes(user.role)) {
return res.status(403).json({ error: 'Доступ запрещен. У вас нет прав модератора.' });
}
if (user.banned) {
return res.status(403).json({ error: 'Аккаунт заблокирован' });
}
// Обновить данные пользователя из виджета
if (username && !user.username) {
user.username = username;
}
if (first_name && !user.firstName) {
user.firstName = first_name;
}
if (last_name && !user.lastName) {
user.lastName = last_name;
}
if (photo_url && !user.photoUrl) {
user.photoUrl = photo_url;
}
user.lastActiveAt = new Date();
await user.save();
// Генерировать JWT токены
const tokens = signAuthTokens(user);
setAuthCookies(res, tokens);
logSecurityEvent('MODERATION_TELEGRAM_WIDGET_LOGIN_SUCCESS', req, { userId: user._id });
res.json({
success: true,
user: {
id: user._id,
username: user.username,
role: user.role,
telegramId: user.telegramId
},
accessToken: tokens.accessToken
});
} catch (error) {
console.error('Ошибка авторизации через Telegram Widget:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
// Авторизация через Telegram (для модерации)
router.post('/telegram', authLimiter, authenticateModeration, async (req, res) => {
try {
const user = req.user;
const { isModerationAdmin } = require('../services/moderationAdmin');
const { normalizeUsername } = require('../services/moderationAdmin');
const config = require('../config');
if (!user) {
return res.status(403).json({ error: 'Доступ запрещен' });
}
if (user.banned) {
return res.status(403).json({ error: 'Аккаунт заблокирован' });
}
// Проверить доступ: роль admin/moderator ИЛИ является модератором через ModerationAdmin
const username = normalizeUsername(user.username);
const telegramId = user.telegramId;
const OWNER_USERNAMES = new Set(config.moderationOwnerUsernames || []);
const isOwner = OWNER_USERNAMES.has(username);
const isAdminByRole = ['moderator', 'admin'].includes(user.role);
const isAdminByDB = await isModerationAdmin({ telegramId, username });
if (!isOwner && !isAdminByRole && !isAdminByDB) {
return res.status(403).json({
error: 'Доступ запрещен. У вас нет прав модератора. Обратитесь к администратору.'
});
}
// Обновить время последней активности
user.lastActiveAt = new Date();
await user.save();
// Генерировать токены
const tokens = signAuthTokens(user);
// Установить cookies
setAuthCookies(res, tokens);
logSecurityEvent('MODERATION_TELEGRAM_LOGIN_SUCCESS', req, { userId: user._id });
// Email не возвращаем в ответе для безопасности
res.json({
success: true,
user: {
id: user._id,
username: user.username,
role: user.role,
telegramId: user.telegramId
},
accessToken: tokens.accessToken
});
} catch (error) {
console.error('Ошибка авторизации через Telegram:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
// Выход
router.post('/logout', (req, res) => {
clearAuthCookies(res);
res.json({ success: true });
});
// Проверка текущей сессии
router.get('/me', async (req, res) => {
try {
// Получить токен из заголовка или cookie
let token = null;
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
token = authHeader.slice(7);
} else if (req.cookies && req.cookies[require('../utils/tokens').ACCESS_COOKIE]) {
token = req.cookies[require('../utils/tokens').ACCESS_COOKIE];
}
if (!token) {
return res.status(401).json({ error: 'Не авторизован' });
}
// Проверить токен
let payload;
try {
payload = verifyAccessToken(token);
} catch (error) {
return res.status(401).json({ error: 'Неверный токен' });
}
// Найти пользователя
const user = await User.findById(payload.userId);
if (!user) {
return res.status(401).json({ error: 'Пользователь не найден' });
}
if (user.banned) {
return res.status(403).json({ error: 'Аккаунт заблокирован' });
}
// Проверить роль (только модераторы и админы)
if (!['moderator', 'admin'].includes(user.role)) {
return res.status(403).json({ error: 'Доступ запрещен' });
}
res.json({
success: true,
user: {
id: user._id,
username: user.username,
role: user.role,
telegramId: user.telegramId
}
});
} catch (error) {
console.error('Ошибка проверки сессии:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
module.exports = router;