mirror of
https://gitlab.com/foxixus/neomovies-api.git
synced 2025-10-28 01:48:51 +05:00
fix auth, reactions and etc
This commit is contained in:
14
src/index.js
14
src/index.js
@@ -266,13 +266,13 @@ const routerToUse = reactionsRouter.default || reactionsRouter;
|
|||||||
require('./utils/cleanup');
|
require('./utils/cleanup');
|
||||||
const authRouter = require('./routes/auth');
|
const authRouter = require('./routes/auth');
|
||||||
|
|
||||||
app.use('/api/movies', moviesRouter);
|
app.use('/movies', moviesRouter);
|
||||||
app.use('/api/tv', tvRouter);
|
app.use('/tv', tvRouter);
|
||||||
app.use('/api/images', imagesRouter);
|
app.use('/images', imagesRouter);
|
||||||
app.use('/api/categories', categoriesRouter);
|
app.use('/categories', categoriesRouter);
|
||||||
app.use('/api/favorites', favoritesRouter);
|
app.use('/favorites', favoritesRouter);
|
||||||
app.use('/api/players', playersRouter);
|
app.use('/players', playersRouter);
|
||||||
app.use('/api/reactions', routerToUse);
|
app.use('/reactions', routerToUse);
|
||||||
app.use('/auth', authRouter);
|
app.use('/auth', authRouter);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ const bcrypt = require('bcrypt');
|
|||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
const { getDb } = require('../db');
|
const { getDb } = require('../db');
|
||||||
|
const { ObjectId } = require('mongodb');
|
||||||
const { sendVerificationEmail } = require('../utils/mailer');
|
const { sendVerificationEmail } = require('../utils/mailer');
|
||||||
|
const authRequired = require('../middleware/auth');
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @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;
|
module.exports = router;
|
||||||
|
|||||||
@@ -1,7 +1,34 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const { formatDate } = require('../utils/date');
|
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 для логирования запросов
|
// Middleware для логирования запросов
|
||||||
router.use((req, res, next) => {
|
router.use((req, res, next) => {
|
||||||
console.log('Movies API Request:', {
|
console.log('Movies API Request:', {
|
||||||
@@ -89,16 +116,8 @@ router.get('/search', async (req, res) => {
|
|||||||
results_count: data.results?.length
|
results_count: data.results?.length
|
||||||
});
|
});
|
||||||
|
|
||||||
// Форматируем даты в результатах
|
const formattedResults = filterAndFormat(data.results);
|
||||||
const formattedResults = data.results.map(movie => ({
|
res.json({ ...data, results: formattedResults });
|
||||||
...movie,
|
|
||||||
release_date: formatDate(movie.release_date)
|
|
||||||
}));
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
...data,
|
|
||||||
results: formattedResults
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error searching movies:', error);
|
console.error('Error searching movies:', error);
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -168,18 +187,10 @@ router.get('/search/multi', async (req, res) => { // Путь должен бы
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Объединяем и сортируем результаты по популярности
|
// Объединяем и сортируем результаты по популярности
|
||||||
const combinedResults = [
|
const combinedResults = filterAndFormat([
|
||||||
...moviesData.results.map(movie => ({
|
...moviesData.results.map(item => ({ ...item, media_type: 'movie' })),
|
||||||
...movie,
|
...tvData.results.map(item => ({ ...item, media_type: 'tv' }))
|
||||||
media_type: 'movie',
|
]).sort((a, b) => b.popularity - a.popularity);
|
||||||
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 itemsPerPage = 20;
|
const itemsPerPage = 20;
|
||||||
@@ -263,10 +274,7 @@ router.get('/popular', async (req, res) => {
|
|||||||
throw new Error('Invalid response from TMDB');
|
throw new Error('Invalid response from TMDB');
|
||||||
}
|
}
|
||||||
|
|
||||||
const formattedResults = movies.results.map(movie => ({
|
const formattedResults = filterAndFormat(movies.results);
|
||||||
...movie,
|
|
||||||
release_date: formatDate(movie.release_date)
|
|
||||||
}));
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
...movies,
|
...movies,
|
||||||
@@ -333,10 +341,7 @@ router.get('/top-rated', async (req, res) => {
|
|||||||
throw new Error('Invalid response from TMDB');
|
throw new Error('Invalid response from TMDB');
|
||||||
}
|
}
|
||||||
|
|
||||||
const formattedResults = movies.results.map(movie => ({
|
const formattedResults = filterAndFormat(movies.results);
|
||||||
...movie,
|
|
||||||
release_date: formatDate(movie.release_date)
|
|
||||||
}));
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
...movies,
|
...movies,
|
||||||
@@ -523,10 +528,7 @@ router.get('/upcoming', async (req, res) => {
|
|||||||
throw new Error('Invalid response from TMDB');
|
throw new Error('Invalid response from TMDB');
|
||||||
}
|
}
|
||||||
|
|
||||||
const formattedResults = movies.results.map(movie => ({
|
const formattedResults = filterAndFormat(movies.results);
|
||||||
...movie,
|
|
||||||
release_date: formatDate(movie.release_date)
|
|
||||||
}));
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
...movies,
|
...movies,
|
||||||
|
|||||||
@@ -9,18 +9,19 @@ const router = Router();
|
|||||||
const CUB_API_URL = 'https://cub.rip/api';
|
const CUB_API_URL = 'https://cub.rip/api';
|
||||||
const VALID_REACTIONS = ['fire', 'nice', 'think', 'bore', 'shit'];
|
const VALID_REACTIONS = ['fire', 'nice', 'think', 'bore', 'shit'];
|
||||||
|
|
||||||
// Получить все счетчики реакций для медиа
|
// [PUBLIC] Получить все счетчики реакций для медиа
|
||||||
router.get('/:mediaId/counts', async (req, res) => {
|
router.get('/:mediaType/:mediaId/counts', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { mediaId } = req.params;
|
const { mediaType, mediaId } = req.params;
|
||||||
const response = await fetch(`${CUB_API_URL}/reactions/get/${mediaId}`);
|
const cubId = `${mediaType}_${mediaId}`;
|
||||||
|
const response = await fetch(`${CUB_API_URL}/reactions/get/${cubId}`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`CUB API request failed with status ${response.status}`);
|
// Возвращаем пустой объект, если на CUB.RIP еще нет реакций
|
||||||
|
return res.json({});
|
||||||
}
|
}
|
||||||
const data = await response.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;
|
acc[reaction.type] = reaction.counter;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
@@ -32,46 +33,15 @@ router.get('/:mediaId/counts', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- PUBLIC ROUTE ---
|
// [AUTH] Получить реакцию текущего пользователя для медиа
|
||||||
// Получить общее количество реакций для медиа
|
router.get('/:mediaType/:mediaId/my-reaction', authRequired, async (req, res) => {
|
||||||
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) => {
|
|
||||||
try {
|
try {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
const { mediaType, mediaId } = req.params;
|
const { mediaType, mediaId } = req.params;
|
||||||
const userId = req.user.id;
|
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);
|
res.json(reaction);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Get user reaction error:', err);
|
console.error('Get user reaction error:', err);
|
||||||
@@ -79,48 +49,61 @@ router.get('/:mediaType/:mediaId', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Добавить, обновить или удалить реакцию
|
// [AUTH] Добавить, обновить или удалить реакцию
|
||||||
router.post('/', async (req, res) => {
|
router.post('/', authRequired, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
const { mediaId, mediaType, type } = req.body;
|
const { mediaId, type } = req.body; // mediaId здесь это fullMediaId, например "movie_12345"
|
||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
|
|
||||||
if (!mediaId || !mediaType || !type) {
|
if (!mediaId || !type) {
|
||||||
return res.status(400).json({ error: 'mediaId, mediaType, and type are required' });
|
return res.status(400).json({ error: 'mediaId and type are required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!VALID_REACTIONS.includes(type)) {
|
if (!VALID_REACTIONS.includes(type)) {
|
||||||
return res.status(400).json({ error: 'Invalid reaction type' });
|
return res.status(400).json({ error: 'Invalid reaction type' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const cubId = `${mediaType}_${mediaId}`;
|
const existingReaction = await db.collection('reactions').findOne({ userId, mediaId });
|
||||||
const existingReaction = await db.collection('reactions').findOne({ userId, mediaId, mediaType });
|
|
||||||
|
|
||||||
if (existingReaction) {
|
if (existingReaction) {
|
||||||
|
// Если тип реакции тот же, удаляем ее (отмена реакции)
|
||||||
if (existingReaction.type === type) {
|
if (existingReaction.type === type) {
|
||||||
|
// Отправляем запрос на удаление в CUB API
|
||||||
|
await fetch(`${CUB_API_URL}/reactions/remove/${mediaId}/${type}`);
|
||||||
await db.collection('reactions').deleteOne({ _id: existingReaction._id });
|
await db.collection('reactions').deleteOne({ _id: existingReaction._id });
|
||||||
return res.status(204).send();
|
return res.status(204).send();
|
||||||
} else {
|
} else {
|
||||||
await db.collection('reactions').updateOne(
|
// Если тип другой, обновляем его
|
||||||
{ _id: existingReaction._id },
|
// Атомарно выполняем операции с CUB API и базой данных
|
||||||
{ $set: { type, createdAt: new Date() } }
|
await Promise.all([
|
||||||
);
|
// 1. Удаляем старую реакцию из CUB API
|
||||||
await fetch(`${CUB_API_URL}/reactions/add/${cubId}/${type}`);
|
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 });
|
const updatedReaction = await db.collection('reactions').findOne({ _id: existingReaction._id });
|
||||||
return res.json(updatedReaction);
|
return res.json(updatedReaction);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Если реакции не было, создаем новую
|
||||||
|
const mediaType = mediaId.split('_')[0]; // Извлекаем 'movie' или 'tv'
|
||||||
const newReaction = {
|
const newReaction = {
|
||||||
userId,
|
userId,
|
||||||
mediaId,
|
mediaId, // full mediaId, e.g., 'movie_12345'
|
||||||
mediaType,
|
mediaType,
|
||||||
type,
|
type,
|
||||||
createdAt: new Date()
|
createdAt: new Date()
|
||||||
};
|
};
|
||||||
const result = await db.collection('reactions').insertOne(newReaction);
|
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 });
|
const insertedDoc = await db.collection('reactions').findOne({ _id: result.insertedId });
|
||||||
return res.status(201).json(insertedDoc);
|
return res.status(201).json(insertedDoc);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user