From cf5dfc7e5404ee21d1c0f76cc9c8090a817ce7cd Mon Sep 17 00:00:00 2001 From: Foxix Date: Tue, 8 Jul 2025 16:43:41 +0300 Subject: [PATCH] fix auth, reactions and etc --- src/index.js | 14 +++--- src/routes/auth.js | 47 ++++++++++++++++++++ src/routes/movies.js | 70 ++++++++++++++--------------- src/routes/reactions.js | 97 +++++++++++++++++------------------------ 4 files changed, 130 insertions(+), 98 deletions(-) diff --git a/src/index.js b/src/index.js index 4ab80c0..3c7ee63 100644 --- a/src/index.js +++ b/src/index.js @@ -266,13 +266,13 @@ const routerToUse = reactionsRouter.default || reactionsRouter; require('./utils/cleanup'); const authRouter = require('./routes/auth'); -app.use('/api/movies', moviesRouter); -app.use('/api/tv', tvRouter); -app.use('/api/images', imagesRouter); -app.use('/api/categories', categoriesRouter); -app.use('/api/favorites', favoritesRouter); -app.use('/api/players', playersRouter); -app.use('/api/reactions', routerToUse); +app.use('/movies', moviesRouter); +app.use('/tv', tvRouter); +app.use('/images', imagesRouter); +app.use('/categories', categoriesRouter); +app.use('/favorites', favoritesRouter); +app.use('/players', playersRouter); +app.use('/reactions', routerToUse); app.use('/auth', authRouter); /** diff --git a/src/routes/auth.js b/src/routes/auth.js index 40777e8..1c7e29f 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -3,7 +3,10 @@ const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const { v4: uuidv4 } = require('uuid'); const { getDb } = require('../db'); +const { ObjectId } = require('mongodb'); const { sendVerificationEmail } = require('../utils/mailer'); +const authRequired = require('../middleware/auth'); +const fetch = require('node-fetch'); /** * @swagger @@ -204,4 +207,48 @@ router.post('/login', async (req, res) => { } }); +// Delete account +/** + * @swagger + * /auth/profile: + * delete: + * tags: [auth] + * summary: Удаление аккаунта пользователя + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Аккаунт успешно удален + * 500: + * description: Ошибка сервера + */ +router.delete('/profile', authRequired, async (req, res) => { + try { + const db = await getDb(); + const userId = req.user.id; + + // 1. Найти все реакции пользователя, чтобы уменьшить счетчики в cub.rip + const userReactions = await db.collection('reactions').find({ userId }).toArray(); + if (userReactions.length > 0) { + const CUB_API_URL = process.env.CUB_API_URL || 'https://cub.rip/api'; + const removalPromises = userReactions.map(reaction => + fetch(`${CUB_API_URL}/reactions/remove/${reaction.mediaId}/${reaction.type}`, { + method: 'POST' // или 'DELETE', в зависимости от API + }) + ); + await Promise.all(removalPromises); + } + + // 2. Удалить все данные пользователя + await db.collection('users').deleteOne({ _id: new ObjectId(userId) }); + await db.collection('favorites').deleteMany({ userId }); + await db.collection('reactions').deleteMany({ userId }); + + res.status(200).json({ success: true, message: 'Account deleted successfully.' }); + } catch (err) { + console.error('Delete account error:', err); + res.status(500).json({ error: 'Failed to delete account.' }); + } +}); + module.exports = router; diff --git a/src/routes/movies.js b/src/routes/movies.js index 7d7e40d..b8e87f0 100644 --- a/src/routes/movies.js +++ b/src/routes/movies.js @@ -1,7 +1,34 @@ const express = require('express'); const router = express.Router(); + const { formatDate } = require('../utils/date'); +// Helper to check if a title contains valid characters (Cyrillic, Latin, numbers, common punctuation) +const isValidTitle = (title = '') => { + if (!title) return true; // Allow items with no title (e.g., some TV episodes) + // Regular expression to match titles containing Cyrillic, Latin, numbers, and common punctuation. + const validTitleRegex = /^[\p{Script=Cyrillic}\p{Script=Latin}\d\s:!?'.,()-]+$/u; + return validTitleRegex.test(title.trim()); +}; + +// Function to filter and format results +const filterAndFormat = (results = []) => { + if (!Array.isArray(results)) return []; + return results + .filter(item => { + if (!item) return false; + // Filter out items with a vote average of 0, unless they are upcoming (as they might not have votes yet) + if (item.vote_average === 0 && !item.release_date) return false; + // Filter based on title validity + return isValidTitle(item.title || item.name || ''); + }) + .map(item => ({ + ...item, + release_date: item.release_date ? formatDate(item.release_date) : null, + first_air_date: item.first_air_date ? formatDate(item.first_air_date) : null, + })); +}; + // Middleware для логирования запросов router.use((req, res, next) => { console.log('Movies API Request:', { @@ -89,16 +116,8 @@ router.get('/search', async (req, res) => { results_count: data.results?.length }); - // Форматируем даты в результатах - const formattedResults = data.results.map(movie => ({ - ...movie, - release_date: formatDate(movie.release_date) - })); - - res.json({ - ...data, - results: formattedResults - }); + const formattedResults = filterAndFormat(data.results); + res.json({ ...data, results: formattedResults }); } catch (error) { console.error('Error searching movies:', error); res.status(500).json({ error: error.message }); @@ -168,18 +187,10 @@ router.get('/search/multi', async (req, res) => { // Путь должен бы ]); // Объединяем и сортируем результаты по популярности - const combinedResults = [ - ...moviesData.results.map(movie => ({ - ...movie, - media_type: 'movie', - release_date: formatDate(movie.release_date) - })), - ...tvData.results.map(show => ({ - ...show, - media_type: 'tv', - first_air_date: formatDate(show.first_air_date) - })) - ].sort((a, b) => b.popularity - a.popularity); + const combinedResults = filterAndFormat([ + ...moviesData.results.map(item => ({ ...item, media_type: 'movie' })), + ...tvData.results.map(item => ({ ...item, media_type: 'tv' })) + ]).sort((a, b) => b.popularity - a.popularity); // Пагинация результатов const itemsPerPage = 20; @@ -263,10 +274,7 @@ router.get('/popular', async (req, res) => { throw new Error('Invalid response from TMDB'); } - const formattedResults = movies.results.map(movie => ({ - ...movie, - release_date: formatDate(movie.release_date) - })); + const formattedResults = filterAndFormat(movies.results); res.json({ ...movies, @@ -333,10 +341,7 @@ router.get('/top-rated', async (req, res) => { throw new Error('Invalid response from TMDB'); } - const formattedResults = movies.results.map(movie => ({ - ...movie, - release_date: formatDate(movie.release_date) - })); + const formattedResults = filterAndFormat(movies.results); res.json({ ...movies, @@ -523,10 +528,7 @@ router.get('/upcoming', async (req, res) => { throw new Error('Invalid response from TMDB'); } - const formattedResults = movies.results.map(movie => ({ - ...movie, - release_date: formatDate(movie.release_date) - })); + const formattedResults = filterAndFormat(movies.results); res.json({ ...movies, diff --git a/src/routes/reactions.js b/src/routes/reactions.js index 237a2c2..d72da35 100644 --- a/src/routes/reactions.js +++ b/src/routes/reactions.js @@ -9,18 +9,19 @@ const router = Router(); const CUB_API_URL = 'https://cub.rip/api'; const VALID_REACTIONS = ['fire', 'nice', 'think', 'bore', 'shit']; -// Получить все счетчики реакций для медиа -router.get('/:mediaId/counts', async (req, res) => { +// [PUBLIC] Получить все счетчики реакций для медиа +router.get('/:mediaType/:mediaId/counts', async (req, res) => { try { - const { mediaId } = req.params; - const response = await fetch(`${CUB_API_URL}/reactions/get/${mediaId}`); + const { mediaType, mediaId } = req.params; + const cubId = `${mediaType}_${mediaId}`; + const response = await fetch(`${CUB_API_URL}/reactions/get/${cubId}`); if (!response.ok) { - throw new Error(`CUB API request failed with status ${response.status}`); + // Возвращаем пустой объект, если на CUB.RIP еще нет реакций + return res.json({}); } const data = await response.json(); - // Преобразуем ответ от CUB API в удобный формат { type: count } - const counts = data.result.reduce((acc, reaction) => { + const counts = (data.result || []).reduce((acc, reaction) => { acc[reaction.type] = reaction.counter; return acc; }, {}); @@ -32,46 +33,15 @@ router.get('/:mediaId/counts', async (req, res) => { } }); -// --- PUBLIC ROUTE --- -// Получить общее количество реакций для медиа -router.get('/counts/:mediaType/:mediaId', async (req, res) => { - try { - const { mediaType, mediaId } = req.params; - const cubId = `${mediaType}_${mediaId}`; - - const response = await fetch(`${CUB_API_URL}/reactions/get/${cubId}`); - if (!response.ok) { - // Если CUB API возвращает ошибку, считаем, что реакций нет - console.error(`CUB API error for ${cubId}:`, response.statusText); - return res.json({ total: 0 }); - } - - const data = await response.json(); - if (!data.secuses || !Array.isArray(data.result)) { - return res.json({ total: 0 }); - } - - const total = data.result.reduce((sum, reaction) => sum + (reaction.counter || 0), 0); - res.json({ total }); - - } catch (err) { - console.error('Get total reactions error:', err); - res.status(500).json({ error: 'Failed to get total reactions' }); - } -}); - - -// --- AUTH REQUIRED ROUTES --- -router.use(authRequired); - -// Получить реакцию текущего пользователя для медиа -router.get('/:mediaType/:mediaId', async (req, res) => { +// [AUTH] Получить реакцию текущего пользователя для медиа +router.get('/:mediaType/:mediaId/my-reaction', authRequired, async (req, res) => { try { const db = await getDb(); const { mediaType, mediaId } = req.params; const userId = req.user.id; + const fullMediaId = `${mediaType}_${mediaId}`; - const reaction = await db.collection('reactions').findOne({ userId, mediaId, mediaType }); + const reaction = await db.collection('reactions').findOne({ userId, mediaId: fullMediaId }); res.json(reaction); } catch (err) { console.error('Get user reaction error:', err); @@ -79,48 +49,61 @@ router.get('/:mediaType/:mediaId', async (req, res) => { } }); -// Добавить, обновить или удалить реакцию -router.post('/', async (req, res) => { +// [AUTH] Добавить, обновить или удалить реакцию +router.post('/', authRequired, async (req, res) => { try { const db = await getDb(); - const { mediaId, mediaType, type } = req.body; + const { mediaId, type } = req.body; // mediaId здесь это fullMediaId, например "movie_12345" const userId = req.user.id; - if (!mediaId || !mediaType || !type) { - return res.status(400).json({ error: 'mediaId, mediaType, and type are required' }); + if (!mediaId || !type) { + return res.status(400).json({ error: 'mediaId and type are required' }); } if (!VALID_REACTIONS.includes(type)) { return res.status(400).json({ error: 'Invalid reaction type' }); } - const cubId = `${mediaType}_${mediaId}`; - const existingReaction = await db.collection('reactions').findOne({ userId, mediaId, mediaType }); + const existingReaction = await db.collection('reactions').findOne({ userId, mediaId }); if (existingReaction) { + // Если тип реакции тот же, удаляем ее (отмена реакции) if (existingReaction.type === type) { + // Отправляем запрос на удаление в CUB API + await fetch(`${CUB_API_URL}/reactions/remove/${mediaId}/${type}`); await db.collection('reactions').deleteOne({ _id: existingReaction._id }); return res.status(204).send(); } else { - await db.collection('reactions').updateOne( - { _id: existingReaction._id }, - { $set: { type, createdAt: new Date() } } - ); - await fetch(`${CUB_API_URL}/reactions/add/${cubId}/${type}`); + // Если тип другой, обновляем его + // Атомарно выполняем операции с CUB API и базой данных + await Promise.all([ + // 1. Удаляем старую реакцию из CUB API + fetch(`${CUB_API_URL}/reactions/remove/${mediaId}/${existingReaction.type}`), + // 2. Добавляем новую реакцию в CUB API + fetch(`${CUB_API_URL}/reactions/add/${mediaId}/${type}`), + // 3. Обновляем реакцию в нашей базе данных + db.collection('reactions').updateOne( + { _id: existingReaction._id }, + { $set: { type, createdAt: new Date() } } + ) + ]); + const updatedReaction = await db.collection('reactions').findOne({ _id: existingReaction._id }); return res.json(updatedReaction); } } else { + // Если реакции не было, создаем новую + const mediaType = mediaId.split('_')[0]; // Извлекаем 'movie' или 'tv' const newReaction = { userId, - mediaId, + mediaId, // full mediaId, e.g., 'movie_12345' mediaType, type, createdAt: new Date() }; const result = await db.collection('reactions').insertOne(newReaction); - await fetch(`${CUB_API_URL}/reactions/add/${cubId}/${type}`); - + // Отправляем запрос в CUB API + await fetch(`${CUB_API_URL}/reactions/add/${mediaId}/${type}`); const insertedDoc = await db.collection('reactions').findOne({ _id: result.insertedId }); return res.status(201).json(insertedDoc); }