mirror of
https://github.com/movixcorp/MovixOpenSource.git
synced 2026-07-26 16:32:07 +00:00
776 lines
33 KiB
JavaScript
776 lines
33 KiB
JavaScript
/**
|
|
* Link Submissions API Routes
|
|
* System for users to propose streaming links for movies/episodes/seasons
|
|
* Links must be approved by staff before being added
|
|
*
|
|
* Encourages usage of https://seekstreaming.com/ as a preferred source
|
|
*
|
|
* Uses MySQL database (same as the rest of the app)
|
|
*/
|
|
|
|
const express = require('express');
|
|
const jwt = require('jsonwebtoken');
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
const { verifyAccessKey } = require('./checkVip');
|
|
|
|
const JWT_SECRET = process.env.JWT_SECRET;
|
|
|
|
/**
|
|
* Creates and returns the link submissions router
|
|
* @param {Object} mysqlPool - MySQL connection pool from server.js
|
|
* @param {Object} redis - Redis client instance from server.js
|
|
* @returns {express.Router} Express router with link submission routes
|
|
*/
|
|
function createLinkSubmissionsRouter(mysqlPool, redis) {
|
|
const router = express.Router();
|
|
|
|
// =====================================
|
|
// MIDDLEWARE
|
|
// =====================================
|
|
|
|
// Verify JWT token with MySQL session validation (same as wishboardRoutes)
|
|
const requireAuth = async (req, res, next) => {
|
|
const token = req.headers.authorization?.split(' ')[1];
|
|
if (!token) {
|
|
return res.status(401).json({ error: 'Non authentifié' });
|
|
}
|
|
|
|
try {
|
|
const decoded = jwt.verify(token, JWT_SECRET, { algorithms: ['HS256'] });
|
|
const { sub: userId, userType, sessionId } = decoded;
|
|
|
|
if (!['oauth', 'bip39'].includes(userType) || !userId || !sessionId) {
|
|
return res.status(401).json({ error: 'Token invalide' });
|
|
}
|
|
|
|
if (!mysqlPool) {
|
|
return res.status(503).json({ error: 'Service temporairement indisponible' });
|
|
}
|
|
|
|
const [rows] = await mysqlPool.execute(
|
|
'SELECT id FROM user_sessions WHERE id = ? AND user_id = ? AND user_type = ?',
|
|
[sessionId, userId, userType]
|
|
);
|
|
|
|
if (rows.length === 0) {
|
|
return res.status(401).json({ error: 'Session invalide ou expirée' });
|
|
}
|
|
|
|
req.user = { userId, userType, sessionId };
|
|
next();
|
|
} catch (error) {
|
|
return res.status(401).json({ error: 'Token invalide' });
|
|
}
|
|
};
|
|
|
|
// Check admin or uploader status
|
|
const requireUploaderOrAdmin = async (req, res, next) => {
|
|
try {
|
|
if (!mysqlPool) {
|
|
return res.status(503).json({ error: 'Service temporairement indisponible' });
|
|
}
|
|
|
|
const authType = req.user.userType === 'bip39' ? 'bip-39' : req.user.userType;
|
|
const [rows] = await mysqlPool.execute(
|
|
'SELECT * FROM admins WHERE user_id = ? AND auth_type = ?',
|
|
[req.user.userId, authType]
|
|
);
|
|
|
|
if (rows.length === 0) {
|
|
return res.status(403).json({ error: 'Accès refusé - Droits requis' });
|
|
}
|
|
|
|
const role = rows[0].role || 'admin';
|
|
if (role !== 'admin' && role !== 'uploader') {
|
|
return res.status(403).json({ error: 'Accès refusé - Droits insuffisants' });
|
|
}
|
|
|
|
req.admin = { userId: req.user.userId, userType: req.user.userType, role };
|
|
next();
|
|
} catch (error) {
|
|
console.error('Admin/Uploader check error:', error);
|
|
return res.status(500).json({ error: 'Erreur lors de la vérification des droits' });
|
|
}
|
|
};
|
|
|
|
// Get profile ID from headers or body
|
|
const getProfileId = (req) => {
|
|
return req.headers['x-profile-id'] || req.body?.profileId || req.query?.profileId || null;
|
|
};
|
|
|
|
// Verify that the profileId belongs to the authenticated user
|
|
async function verifyProfileOwnership(userId, userType, profileId) {
|
|
try {
|
|
const safeUserId = String(userId).replace(/[^a-zA-Z0-9_\-]/g, '');
|
|
let userFilePath;
|
|
if (userType === 'bip39') {
|
|
userFilePath = path.join(__dirname, 'data', 'users', `bip39-${safeUserId}.json`);
|
|
} else {
|
|
userFilePath = path.join(__dirname, 'data', 'users', `${safeUserId}.json`);
|
|
}
|
|
const userFile = await fs.readFile(userFilePath, 'utf8');
|
|
const user = JSON.parse(userFile);
|
|
if (user.profiles) {
|
|
return user.profiles.some(p => p.id === profileId);
|
|
}
|
|
return false;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Get user data from JSON files
|
|
async function getUserData(userId, userType, profileId = null) {
|
|
try {
|
|
let userData = { username: 'Utilisateur', avatar: null, isVip: false };
|
|
const safeUserId = String(userId).replace(/[^a-zA-Z0-9_\-]/g, '');
|
|
|
|
let userFilePath;
|
|
if (userType === 'bip39') {
|
|
userFilePath = path.join(__dirname, 'data', 'users', `bip39-${safeUserId}.json`);
|
|
} else {
|
|
userFilePath = path.join(__dirname, 'data', 'users', `${safeUserId}.json`);
|
|
}
|
|
|
|
try {
|
|
const userFile = await fs.readFile(userFilePath, 'utf8');
|
|
const user = JSON.parse(userFile);
|
|
if (profileId && user.profiles) {
|
|
const profile = user.profiles.find(p => p.id === profileId);
|
|
if (profile) {
|
|
userData.username = profile.name || 'Utilisateur';
|
|
userData.avatar = profile.avatar;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
// File not found — keep defaults
|
|
}
|
|
|
|
return userData;
|
|
} catch (error) {
|
|
return { username: 'Utilisateur', avatar: null, isVip: false };
|
|
}
|
|
}
|
|
|
|
// Rate limiting: check submission count (15 movies, 10 unique series per 24h)
|
|
const checkSubmissionLimit = async (profileId) => {
|
|
// Count movies submitted in last 24h
|
|
const [movieResult] = await mysqlPool.execute(
|
|
`SELECT COUNT(*) as count FROM link_submissions
|
|
WHERE profile_id = ? AND media_type = 'movie'
|
|
AND created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)`,
|
|
[profileId]
|
|
);
|
|
// Count unique series (by tmdb_id) submitted in last 24h
|
|
const [seriesResult] = await mysqlPool.execute(
|
|
`SELECT COUNT(DISTINCT tmdb_id) as count FROM link_submissions
|
|
WHERE profile_id = ? AND media_type = 'tv'
|
|
AND created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)`,
|
|
[profileId]
|
|
);
|
|
const movieCount = movieResult[0].count;
|
|
const seriesCount = seriesResult[0].count;
|
|
const movieLimit = 15;
|
|
const seriesLimit = 10;
|
|
return {
|
|
movies: { count: movieCount, limit: movieLimit, remaining: Math.max(0, movieLimit - movieCount) },
|
|
series: { count: seriesCount, limit: seriesLimit, remaining: Math.max(0, seriesLimit - seriesCount) },
|
|
};
|
|
};
|
|
|
|
// Check if a specific series tmdb_id was already counted in the 24h window
|
|
const isSeriesAlreadyCounted = async (profileId, tmdbId) => {
|
|
const [result] = await mysqlPool.execute(
|
|
`SELECT COUNT(*) as count FROM link_submissions
|
|
WHERE profile_id = ? AND media_type = 'tv' AND tmdb_id = ?
|
|
AND created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)`,
|
|
[profileId, tmdbId]
|
|
);
|
|
return result[0].count > 0;
|
|
};
|
|
|
|
// =====================================
|
|
// USER ENDPOINTS
|
|
// =====================================
|
|
|
|
/**
|
|
* POST /api/link-submissions
|
|
* Submit a new link for a movie/episode/season
|
|
*/
|
|
router.post('/', requireAuth, async (req, res) => {
|
|
try {
|
|
const { tmdb_id, media_type, season_number, episode_number, url, source_name } = req.body;
|
|
const userId = req.user.userId;
|
|
const profileId = getProfileId(req);
|
|
|
|
if (!profileId) {
|
|
return res.status(400).json({ error: 'Profile ID requis' });
|
|
}
|
|
|
|
const isOwner = await verifyProfileOwnership(userId, req.user.userType, profileId);
|
|
if (!isOwner) {
|
|
return res.status(403).json({ error: 'Ce profil ne vous appartient pas' });
|
|
}
|
|
|
|
if (!tmdb_id || !media_type || !url) {
|
|
return res.status(400).json({ error: 'tmdb_id, media_type et url sont requis' });
|
|
}
|
|
|
|
if (!['movie', 'tv'].includes(media_type)) {
|
|
return res.status(400).json({ error: 'media_type doit être movie ou tv' });
|
|
}
|
|
|
|
// Validate URL format
|
|
try {
|
|
new URL(url);
|
|
} catch {
|
|
return res.status(400).json({ error: 'URL invalide' });
|
|
}
|
|
|
|
// Basic URL sanitization
|
|
const cleanUrl = url.trim();
|
|
if (cleanUrl.length > 2048) {
|
|
return res.status(400).json({ error: 'URL trop longue' });
|
|
}
|
|
|
|
// Check rate limit based on media type
|
|
const limits = await checkSubmissionLimit(profileId);
|
|
if (media_type === 'movie') {
|
|
if (limits.movies.remaining <= 0) {
|
|
return res.status(429).json({
|
|
error: 'Limite de films atteinte (15 films/24h).',
|
|
limit_reached: true
|
|
});
|
|
}
|
|
} else {
|
|
// For TV: only count against limit if it's a NEW series
|
|
const alreadyCounted = await isSeriesAlreadyCounted(profileId, tmdb_id);
|
|
if (!alreadyCounted && limits.series.remaining <= 0) {
|
|
return res.status(429).json({
|
|
error: 'Limite de séries atteinte (10 séries/24h).',
|
|
limit_reached: true
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check for duplicate (same URL for same content)
|
|
const [existing] = await mysqlPool.execute(
|
|
`SELECT id FROM link_submissions WHERE tmdb_id = ? AND media_type = ? AND url = ? AND status != 'rejected'`,
|
|
[tmdb_id, media_type, cleanUrl]
|
|
);
|
|
|
|
if (existing.length > 0) {
|
|
return res.status(409).json({ error: 'Ce lien a déjà été soumis pour ce contenu' });
|
|
}
|
|
|
|
// Insert submission
|
|
const seasonNum = (media_type === 'tv' && season_number !== undefined && season_number !== null) ? parseInt(season_number, 10) : null;
|
|
const episodeNum = (media_type === 'tv' && episode_number !== undefined && episode_number !== null) ? parseInt(episode_number, 10) : null;
|
|
const sourceName = source_name ? source_name.trim().substring(0, 100) : null;
|
|
|
|
const [result] = await mysqlPool.execute(
|
|
`INSERT INTO link_submissions
|
|
(user_id, profile_id, tmdb_id, media_type, season_number, episode_number, url, source_name, status)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending')`,
|
|
[userId, profileId, tmdb_id, media_type, seasonNum, episodeNum, cleanUrl, sourceName]
|
|
);
|
|
|
|
res.status(201).json({
|
|
id: result.insertId,
|
|
message: 'Lien soumis avec succès ! Il sera examiné par notre équipe.'
|
|
});
|
|
} catch (error) {
|
|
console.error('Error creating link submission:', error);
|
|
res.status(500).json({ error: 'Erreur lors de la soumission du lien' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/link-submissions/my
|
|
* Get current user's submissions
|
|
*/
|
|
router.get('/my', requireAuth, async (req, res) => {
|
|
try {
|
|
const profileId = getProfileId(req);
|
|
if (!profileId) {
|
|
return res.status(400).json({ error: 'Profile ID requis' });
|
|
}
|
|
|
|
const isOwner = await verifyProfileOwnership(req.user.userId, req.user.userType, profileId);
|
|
if (!isOwner) {
|
|
return res.status(403).json({ error: 'Ce profil ne vous appartient pas' });
|
|
}
|
|
|
|
const [submissions] = await mysqlPool.execute(
|
|
`SELECT * FROM link_submissions WHERE profile_id = ? ORDER BY created_at DESC LIMIT 50`,
|
|
[profileId]
|
|
);
|
|
|
|
res.json({ submissions });
|
|
} catch (error) {
|
|
console.error('Error fetching user submissions:', error);
|
|
res.status(500).json({ error: 'Erreur lors de la récupération des soumissions' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/link-submissions/limits
|
|
* Get user's submission rate limit status
|
|
*/
|
|
router.get('/limits', requireAuth, async (req, res) => {
|
|
try {
|
|
const profileId = getProfileId(req);
|
|
if (!profileId) {
|
|
return res.status(400).json({ error: 'Profile ID requis' });
|
|
}
|
|
|
|
const isOwner = await verifyProfileOwnership(req.user.userId, req.user.userType, profileId);
|
|
if (!isOwner) {
|
|
return res.status(403).json({ error: 'Ce profil ne vous appartient pas' });
|
|
}
|
|
|
|
const limitStatus = await checkSubmissionLimit(profileId);
|
|
res.json(limitStatus);
|
|
} catch (error) {
|
|
console.error('Error fetching limits:', error);
|
|
res.status(500).json({ error: 'Erreur lors de la récupération des limites' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/link-submissions/bulk
|
|
* Submit links for multiple episodes at once (same series, same URL pattern)
|
|
*/
|
|
router.post('/bulk', requireAuth, async (req, res) => {
|
|
try {
|
|
const { tmdb_id, media_type, season_number, episode_urls, source_name } = req.body;
|
|
const userId = req.user.userId;
|
|
const profileId = getProfileId(req);
|
|
|
|
if (!profileId) return res.status(400).json({ error: 'Profile ID requis' });
|
|
|
|
const isOwner = await verifyProfileOwnership(userId, req.user.userType, profileId);
|
|
if (!isOwner) {
|
|
return res.status(403).json({ error: 'Ce profil ne vous appartient pas' });
|
|
}
|
|
|
|
if (!tmdb_id || media_type !== 'tv') return res.status(400).json({ error: 'tmdb_id et media_type=tv sont requis' });
|
|
if (!episode_urls || typeof episode_urls !== 'object' || Object.keys(episode_urls).length === 0) return res.status(400).json({ error: 'episode_urls requis (objet {episode_number: url})' });
|
|
if (season_number === undefined || season_number === null) return res.status(400).json({ error: 'season_number requis' });
|
|
|
|
// Validate all URLs
|
|
for (const [ep, url] of Object.entries(episode_urls)) {
|
|
if (!url || typeof url !== 'string' || !url.trim()) return res.status(400).json({ error: `URL manquante pour l'épisode ${ep}` });
|
|
try { new URL(url.trim()); } catch { return res.status(400).json({ error: `URL invalide pour l'épisode ${ep}` }); }
|
|
if (url.trim().length > 2048) return res.status(400).json({ error: `URL trop longue pour l'épisode ${ep}` });
|
|
}
|
|
|
|
// Check rate limit (counts as 1 series if not already counted)
|
|
const limits = await checkSubmissionLimit(profileId);
|
|
const alreadyCounted = await isSeriesAlreadyCounted(profileId, tmdb_id);
|
|
if (!alreadyCounted && limits.series.remaining <= 0) {
|
|
return res.status(429).json({ error: 'Limite de séries atteinte (10 séries/24h).', limit_reached: true });
|
|
}
|
|
|
|
const seasonNum = parseInt(season_number, 10);
|
|
const sourceName = source_name ? source_name.trim().substring(0, 100) : null;
|
|
let inserted = 0;
|
|
let duplicates = 0;
|
|
|
|
for (const [epNumStr, epUrl] of Object.entries(episode_urls)) {
|
|
const episodeNum = parseInt(epNumStr, 10);
|
|
const cleanUrl = String(epUrl).trim();
|
|
// Check duplicate
|
|
const [existing] = await mysqlPool.execute(
|
|
`SELECT id FROM link_submissions WHERE tmdb_id = ? AND media_type = 'tv' AND season_number = ? AND episode_number = ? AND url = ? AND status != 'rejected'`,
|
|
[tmdb_id, seasonNum, episodeNum, cleanUrl]
|
|
);
|
|
if (existing.length > 0) { duplicates++; continue; }
|
|
|
|
await mysqlPool.execute(
|
|
`INSERT INTO link_submissions (user_id, profile_id, tmdb_id, media_type, season_number, episode_number, url, source_name, status) VALUES (?, ?, ?, 'tv', ?, ?, ?, ?, 'pending')`,
|
|
[userId, profileId, tmdb_id, seasonNum, episodeNum, cleanUrl, sourceName]
|
|
);
|
|
inserted++;
|
|
}
|
|
|
|
res.status(201).json({ message: `${inserted} épisode(s) soumis`, inserted, duplicates });
|
|
} catch (error) {
|
|
console.error('Error bulk submitting:', error);
|
|
res.status(500).json({ error: 'Erreur lors de la soumission en masse' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* DELETE /api/link-submissions/:id
|
|
* User can delete their own pending submission
|
|
*/
|
|
router.delete('/:id', requireAuth, async (req, res) => {
|
|
try {
|
|
const submissionId = req.params.id;
|
|
const profileId = getProfileId(req);
|
|
|
|
if (!profileId) {
|
|
return res.status(400).json({ error: 'Profile ID requis' });
|
|
}
|
|
|
|
const isOwner = await verifyProfileOwnership(req.user.userId, req.user.userType, profileId);
|
|
if (!isOwner) {
|
|
return res.status(403).json({ error: 'Ce profil ne vous appartient pas' });
|
|
}
|
|
|
|
// Only allow deleting own submissions that are still pending
|
|
const [result] = await mysqlPool.execute(
|
|
`DELETE FROM link_submissions WHERE id = ? AND profile_id = ? AND status = 'pending'`,
|
|
[submissionId, profileId]
|
|
);
|
|
|
|
if (result.affectedRows === 0) {
|
|
return res.status(404).json({ error: 'Soumission non trouvée ou déjà traitée' });
|
|
}
|
|
|
|
res.json({ message: 'Soumission supprimée' });
|
|
} catch (error) {
|
|
console.error('Error deleting submission:', error);
|
|
res.status(500).json({ error: 'Erreur lors de la suppression' });
|
|
}
|
|
});
|
|
|
|
// =====================================
|
|
// ADMIN ENDPOINTS
|
|
// =====================================
|
|
|
|
/**
|
|
* GET /api/link-submissions/admin
|
|
* Admin: list all submissions with filters
|
|
*/
|
|
router.get('/admin', requireAuth, requireUploaderOrAdmin, async (req, res) => {
|
|
try {
|
|
const { status = 'pending', media_type, search, page = 1, limit = 50 } = req.query;
|
|
const offset = (parseInt(page) - 1) * parseInt(limit);
|
|
|
|
let whereConditions = ['1=1'];
|
|
let whereParams = [];
|
|
|
|
if (status && status !== 'all') {
|
|
whereConditions.push('ls.status = ?');
|
|
whereParams.push(status);
|
|
}
|
|
|
|
if (media_type && media_type !== 'all') {
|
|
whereConditions.push('ls.media_type = ?');
|
|
whereParams.push(media_type);
|
|
}
|
|
|
|
if (search) {
|
|
const numSearch = parseInt(search);
|
|
if (!isNaN(numSearch)) {
|
|
whereConditions.push('(ls.tmdb_id = ? OR ls.id = ?)');
|
|
whereParams.push(numSearch, numSearch);
|
|
} else {
|
|
whereConditions.push('(ls.url LIKE ? OR ls.source_name LIKE ?)');
|
|
whereParams.push(`%${search}%`, `%${search}%`);
|
|
}
|
|
}
|
|
|
|
const whereClause = whereConditions.join(' AND ');
|
|
|
|
const [submissions] = await mysqlPool.execute(
|
|
`SELECT ls.* FROM link_submissions ls
|
|
WHERE ${whereClause}
|
|
ORDER BY ls.created_at DESC
|
|
LIMIT ? OFFSET ?`,
|
|
[...whereParams, parseInt(limit), offset]
|
|
);
|
|
|
|
// Enrich with user data
|
|
const enriched = await Promise.all(submissions.map(async (sub) => {
|
|
let user = { username: 'Inconnu', avatar: null };
|
|
try {
|
|
let userType = 'oauth';
|
|
try {
|
|
await fs.access(path.join(__dirname, 'data', 'users', `bip39-${sub.user_id}.json`));
|
|
userType = 'bip39';
|
|
} catch { /* keep oauth */ }
|
|
user = await getUserData(sub.user_id, userType, sub.profile_id);
|
|
} catch { /* ignore */ }
|
|
return { ...sub, user };
|
|
}));
|
|
|
|
// Get stats
|
|
const [statsResult] = await mysqlPool.execute(`
|
|
SELECT
|
|
COUNT(*) as total,
|
|
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
|
|
SUM(CASE WHEN status = 'approved' THEN 1 ELSE 0 END) as approved,
|
|
SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected
|
|
FROM link_submissions
|
|
`);
|
|
|
|
// Get total count for pagination
|
|
const [countResult] = await mysqlPool.execute(
|
|
`SELECT COUNT(*) as total FROM link_submissions ls WHERE ${whereClause}`,
|
|
whereParams
|
|
);
|
|
|
|
res.json({
|
|
submissions: enriched,
|
|
has_more: countResult[0].total > offset + submissions.length,
|
|
stats: {
|
|
total: statsResult[0].total || 0,
|
|
pending: statsResult[0].pending || 0,
|
|
approved: statsResult[0].approved || 0,
|
|
rejected: statsResult[0].rejected || 0
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching admin submissions:', error);
|
|
res.status(500).json({ error: 'Erreur lors de la récupération des soumissions' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PUT /api/link-submissions/admin/:id/approve
|
|
* Admin: approve a submission and add the link to films/series table
|
|
*/
|
|
router.put('/admin/:id/approve', requireAuth, requireUploaderOrAdmin, async (req, res) => {
|
|
try {
|
|
const submissionId = req.params.id;
|
|
|
|
// Get the submission
|
|
const [submissions] = await mysqlPool.execute(
|
|
'SELECT * FROM link_submissions WHERE id = ?',
|
|
[submissionId]
|
|
);
|
|
|
|
if (submissions.length === 0) {
|
|
return res.status(404).json({ error: 'Soumission non trouvée' });
|
|
}
|
|
|
|
const sub = submissions[0];
|
|
|
|
if (sub.status !== 'pending') {
|
|
return res.status(400).json({ error: 'Cette soumission a déjà été traitée' });
|
|
}
|
|
|
|
// Add the link to the appropriate table
|
|
if (sub.media_type === 'movie') {
|
|
// Check if film exists
|
|
const [existing] = await mysqlPool.execute('SELECT id, links FROM films WHERE id = ?', [sub.tmdb_id]);
|
|
|
|
if (existing.length > 0) {
|
|
// Merge link
|
|
let currentLinks = [];
|
|
try {
|
|
currentLinks = JSON.parse(existing[0].links || '[]');
|
|
} catch { currentLinks = []; }
|
|
|
|
if (!currentLinks.includes(sub.url)) {
|
|
currentLinks.push(sub.url);
|
|
}
|
|
|
|
await mysqlPool.execute(
|
|
'UPDATE films SET links = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
|
[JSON.stringify(currentLinks), sub.tmdb_id]
|
|
);
|
|
} else {
|
|
// Create new entry
|
|
await mysqlPool.execute(
|
|
'INSERT INTO films (id, links) VALUES (?, ?)',
|
|
[sub.tmdb_id, JSON.stringify([sub.url])]
|
|
);
|
|
}
|
|
} else if (sub.media_type === 'tv') {
|
|
const seasonNum = sub.season_number;
|
|
const episodeNum = sub.episode_number;
|
|
|
|
if (seasonNum !== null && episodeNum !== null) {
|
|
// Specific episode
|
|
const [existing] = await mysqlPool.execute(
|
|
'SELECT id, links FROM series WHERE series_id = ? AND season_number = ? AND episode_number = ?',
|
|
[sub.tmdb_id, seasonNum, episodeNum]
|
|
);
|
|
|
|
if (existing.length > 0) {
|
|
let currentLinks = [];
|
|
try {
|
|
currentLinks = JSON.parse(existing[0].links || '[]');
|
|
} catch { currentLinks = []; }
|
|
|
|
if (!currentLinks.includes(sub.url)) {
|
|
currentLinks.push(sub.url);
|
|
}
|
|
|
|
await mysqlPool.execute(
|
|
'UPDATE series SET links = ? WHERE series_id = ? AND season_number = ? AND episode_number = ?',
|
|
[JSON.stringify(currentLinks), sub.tmdb_id, seasonNum, episodeNum]
|
|
);
|
|
} else {
|
|
await mysqlPool.execute(
|
|
'INSERT INTO series (series_id, season_number, episode_number, links) VALUES (?, ?, ?, ?)',
|
|
[sub.tmdb_id, seasonNum, episodeNum, JSON.stringify([sub.url])]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update submission status
|
|
await mysqlPool.execute(
|
|
`UPDATE link_submissions SET status = 'approved', reviewed_by = ?, reviewed_at = NOW() WHERE id = ?`,
|
|
[req.admin.userId, submissionId]
|
|
);
|
|
|
|
res.json({ message: 'Lien approuvé et ajouté avec succès' });
|
|
} catch (error) {
|
|
console.error('Error approving submission:', error);
|
|
res.status(500).json({ error: 'Erreur lors de l\'approbation' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PUT /api/link-submissions/admin/:id/reject
|
|
* Admin: reject a submission with optional reason
|
|
*/
|
|
router.put('/admin/:id/reject', requireAuth, requireUploaderOrAdmin, async (req, res) => {
|
|
try {
|
|
const submissionId = req.params.id;
|
|
const { reason } = req.body;
|
|
|
|
const [submissions] = await mysqlPool.execute(
|
|
'SELECT * FROM link_submissions WHERE id = ?',
|
|
[submissionId]
|
|
);
|
|
|
|
if (submissions.length === 0) {
|
|
return res.status(404).json({ error: 'Soumission non trouvée' });
|
|
}
|
|
|
|
if (submissions[0].status !== 'pending') {
|
|
return res.status(400).json({ error: 'Cette soumission a déjà été traitée' });
|
|
}
|
|
|
|
await mysqlPool.execute(
|
|
`UPDATE link_submissions SET status = 'rejected', rejection_reason = ?, reviewed_by = ?, reviewed_at = NOW() WHERE id = ?`,
|
|
[reason || null, req.admin.userId, submissionId]
|
|
);
|
|
|
|
res.json({ message: 'Soumission rejetée' });
|
|
} catch (error) {
|
|
console.error('Error rejecting submission:', error);
|
|
res.status(500).json({ error: 'Erreur lors du rejet' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PUT /api/link-submissions/admin/bulk-approve
|
|
* Admin: approve multiple submissions at once
|
|
*/
|
|
router.put('/admin/bulk-approve', requireAuth, requireUploaderOrAdmin, async (req, res) => {
|
|
try {
|
|
const { ids } = req.body;
|
|
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
|
return res.status(400).json({ error: 'IDs requis' });
|
|
}
|
|
|
|
let approved = 0;
|
|
let errors = 0;
|
|
|
|
for (const id of ids) {
|
|
try {
|
|
const [submissions] = await mysqlPool.execute(
|
|
'SELECT * FROM link_submissions WHERE id = ? AND status = ?',
|
|
[id, 'pending']
|
|
);
|
|
|
|
if (submissions.length === 0) {
|
|
errors++;
|
|
continue;
|
|
}
|
|
|
|
const sub = submissions[0];
|
|
|
|
// Add link to appropriate table
|
|
if (sub.media_type === 'movie') {
|
|
const [existing] = await mysqlPool.execute('SELECT id, links FROM films WHERE id = ?', [sub.tmdb_id]);
|
|
if (existing.length > 0) {
|
|
let currentLinks = [];
|
|
try { currentLinks = JSON.parse(existing[0].links || '[]'); } catch { currentLinks = []; }
|
|
if (!currentLinks.includes(sub.url)) currentLinks.push(sub.url);
|
|
await mysqlPool.execute('UPDATE films SET links = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [JSON.stringify(currentLinks), sub.tmdb_id]);
|
|
} else {
|
|
await mysqlPool.execute('INSERT INTO films (id, links) VALUES (?, ?)', [sub.tmdb_id, JSON.stringify([sub.url])]);
|
|
}
|
|
} else if (sub.media_type === 'tv' && sub.season_number !== null && sub.episode_number !== null) {
|
|
const [existing] = await mysqlPool.execute('SELECT id, links FROM series WHERE series_id = ? AND season_number = ? AND episode_number = ?', [sub.tmdb_id, sub.season_number, sub.episode_number]);
|
|
if (existing.length > 0) {
|
|
let currentLinks = [];
|
|
try { currentLinks = JSON.parse(existing[0].links || '[]'); } catch { currentLinks = []; }
|
|
if (!currentLinks.includes(sub.url)) currentLinks.push(sub.url);
|
|
await mysqlPool.execute('UPDATE series SET links = ? WHERE series_id = ? AND season_number = ? AND episode_number = ?', [JSON.stringify(currentLinks), sub.tmdb_id, sub.season_number, sub.episode_number]);
|
|
} else {
|
|
await mysqlPool.execute('INSERT INTO series (series_id, season_number, episode_number, links) VALUES (?, ?, ?, ?)', [sub.tmdb_id, sub.season_number, sub.episode_number, JSON.stringify([sub.url])]);
|
|
}
|
|
}
|
|
|
|
await mysqlPool.execute(
|
|
`UPDATE link_submissions SET status = 'approved', reviewed_by = ?, reviewed_at = NOW() WHERE id = ?`,
|
|
[req.admin.userId, id]
|
|
);
|
|
approved++;
|
|
} catch (err) {
|
|
console.error(`Error approving submission ${id}:`, err);
|
|
errors++;
|
|
}
|
|
}
|
|
|
|
res.json({ message: `${approved} lien(s) approuvé(s)`, approved, errors });
|
|
} catch (error) {
|
|
console.error('Error bulk approving:', error);
|
|
res.status(500).json({ error: 'Erreur lors de l\'approbation en masse' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PUT /api/link-submissions/admin/bulk-reject
|
|
* Admin: reject multiple submissions at once
|
|
*/
|
|
router.put('/admin/bulk-reject', requireAuth, requireUploaderOrAdmin, async (req, res) => {
|
|
try {
|
|
const { ids, reason } = req.body;
|
|
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
|
return res.status(400).json({ error: 'IDs requis' });
|
|
}
|
|
|
|
const placeholders = ids.map(() => '?').join(',');
|
|
await mysqlPool.execute(
|
|
`UPDATE link_submissions SET status = 'rejected', rejection_reason = ?, reviewed_by = ?, reviewed_at = NOW() WHERE id IN (${placeholders}) AND status = 'pending'`,
|
|
[reason || null, req.admin.userId, ...ids]
|
|
);
|
|
|
|
res.json({ message: `${ids.length} soumission(s) rejetée(s)` });
|
|
} catch (error) {
|
|
console.error('Error bulk rejecting:', error);
|
|
res.status(500).json({ error: 'Erreur lors du rejet en masse' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* DELETE /api/link-submissions/admin/:id
|
|
* Admin: permanently delete a submission
|
|
*/
|
|
router.delete('/admin/:id', requireAuth, requireUploaderOrAdmin, async (req, res) => {
|
|
try {
|
|
const submissionId = req.params.id;
|
|
await mysqlPool.execute('DELETE FROM link_submissions WHERE id = ?', [submissionId]);
|
|
res.json({ message: 'Soumission supprimée' });
|
|
} catch (error) {
|
|
console.error('Error deleting submission:', error);
|
|
res.status(500).json({ error: 'Erreur lors de la suppression' });
|
|
}
|
|
});
|
|
|
|
return router;
|
|
}
|
|
|
|
module.exports = { createLinkSubmissionsRouter };
|