diff --git a/backend/controllers/userController.js b/backend/controllers/userController.js index d83155d..f81e6ca 100644 --- a/backend/controllers/userController.js +++ b/backend/controllers/userController.js @@ -194,7 +194,8 @@ const uploadUserProfilePhoto = async (req, res, next) => { const newPhoto = { url: result.secure_url, public_id: result.public_id, - isProfilePhoto: user.photos.length === 0, + // Если это первая фотография, делаем ее главной. Иначе - нет. + isProfilePhoto: user.photos.length === 0, }; // Добавляем новое фото в массив фотографий пользователя @@ -202,19 +203,23 @@ const uploadUserProfilePhoto = async (req, res, next) => { await user.save(); // Получаем обновленного пользователя с обновленным массивом фотографий - const updatedUser = await User.findById(req.user._id); + 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: newPhoto, + photo: addedPhotoWithId, // Возвращаем фото с _id allPhotos: allPhotos }); @@ -227,12 +232,115 @@ const uploadUserProfilePhoto = async (req, res, next) => { } }; -// Другие возможные функции для userController (например, getUserById, getAllUsers для админа и т.д.) -// const getUserById = async (req, res, next) => { ... }; +// @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, }; \ No newline at end of file diff --git a/backend/routes/userRoutes.js b/backend/routes/userRoutes.js index f5363f8..a74052e 100644 --- a/backend/routes/userRoutes.js +++ b/backend/routes/userRoutes.js @@ -1,6 +1,6 @@ const express = require('express'); const router = express.Router(); -const { updateUserProfile, getUsersForSwiping, uploadUserProfilePhoto } = require('../controllers/userController'); +const { updateUserProfile, getUsersForSwiping, uploadUserProfilePhoto, setMainPhoto, deletePhoto } = require('../controllers/userController'); const { protect } = require('../middleware/authMiddleware'); // Нам нужен protect для защиты маршрута const multer = require('multer'); // 1. Импортируем multer const path = require('path'); // Может понадобиться для фильтрации файлов @@ -39,6 +39,8 @@ router.get('/suggestions', protect, getUsersForSwiping); // <--- НОВЫЙ МА // Маршрут для загрузки фотографии профиля // POST /api/users/profile/photo router.post('/profile/photo', protect, upload.single('profilePhoto'), uploadUserProfilePhoto); +router.put('/profile/photo/:photoId/set-main', protect, setMainPhoto); // <--- НОВЫЙ МАРШРУТ +router.delete('/profile/photo/:photoId', protect, deletePhoto); // <--- НОВЫЙ МАРШРУТ // Маршрут для получения профиля по ID (например, для просмотра чужих профилей, если это нужно) // GET /api/users/:id diff --git a/package-lock.json b/package-lock.json index d886d2f..8596527 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,9 @@ "axios": "^1.9.0", "bcryptjs": "^3.0.2", "bootstrap": "^5.3.6", + "browser-image-compression": "^2.0.2", "cloudinary": "^2.6.1", + "compressorjs": "^1.2.1", "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^5.1.0", @@ -2816,6 +2818,12 @@ "bcrypt": "bin/bcrypt" } }, + "node_modules/blueimp-canvas-to-blob": { + "version": "3.29.0", + "resolved": "https://registry.npmjs.org/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.29.0.tgz", + "integrity": "sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -2866,6 +2874,15 @@ "concat-map": "0.0.1" } }, + "node_modules/browser-image-compression": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz", + "integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==", + "license": "MIT", + "dependencies": { + "uzip": "0.20201231.0" + } + }, "node_modules/browserslist": { "version": "4.24.5", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", @@ -3088,6 +3105,16 @@ "node": ">=4.0.0" } }, + "node_modules/compressorjs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/compressorjs/-/compressorjs-1.2.1.tgz", + "integrity": "sha512-+geIjeRnPhQ+LLvvA7wxBQE5ddeLU7pJ3FsKFWirDw6veY3s9iLxAQEw7lXGHnhCJvBujEQWuNnGzZcvCvdkLQ==", + "license": "MIT", + "dependencies": { + "blueimp-canvas-to-blob": "^3.29.0", + "is-blob": "^2.1.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4343,6 +4370,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-blob": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-blob/-/is-blob-2.1.0.tgz", + "integrity": "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-boolean-object": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", @@ -6683,6 +6722,12 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uzip": { + "version": "0.20201231.0", + "resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz", + "integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==", + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index bafa834..24b0afb 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "axios": "^1.9.0", "bcryptjs": "^3.0.2", "bootstrap": "^5.3.6", + "browser-image-compression": "^2.0.2", "cloudinary": "^2.6.1", + "compressorjs": "^1.2.1", "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^5.1.0", diff --git a/src/components/EditProfileForm.vue b/src/components/EditProfileForm.vue index aa9a6d0..f6a30a3 100644 --- a/src/components/EditProfileForm.vue +++ b/src/components/EditProfileForm.vue @@ -37,10 +37,10 @@