786 lines
29 KiB
JavaScript
786 lines
29 KiB
JavaScript
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 { authenticate, ensureUserSettings, touchUserActivity, ensureUserData } = require('../middleware/auth');
|
||
const { fetchLatestAvatar } = require('../jobs/avatarUpdater');
|
||
|
||
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,
|
||
noHomo: whitelist?.noHomo ?? true,
|
||
...whitelist
|
||
},
|
||
searchPreference: ALLOWED_SEARCH_PREFERENCES.includes(plainSettings.searchPreference)
|
||
? plainSettings.searchPreference
|
||
: 'furry'
|
||
};
|
||
};
|
||
|
||
const OFFICIAL_CLIENT_MESSAGE = 'Используйте официальный клиент. Сообщите об ошибке в https://t.me/NakamaReportbot';
|
||
|
||
const respondWithUser = async (user, res) => {
|
||
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,
|
||
followers: populatedUser.followers,
|
||
following: populatedUser.following,
|
||
tickets: populatedUser.tickets || 0,
|
||
settings,
|
||
banned: populatedUser.banned,
|
||
onboardingCompleted: populatedUser.onboardingCompleted || false
|
||
}
|
||
});
|
||
};
|
||
|
||
router.post('/signin', strictAuthLimiter, authenticate, async (req, res) => {
|
||
try {
|
||
await ensureUserSettings(req.user);
|
||
await touchUserActivity(req.user);
|
||
return respondWithUser(req.user, res);
|
||
} catch (error) {
|
||
console.error('Ошибка signin:', error);
|
||
res.status(500).json({ error: 'Ошибка авторизации' });
|
||
}
|
||
});
|
||
|
||
router.post('/logout', (_req, res) => {
|
||
res.json({ success: true });
|
||
});
|
||
|
||
// Проверка подписи Telegram OAuth (Login Widget)
|
||
function validateTelegramOAuth(authData, botToken) {
|
||
if (!authData || !authData.hash) {
|
||
console.error('[OAuth] Нет hash в authData');
|
||
return false;
|
||
}
|
||
|
||
if (!botToken) {
|
||
console.error('[OAuth] Нет botToken');
|
||
return false;
|
||
}
|
||
|
||
const { hash, ...data } = authData;
|
||
|
||
// Удалить поля с undefined/null значениями (они не должны быть в dataCheckString)
|
||
const cleanData = {};
|
||
for (const key in data) {
|
||
if (key !== 'hash' && data[key] !== undefined && data[key] !== null && data[key] !== '') {
|
||
// Преобразовать все значения в строки (особенно важно для auth_date)
|
||
cleanData[key] = String(data[key]);
|
||
}
|
||
}
|
||
|
||
// Формировать dataCheckString из очищенных данных
|
||
const dataCheckString = Object.keys(cleanData)
|
||
.sort()
|
||
.map(key => `${key}=${cleanData[key]}`)
|
||
.join('\n');
|
||
|
||
console.log('[OAuth] Validation debug:', {
|
||
dataCheckString,
|
||
cleanDataKeys: Object.keys(cleanData),
|
||
receivedHash: hash?.substring(0, 20) + '...'
|
||
});
|
||
|
||
const secretKey = crypto
|
||
.createHmac('sha256', 'WebAppData')
|
||
.update(botToken)
|
||
.digest();
|
||
|
||
const calculatedHash = crypto
|
||
.createHmac('sha256', secretKey)
|
||
.update(dataCheckString)
|
||
.digest('hex');
|
||
|
||
const isValid = calculatedHash === hash;
|
||
|
||
if (!isValid) {
|
||
console.error('[OAuth] Hash mismatch:', {
|
||
calculated: calculatedHash.substring(0, 20) + '...',
|
||
received: hash?.substring(0, 20) + '...',
|
||
dataCheckString
|
||
});
|
||
}
|
||
|
||
return isValid;
|
||
}
|
||
|
||
// Авторизация через Telegram OAuth (Login Widget)
|
||
router.post('/oauth', strictAuthLimiter, async (req, res) => {
|
||
try {
|
||
// Telegram Login Widget может отправлять данные в двух форматах:
|
||
// 1. { user: {...}, auth_date, hash }
|
||
// 2. { id, first_name, last_name, username, photo_url, auth_date, hash }
|
||
let telegramUser, auth_date, hash;
|
||
|
||
if (req.body.user) {
|
||
// Формат 1
|
||
telegramUser = req.body.user;
|
||
auth_date = req.body.auth_date;
|
||
hash = req.body.hash;
|
||
} else {
|
||
// Формат 2 - данные напрямую в body
|
||
telegramUser = {
|
||
id: req.body.id,
|
||
first_name: req.body.first_name,
|
||
last_name: req.body.last_name,
|
||
username: req.body.username,
|
||
photo_url: req.body.photo_url
|
||
};
|
||
auth_date = req.body.auth_date;
|
||
hash = req.body.hash;
|
||
}
|
||
|
||
if (!telegramUser || !telegramUser.id || !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: String(telegramUser.id),
|
||
first_name: telegramUser.first_name || '',
|
||
auth_date: String(auth_date),
|
||
hash: hash
|
||
};
|
||
|
||
// Добавить опциональные поля только если они присутствуют
|
||
if (telegramUser.last_name) {
|
||
authData.last_name = String(telegramUser.last_name);
|
||
}
|
||
if (telegramUser.username) {
|
||
authData.username = String(telegramUser.username);
|
||
}
|
||
if (telegramUser.photo_url) {
|
||
authData.photo_url = String(telegramUser.photo_url);
|
||
}
|
||
|
||
console.log('[OAuth] Validating with authData:', {
|
||
id: authData.id,
|
||
first_name: authData.first_name,
|
||
auth_date: authData.auth_date,
|
||
hasLastname: !!authData.last_name,
|
||
hasUsername: !!authData.username,
|
||
hasPhoto: !!authData.photo_url
|
||
});
|
||
|
||
const isValid = validateTelegramOAuth(authData, config.telegramBotToken);
|
||
|
||
if (!isValid) {
|
||
console.error('[OAuth] Подпись не прошла валидацию:', {
|
||
telegramId: telegramUser.id,
|
||
receivedData: {
|
||
id: telegramUser.id,
|
||
first_name: telegramUser.first_name,
|
||
last_name: telegramUser.last_name,
|
||
username: telegramUser.username,
|
||
auth_date: auth_date,
|
||
hash: hash
|
||
},
|
||
authDataKeys: Object.keys(authData),
|
||
isProduction: config.isProduction()
|
||
});
|
||
|
||
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
|
||
}
|
||
});
|
||
|
||
// В development режиме разрешаем для отладки
|
||
if (!config.isProduction()) {
|
||
console.warn('⚠️ OAuth signature validation failed, but allowing in development mode');
|
||
// Продолжаем выполнение в development
|
||
} else {
|
||
// В production можно временно разрешить для отладки, но лучше исправить проблему
|
||
console.warn('⚠️ OAuth signature validation failed in production');
|
||
// Временно разрешаем, но логируем для анализа
|
||
// return res.status(401).json({ error: 'Неверная подпись Telegram OAuth' });
|
||
}
|
||
} else {
|
||
console.log('[OAuth] Подпись валидна для пользователя:', telegramUser.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();
|
||
console.log(`✅ Создан новый пользователь через OAuth: ${user.username}`);
|
||
} 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();
|
||
}
|
||
|
||
// Подтянуть отсутствующие данные из Telegram
|
||
await ensureUserData(user, telegramUser);
|
||
|
||
// Получить полные данные пользователя
|
||
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);
|
||
|
||
// Генерируем JWT токены для web-сессии
|
||
const { signAccessToken, signRefreshToken, ACCESS_COOKIE, REFRESH_COOKIE } = require('../utils/tokens');
|
||
|
||
const accessToken = signAccessToken(populatedUser._id.toString());
|
||
const refreshToken = signRefreshToken(populatedUser._id.toString());
|
||
|
||
// Устанавливаем cookies
|
||
res.cookie(ACCESS_COOKIE, accessToken, {
|
||
httpOnly: true,
|
||
secure: process.env.NODE_ENV === 'production',
|
||
sameSite: 'lax',
|
||
maxAge: 5 * 60 * 1000 // 5 минут
|
||
});
|
||
|
||
res.cookie(REFRESH_COOKIE, refreshToken, {
|
||
httpOnly: true,
|
||
secure: process.env.NODE_ENV === 'production',
|
||
sameSite: 'lax',
|
||
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 дней
|
||
});
|
||
|
||
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,
|
||
referralCode: populatedUser.referralCode,
|
||
referralsCount: populatedUser.referralsCount || 0,
|
||
tickets: populatedUser.tickets || 0,
|
||
settings,
|
||
banned: populatedUser.banned
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('Ошибка OAuth:', error);
|
||
res.status(500).json({ error: 'Ошибка сервера' });
|
||
}
|
||
});
|
||
|
||
router.post('/verify', authenticate, async (req, res) => {
|
||
try {
|
||
return respondWithUser(req.user, res);
|
||
} catch (error) {
|
||
console.error('Ошибка verify:', error);
|
||
res.status(500).json({ error: 'Ошибка сервера' });
|
||
}
|
||
});
|
||
|
||
// Magic-link авторизация: отправка ссылки на email
|
||
router.post('/magic-link/send', strictAuthLimiter, async (req, res) => {
|
||
try {
|
||
const { email } = req.body;
|
||
|
||
if (!email || typeof email !== 'string') {
|
||
return res.status(400).json({ error: 'Email обязателен' });
|
||
}
|
||
|
||
const emailLower = email.toLowerCase().trim();
|
||
|
||
// Простая валидация email
|
||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailLower)) {
|
||
return res.status(400).json({ error: 'Неверный формат email' });
|
||
}
|
||
|
||
// Найти или создать пользователя
|
||
let user = await User.findOne({ email: emailLower });
|
||
|
||
if (!user) {
|
||
// Создаем нового пользователя
|
||
user = new User({
|
||
email: emailLower,
|
||
username: emailLower.split('@')[0], // Временный username из email
|
||
role: 'user',
|
||
emailVerified: false
|
||
});
|
||
await user.save();
|
||
console.log('[Auth] Создан новый пользователь для email:', emailLower);
|
||
}
|
||
|
||
// Генерируем magic-link токен
|
||
const token = crypto.randomBytes(32).toString('hex');
|
||
const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 минут
|
||
|
||
// Сохраняем токен в пользователя
|
||
user.magicLinkToken = token;
|
||
user.magicLinkExpires = expiresAt;
|
||
await user.save();
|
||
|
||
// Формируем ссылку
|
||
const frontendUrl = config.frontendUrl || 'https://nkm.guru';
|
||
const magicLink = `${frontendUrl}/auth/verify?token=${token}`;
|
||
|
||
// Отправляем email
|
||
const { sendEmail } = require('../utils/email');
|
||
try {
|
||
await sendEmail(
|
||
emailLower,
|
||
'Вход в Nakama',
|
||
`
|
||
<h1>Добро пожаловать в Nakama!</h1>
|
||
<p>Нажмите на ссылку ниже, чтобы войти:</p>
|
||
<p><a href="${magicLink}" style="display: inline-block; padding: 12px 24px; background: #007AFF; color: white; text-decoration: none; border-radius: 8px;">Войти в Nakama</a></p>
|
||
<p>Ссылка действительна 15 минут.</p>
|
||
<p style="color: #666; font-size: 14px;">Если вы не запрашивали эту ссылку, просто проигнорируйте это письмо.</p>
|
||
`,
|
||
`Войдите в Nakama по ссылке: ${magicLink}\n\nСсылка действительна 15 минут.`
|
||
);
|
||
|
||
console.log('[Auth] Magic-link отправлен на:', emailLower);
|
||
res.json({ success: true, message: 'Ссылка отправлена на email' });
|
||
} catch (emailError) {
|
||
console.error('[Auth] Ошибка отправки email:', emailError);
|
||
res.status(500).json({ error: 'Не удалось отправить email. Попробуйте позже.' });
|
||
}
|
||
} catch (error) {
|
||
console.error('[Auth] Ошибка magic-link/send:', error);
|
||
res.status(500).json({ error: 'Ошибка сервера' });
|
||
}
|
||
});
|
||
|
||
// Magic-link авторизация: верификация токена
|
||
router.get('/magic-link/verify', async (req, res) => {
|
||
try {
|
||
const { token } = req.query;
|
||
|
||
if (!token) {
|
||
return res.status(400).json({ error: 'Токен не указан' });
|
||
}
|
||
|
||
// Найти пользователя с этим токеном
|
||
const user = await User.findOne({
|
||
magicLinkToken: token,
|
||
magicLinkExpires: { $gt: new Date() }
|
||
});
|
||
|
||
if (!user) {
|
||
return res.status(401).json({ error: 'Ссылка недействительна или устарела' });
|
||
}
|
||
|
||
// Проверяем, новый пользователь или существующий
|
||
const isNewUser = !user.passwordHash;
|
||
|
||
// Если это новый пользователь, требуем установить пароль
|
||
if (isNewUser) {
|
||
// Возвращаем флаг, что нужна установка пароля
|
||
return res.json({
|
||
requiresPassword: true,
|
||
token: token, // Возвращаем токен для следующего запроса
|
||
email: user.email
|
||
});
|
||
}
|
||
|
||
// Для существующих пользователей сразу авторизуем
|
||
// Очищаем токен
|
||
user.magicLinkToken = undefined;
|
||
user.magicLinkExpires = undefined;
|
||
user.emailVerified = true;
|
||
user.lastActiveAt = new Date();
|
||
await user.save();
|
||
|
||
// Генерируем JWT токен для web-сессии
|
||
const { signAccessToken, signRefreshToken, ACCESS_COOKIE, REFRESH_COOKIE } = require('../utils/tokens');
|
||
|
||
const accessToken = signAccessToken(user._id.toString());
|
||
const refreshToken = signRefreshToken(user._id.toString());
|
||
|
||
// Устанавливаем cookies
|
||
res.cookie(ACCESS_COOKIE, accessToken, {
|
||
httpOnly: true,
|
||
secure: process.env.NODE_ENV === 'production',
|
||
sameSite: 'lax',
|
||
maxAge: 5 * 60 * 1000 // 5 минут
|
||
});
|
||
|
||
res.cookie(REFRESH_COOKIE, refreshToken, {
|
||
httpOnly: true,
|
||
secure: process.env.NODE_ENV === 'production',
|
||
sameSite: 'lax',
|
||
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 дней
|
||
});
|
||
|
||
console.log('[Auth] Magic-link верифицирован для:', user.email);
|
||
return respondWithUser(user, res);
|
||
} catch (error) {
|
||
console.error('[Auth] Ошибка magic-link/verify:', error);
|
||
res.status(500).json({ error: 'Ошибка сервера' });
|
||
}
|
||
});
|
||
|
||
// Установка пароля при регистрации через magic-link
|
||
router.post('/magic-link/set-password', async (req, res) => {
|
||
try {
|
||
const { token, password, username, firstName } = req.body;
|
||
|
||
if (!token || !password) {
|
||
return res.status(400).json({ error: 'Токен и пароль обязательны' });
|
||
}
|
||
|
||
// Валидация пароля
|
||
if (password.length < 8 || password.length > 24) {
|
||
return res.status(400).json({ error: 'Пароль должен быть от 8 до 24 символов' });
|
||
}
|
||
|
||
// Валидация username (обязателен, нельзя менять после регистрации)
|
||
if (!username || username.trim().length < 3 || username.trim().length > 20) {
|
||
return res.status(400).json({ error: 'Юзернейм обязателен и должен быть от 3 до 20 символов' });
|
||
}
|
||
|
||
// Проверить уникальность username (исключая текущего пользователя)
|
||
const existingUser = await User.findOne({
|
||
username: username.trim().toLowerCase(),
|
||
_id: { $ne: user._id }
|
||
});
|
||
if (existingUser) {
|
||
return res.status(400).json({ error: 'Этот юзернейм уже занят' });
|
||
}
|
||
|
||
// Найти пользователя с этим токеном
|
||
const user = await User.findOne({
|
||
magicLinkToken: token,
|
||
magicLinkExpires: { $gt: new Date() }
|
||
});
|
||
|
||
if (!user) {
|
||
return res.status(401).json({ error: 'Ссылка недействительна или устарела' });
|
||
}
|
||
|
||
// Хешируем пароль
|
||
const bcrypt = require('bcryptjs');
|
||
const passwordHash = await bcrypt.hash(password, 10);
|
||
|
||
// Обновляем пользователя
|
||
user.passwordHash = passwordHash;
|
||
user.emailVerified = true;
|
||
user.magicLinkToken = undefined;
|
||
user.magicLinkExpires = undefined;
|
||
user.lastActiveAt = new Date();
|
||
|
||
// Устанавливаем username (нельзя менять после регистрации)
|
||
if (username) {
|
||
user.username = username.trim().toLowerCase();
|
||
}
|
||
|
||
// Устанавливаем firstName (никнейм, можно менять)
|
||
if (firstName) {
|
||
user.firstName = firstName.trim();
|
||
}
|
||
|
||
await user.save();
|
||
|
||
// Генерируем JWT токен
|
||
const { signAccessToken, signRefreshToken, ACCESS_COOKIE, REFRESH_COOKIE } = require('../utils/tokens');
|
||
|
||
const accessToken = signAccessToken(user._id.toString());
|
||
const refreshToken = signRefreshToken(user._id.toString());
|
||
|
||
// Устанавливаем cookies
|
||
res.cookie(ACCESS_COOKIE, accessToken, {
|
||
httpOnly: true,
|
||
secure: process.env.NODE_ENV === 'production',
|
||
sameSite: 'lax',
|
||
maxAge: 5 * 60 * 1000
|
||
});
|
||
|
||
res.cookie(REFRESH_COOKIE, refreshToken, {
|
||
httpOnly: true,
|
||
secure: process.env.NODE_ENV === 'production',
|
||
sameSite: 'lax',
|
||
maxAge: 7 * 24 * 60 * 60 * 1000
|
||
});
|
||
|
||
console.log('[Auth] Пароль установлен для:', user.email);
|
||
return respondWithUser(user, res);
|
||
} catch (error) {
|
||
console.error('[Auth] Ошибка magic-link/set-password:', error);
|
||
res.status(500).json({ error: 'Ошибка сервера' });
|
||
}
|
||
});
|
||
|
||
// Email + password авторизация
|
||
router.post('/login-email', strictAuthLimiter, async (req, res) => {
|
||
try {
|
||
const { email, password } = req.body;
|
||
|
||
if (!email || !password) {
|
||
return res.status(400).json({ error: 'Email и пароль обязательны' });
|
||
}
|
||
|
||
const emailLower = email.toLowerCase().trim();
|
||
|
||
// Найти пользователя с паролем
|
||
const user = await User.findOne({
|
||
email: emailLower,
|
||
passwordHash: { $exists: true }
|
||
}).select('+passwordHash');
|
||
|
||
if (!user) {
|
||
return res.status(401).json({ error: 'Неверный email или пароль' });
|
||
}
|
||
|
||
if (user.banned) {
|
||
return res.status(403).json({ error: 'Аккаунт заблокирован' });
|
||
}
|
||
|
||
// Проверить пароль
|
||
const bcrypt = require('bcryptjs');
|
||
const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
|
||
|
||
if (!isPasswordValid) {
|
||
return res.status(401).json({ error: 'Неверный email или пароль' });
|
||
}
|
||
|
||
// Обновить активность
|
||
user.lastActiveAt = new Date();
|
||
await user.save();
|
||
|
||
// Генерируем JWT токены
|
||
const { signAccessToken, signRefreshToken, ACCESS_COOKIE, REFRESH_COOKIE } = require('../utils/tokens');
|
||
|
||
const accessToken = signAccessToken(user._id.toString());
|
||
const refreshToken = signRefreshToken(user._id.toString());
|
||
|
||
res.cookie(ACCESS_COOKIE, accessToken, {
|
||
httpOnly: true,
|
||
secure: process.env.NODE_ENV === 'production',
|
||
sameSite: 'lax',
|
||
maxAge: 5 * 60 * 1000
|
||
});
|
||
|
||
res.cookie(REFRESH_COOKIE, refreshToken, {
|
||
httpOnly: true,
|
||
secure: process.env.NODE_ENV === 'production',
|
||
sameSite: 'lax',
|
||
maxAge: 7 * 24 * 60 * 60 * 1000
|
||
});
|
||
|
||
console.log('[Auth] Email авторизация для:', user.email);
|
||
return respondWithUser(user, res);
|
||
} catch (error) {
|
||
console.error('[Auth] Ошибка login-email:', 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' }
|
||
]);
|
||
|
||
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,
|
||
referralCode: populatedUser.referralCode,
|
||
referralsCount: populatedUser.referralsCount || 0,
|
||
tickets: populatedUser.tickets || 0,
|
||
settings,
|
||
banned: populatedUser.banned
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('Ошибка проверки сессии:', error);
|
||
res.status(500).json({ error: 'Ошибка сервера' });
|
||
}
|
||
});
|
||
|
||
// Привязка Telegram к существующему аккаунту
|
||
router.post('/link-telegram', authenticate, async (req, res) => {
|
||
try {
|
||
const { telegramId, username, firstName, lastName, photoUrl } = req.body;
|
||
|
||
if (!telegramId) {
|
||
return res.status(400).json({ error: 'telegramId обязателен' });
|
||
}
|
||
|
||
// Проверить, не привязан ли уже этот Telegram к другому аккаунту
|
||
const existingUser = await User.findOne({ telegramId });
|
||
if (existingUser && existingUser._id.toString() !== req.user._id.toString()) {
|
||
return res.status(400).json({ error: 'Этот Telegram уже привязан к другому аккаунту' });
|
||
}
|
||
|
||
// Привязываем Telegram
|
||
req.user.telegramId = telegramId;
|
||
if (username) req.user.username = username;
|
||
if (firstName) req.user.firstName = firstName;
|
||
if (lastName) req.user.lastName = lastName;
|
||
if (photoUrl) req.user.photoUrl = photoUrl;
|
||
|
||
await req.user.save();
|
||
|
||
console.log('[Auth] Telegram привязан к аккаунту:', req.user.email);
|
||
return respondWithUser(req.user, res);
|
||
} catch (error) {
|
||
console.error('[Auth] Ошибка привязки Telegram:', error);
|
||
res.status(500).json({ error: 'Ошибка сервера' });
|
||
}
|
||
});
|
||
|
||
// Привязка email и установка пароля (для Telegram пользователей)
|
||
router.post('/link-email', authenticate, async (req, res) => {
|
||
try {
|
||
const { email, password } = req.body;
|
||
|
||
if (!email || !password) {
|
||
return res.status(400).json({ error: 'Email и пароль обязательны' });
|
||
}
|
||
|
||
// Валидация пароля
|
||
if (password.length < 8 || password.length > 24) {
|
||
return res.status(400).json({ error: 'Пароль должен быть от 8 до 24 символов' });
|
||
}
|
||
|
||
const emailLower = email.toLowerCase().trim();
|
||
|
||
// Проверить формат email
|
||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailLower)) {
|
||
return res.status(400).json({ error: 'Неверный формат email' });
|
||
}
|
||
|
||
// Проверить, не занят ли email
|
||
const existingUser = await User.findOne({ email: emailLower });
|
||
if (existingUser && existingUser._id.toString() !== req.user._id.toString()) {
|
||
return res.status(400).json({ error: 'Этот email уже используется' });
|
||
}
|
||
|
||
// Хешируем пароль
|
||
const bcrypt = require('bcryptjs');
|
||
const passwordHash = await bcrypt.hash(password, 10);
|
||
|
||
// Привязываем email и пароль
|
||
req.user.email = emailLower;
|
||
req.user.passwordHash = passwordHash;
|
||
req.user.emailVerified = true;
|
||
|
||
await req.user.save();
|
||
|
||
console.log('[Auth] Email привязан к аккаунту:', req.user.telegramId);
|
||
return respondWithUser(req.user, res);
|
||
} catch (error) {
|
||
console.error('[Auth] Ошибка привязки email:', error);
|
||
res.status(500).json({ error: 'Ошибка сервера' });
|
||
}
|
||
});
|
||
|
||
module.exports = router;
|
||
|