Reflex/backend/controllers/userController.js
2025-05-21 23:43:24 +07:00

346 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 User = require('../models/User');
const cloudinary = require('../config/cloudinaryConfig'); // Импортируем настроенный Cloudinary SDK
const path = require('path'); // Может понадобиться для получения расширения файла
// @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, // Отправлять весь массив фото здесь, возможно, избыточно для карточки свайпа
// Если нужна только одна фото, то mainPhotoUrl достаточно.
// Если нужны несколько превью - можно вернуть ограниченное количество.
};
});
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);
}
};
module.exports = {
updateUserProfile,
getUsersForSwiping,
uploadUserProfilePhoto,
setMainPhoto, // <--- Добавлено
deletePhoto, // <--- Добавлено
// getUserById,
};