432 lines
15 KiB
JavaScript
432 lines
15 KiB
JavaScript
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;
|
||
|