Reflex/backend/controllers/userController.js
Professional e46b9f268e фикс
2025-05-24 02:24:21 +07:00

527 lines
21 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 cloudinary = require('../config/cloudinaryConfig'); // Импортируем настроенный Cloudinary SDK
const path = require('path'); // Может понадобиться для получения расширения файла
const ProfileView = require('../models/ProfileView');
// @desc Обновить профиль пользователя
// @route PUT /api/users/profile
// @access Private (пользователь должен быть аутентифицирован)
const updateUserProfile = async (req, res, next) => {
try {
console.log('Запрос на обновление профиля от пользователя:', req.user._id);
console.log('Данные для обновления:', req.body);
// req.user должен быть доступен благодаря middleware 'protect'
const user = await User.findById(req.user._id);
if (user) {
// Обновляем только те поля, которые пользователь может изменять
user.name = req.body.name || user.name;
// Обрабатываем dateOfBirth, учитывая null и пустые строки
if (req.body.dateOfBirth === null || req.body.dateOfBirth === '') {
user.dateOfBirth = undefined; // Удаляем поле, если оно пустое или null
} else if (req.body.dateOfBirth) {
user.dateOfBirth = req.body.dateOfBirth;
}
// Обрабатываем gender, учитывая null и пустые строки
if (req.body.gender === null || req.body.gender === '') {
user.gender = undefined; // Удаляем поле, если оно пустое или null
} else if (req.body.gender) {
user.gender = req.body.gender;
}
// Обрабатываем bio, учитывая null и пустые строки
if (req.body.bio === null) {
user.bio = ''; // Устанавливаем пустую строку, если null
} else {
user.bio = req.body.bio || user.bio;
}
// Обновление местоположения (если передано)
if (req.body.location) {
user.location.city = req.body.location.city || user.location.city;
user.location.country = req.body.location.country || user.location.country;
}
// Обновление предпочтений (если переданы)
if (req.body.preferences) {
if (req.body.preferences.gender) {
user.preferences.gender = req.body.preferences.gender;
}
if (req.body.preferences.ageRange) {
user.preferences.ageRange.min = req.body.preferences.ageRange.min || user.preferences.ageRange.min;
user.preferences.ageRange.max = req.body.preferences.ageRange.max || user.preferences.ageRange.max;
}
}
console.log('Данные пользователя перед сохранением:', user);
const updatedUser = await user.save();
console.log('Профиль успешно обновлен:', updatedUser);
res.status(200).json({
_id: updatedUser._id,
name: updatedUser.name,
email: updatedUser.email,
dateOfBirth: updatedUser.dateOfBirth,
gender: updatedUser.gender,
bio: updatedUser.bio,
photos: updatedUser.photos,
location: updatedUser.location,
preferences: updatedUser.preferences,
message: 'Профиль успешно обновлен!'
});
} else {
res.status(404);
throw new Error('Пользователь не найден.');
}
} catch (error) {
console.error(`Ошибка в ${req.method} ${req.originalUrl}:`, error.message); // Более информативный лог
// Устанавливаем statusCode на объекте ошибки, если он еще не установлен
// или если мы хотим его переопределить для этого конкретного случая
if (error.name === 'ValidationError' || error.name === 'CastError') {
error.statusCode = 400;
} else if (error.message === 'Пользователь не найден.') { // Если мы сами выбросили эту ошибку с throw
error.statusCode = 404;
} else if (!error.statusCode) {
// Если это какая-то другая ошибка без явно установленного statusCode,
// можно считать ее серверной ошибкой.
error.statusCode = 500;
}
// Передаем ошибку (с установленным error.statusCode) в наш центральный errorHandler
next(error);
}
};
const getUsersForSwiping = async (req, res, next) => {
try {
const currentUserId = req.user._id;
// TODO: Более сложная логика фильтрации и сортировки
const users = await User.find({
_id: { $ne: currentUserId },
// Можно добавить условие, чтобы у пользователя было хотя бы одно фото
// 'photos.0': { $exists: true } // Если нужно показывать только тех, у кого есть фото
})
.select('name dateOfBirth gender bio photos preferences.ageRange'); // photos все еще нужны для выбора главной
const usersWithAgeAndPhoto = users.map(user => {
let age = null;
if (user.dateOfBirth) {
const birthDate = new Date(user.dateOfBirth);
const today = new Date();
age = today.getFullYear() - birthDate.getFullYear();
const m = today.getMonth() - birthDate.getMonth();
if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
}
// --- Логика выбора главной фотографии ---
let mainPhotoUrl = null;
if (user.photos && user.photos.length > 0) {
// Ищем фото, помеченное как главное
const profilePic = user.photos.find(photo => photo.isProfilePhoto === true);
if (profilePic) {
mainPhotoUrl = profilePic.url;
} else {
// Если нет явно помеченного главного фото, берем первое из массива
mainPhotoUrl = user.photos[0].url;
}
}
// -----------------------------------------
return {
_id: user._id,
name: user.name,
age: age,
gender: user.gender,
bio: user.bio,
mainPhotoUrl: mainPhotoUrl, // <--- Добавляем URL главной фотографии
photos: user.photos, // Возвращаем все фото для карусели
};
});
res.status(200).json(usersWithAgeAndPhoto);
} catch (error) {
console.error('Ошибка при получении пользователей для свайпа:', error.message);
next(error);
}
};
const uploadUserProfilePhoto = async (req, res, next) => {
try {
const user = await User.findById(req.user._id);
if (!user) {
const error = new Error('Пользователь не найден.');
error.statusCode = 404;
return next(error);
}
if (!req.file) { // req.file будет добавлен multer'ом
const error = new Error('Файл для загрузки не найден.');
error.statusCode = 400;
return next(error);
}
console.log('[USER_CTRL] Получен файл для загрузки:', req.file.originalname, 'Размер:', req.file.size);
// Проверяем инициализацию Cloudinary
if (!cloudinary || !cloudinary.uploader) {
console.error('[USER_CTRL] Ошибка: Cloudinary не настроен');
const error = new Error('Ошибка настройки службы хранения файлов.');
error.statusCode = 500;
return next(error);
}
// Загрузка файла в Cloudinary
const b64 = Buffer.from(req.file.buffer).toString("base64");
let dataURI = "data:" + req.file.mimetype + ";base64," + b64;
const result = await cloudinary.uploader.upload(dataURI, {
folder: `dating_app/user_photos/${user._id}`,
resource_type: 'image',
});
console.log('[USER_CTRL] Файл успешно загружен в Cloudinary:', result.secure_url);
const newPhoto = {
url: result.secure_url,
public_id: result.public_id,
// Если это первая фотография, делаем ее главной. Иначе - нет.
isProfilePhoto: user.photos.length === 0,
};
// Добавляем новое фото в массив фотографий пользователя
user.photos.push(newPhoto);
await user.save();
// Получаем обновленного пользователя с обновленным массивом фотографий
const updatedUser = await User.findById(req.user._id).lean(); // .lean() для простого объекта
// Важно! Явно создаем массив allPhotos для ответа, чтобы убедиться, что структура каждого объекта фото совпадает
const allPhotos = updatedUser.photos.map(photo => ({
_id: photo._id, // Добавляем _id для фото
url: photo.url,
public_id: photo.public_id,
isProfilePhoto: photo.isProfilePhoto
}));
// Найдем только что добавленное фото в обновленном массиве, чтобы вернуть его с _id
const addedPhotoWithId = allPhotos.find(p => p.public_id === newPhoto.public_id);
// Возвращаем ответ с точной структурой, ожидаемой тестами
res.status(200).json({
message: 'Фотография успешно загружена!',
photo: addedPhotoWithId, // Возвращаем фото с _id
allPhotos: allPhotos
});
} catch (error) {
console.error('[USER_CTRL] Ошибка при загрузке фотографии:', error.message, error.stack);
if (error.http_code) {
error.statusCode = error.http_code;
}
next(error);
}
};
// @desc Установить фотографию как главную
// @route PUT /api/users/profile/photo/:photoId/set-main
// @access Private
const setMainPhoto = async (req, res, next) => {
try {
const user = await User.findById(req.user._id);
const { photoId } = req.params;
if (!user) {
const error = new Error('Пользователь не найден.');
error.statusCode = 404;
return next(error);
}
let photoFound = false;
user.photos.forEach(photo => {
if (photo._id.toString() === photoId) {
photo.isProfilePhoto = true;
photoFound = true;
} else {
photo.isProfilePhoto = false;
}
});
if (!photoFound) {
const error = new Error('Фотография не найдена.');
error.statusCode = 404;
return next(error);
}
await user.save();
const updatedUser = await User.findById(req.user._id).lean();
res.status(200).json({
message: 'Главная фотография успешно обновлена.',
photos: updatedUser.photos.map(p => ({_id: p._id, url: p.url, public_id: p.public_id, isProfilePhoto: p.isProfilePhoto }))
});
} catch (error) {
console.error('[USER_CTRL] Ошибка при установке главной фотографии:', error.message);
next(error);
}
};
// @desc Удалить фотографию пользователя
// @route DELETE /api/users/profile/photo/:photoId
// @access Private
const deletePhoto = async (req, res, next) => {
try {
const user = await User.findById(req.user._id);
const { photoId } = req.params;
if (!user) {
const error = new Error('Пользователь не найден.');
error.statusCode = 404;
return next(error);
}
const photoIndex = user.photos.findIndex(p => p._id.toString() === photoId);
if (photoIndex === -1) {
const error = new Error('Фотография не найдена.');
error.statusCode = 404;
return next(error);
}
const photoToDelete = user.photos[photoIndex];
// Удаление из Cloudinary
if (photoToDelete.public_id) {
try {
await cloudinary.uploader.destroy(photoToDelete.public_id);
console.log(`[USER_CTRL] Фото ${photoToDelete.public_id} удалено из Cloudinary.`);
} catch (cloudinaryError) {
console.error('[USER_CTRL] Ошибка при удалении фото из Cloudinary:', cloudinaryError.message);
// Не прерываем процесс, если фото не удалось удалить из Cloudinary, но логируем ошибку
// Можно добавить более сложную логику обработки, например, пометить фото для последующего удаления
}
}
// Удаление из массива photos пользователя
user.photos.splice(photoIndex, 1);
// Если удаленная фотография была главной и остались другие фотографии,
// делаем первую из оставшихся главной.
if (photoToDelete.isProfilePhoto && user.photos.length > 0) {
user.photos[0].isProfilePhoto = true;
}
await user.save();
const updatedUser = await User.findById(req.user._id).lean();
res.status(200).json({
message: 'Фотография успешно удалена.',
photos: updatedUser.photos.map(p => ({_id: p._id, url: p.url, public_id: p.public_id, isProfilePhoto: p.isProfilePhoto }))
});
} catch (error) {
console.error('[USER_CTRL] Ошибка при удалении фотографии:', error.message);
next(error);
}
};
// @desc Получить профиль пользователя по ID (для просмотра чужих профилей)
// @route GET /api/users/:userId
// @access Private
const getUserById = async (req, res, next) => {
try {
const { userId } = req.params;
const currentUserId = req.user._id;
// Проверяем, что пользователь не запрашивает свой собственный профиль
// (для этого есть отдельный роут /auth/me)
if (currentUserId.equals(userId)) {
const error = new Error('Для получения своего профиля используйте /auth/me');
error.statusCode = 400;
return next(error);
}
// Находим пользователя по ID
const user = await User.findById(userId)
.select('name dateOfBirth gender bio photos location createdAt');
if (!user) {
const error = new Error('Пользователь не найден.');
error.statusCode = 404;
return next(error);
}
// Вычисляем возраст, если указана дата рождения
let age = null;
if (user.dateOfBirth) {
const birthDate = new Date(user.dateOfBirth);
const today = new Date();
age = today.getFullYear() - birthDate.getFullYear();
const m = today.getMonth() - birthDate.getMonth();
if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
}
// Формируем ответ
const userProfile = {
_id: user._id,
name: user.name,
age: age,
gender: user.gender,
bio: user.bio,
photos: user.photos,
location: user.location,
memberSince: user.createdAt
};
res.status(200).json(userProfile);
} catch (error) {
console.error('[USER_CTRL] Ошибка при получении профиля пользователя:', error.message);
next(error);
}
};
// @desc Записать просмотр профиля пользователя
// @route POST /api/users/:userId/view
// @access Private
const recordProfileView = async (req, res, next) => {
try {
const { userId } = req.params;
const viewerId = req.user._id;
const { source = 'other' } = req.body;
// Проверяем, что пользователь не пытается просматривать свой собственный профиль
if (viewerId.equals(userId)) {
return res.status(200).json({ message: 'Просмотр собственного профиля не записывается' });
}
// Проверяем, существует ли пользователь, чей профиль просматривают
const profileOwner = await User.findById(userId);
if (!profileOwner) {
const error = new Error('Пользователь не найден.');
error.statusCode = 404;
return next(error);
}
// Проверяем, был ли уже просмотр от этого пользователя в последние 5 минут
// Это предотвращает спам просмотров при обновлении страницы
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
const recentView = await ProfileView.findOne({
viewer: viewerId,
profileOwner: userId,
viewedAt: { $gte: fiveMinutesAgo }
});
if (recentView) {
console.log(`[USER_CTRL] Недавний просмотр уже существует для пользователя ${viewerId} -> ${userId}`);
return res.status(200).json({ message: 'Просмотр уже записан' });
}
// Создаем новую запись просмотра
const newView = new ProfileView({
viewer: viewerId,
profileOwner: userId,
source: source,
viewedAt: new Date()
});
await newView.save();
console.log(`[USER_CTRL] Записан просмотр профиля ${userId} пользователем ${viewerId}, источник: ${source}`);
res.status(200).json({
message: 'Просмотр профиля записан',
viewId: newView._id
});
} catch (error) {
console.error('[USER_CTRL] Ошибка при записи просмотра профиля:', error.message);
next(error);
}
};
// @desc Получить статистику пользователя для главной страницы
// @route GET /api/users/stats
// @access Private
const getUserStats = async (req, res, next) => {
try {
const currentUserId = req.user._id;
// 1. Получаем количество лайков (кто лайкнул текущего пользователя)
const likesCount = await User.countDocuments({
liked: currentUserId
});
// 2. Получаем количество совпадений (matches)
const matchesCount = await User.findById(currentUserId)
.select('matches')
.then(user => user?.matches?.length || 0);
// 3. Получаем количество непрочитанных сообщений
const Conversation = require('../models/Conversation');
const Message = require('../models/Message');
// Находим все диалоги пользователя
const userConversations = await Conversation.find({
participants: currentUserId
}).select('_id');
const conversationIds = userConversations.map(conv => conv._id);
// Считаем непрочитанные сообщения в этих диалогах
const unreadMessagesCount = await Message.countDocuments({
conversationId: { $in: conversationIds },
sender: { $ne: currentUserId }, // Сообщения от других пользователей
readBy: { $ne: currentUserId } // Которые текущий пользователь не читал
});
// 4. Получаем общее количество сообщений пользователя
const totalMessagesCount = await Message.countDocuments({
conversationId: { $in: conversationIds },
sender: currentUserId // Только сообщения, отправленные текущим пользователем
});
// 5. Получаем количество просмотров профиля
const profileViewsCount = await ProfileView.countDocuments({
profileOwner: currentUserId
});
const stats = {
profileViews: profileViewsCount,
likes: likesCount,
matches: matchesCount,
unreadMessages: unreadMessagesCount,
totalMessages: totalMessagesCount // Добавляем новое значение в ответ
};
console.log(`[USER_CTRL] Статистика пользователя ${currentUserId}:`, stats);
res.status(200).json(stats);
} catch (error) {
console.error('[USER_CTRL] Ошибка при получении статистики пользователя:', error.message);
next(error);
}
};
module.exports = {
updateUserProfile,
getUsersForSwiping,
uploadUserProfilePhoto,
setMainPhoto,
deletePhoto,
getUserById,
getUserStats,
recordProfileView, // <--- Добавляем новую функцию
};