From 864cbaece919eadb33c846cc57a656ead780af8f Mon Sep 17 00:00:00 2001 From: Mike Johnston Date: Thu, 29 Jan 2026 02:00:55 -0500 Subject: [PATCH] bugfix --- backend/src/routes/auth.js | 193 ++++++++++------------ backend/src/routes/challenges.js | 206 +++++++++++------------ backend/src/routes/friends.js | 262 ++++++++++++++---------------- backend/src/routes/leaderboard.js | 130 +++++++-------- backend/src/routes/predictions.js | 226 ++++++++++++-------------- backend/src/routes/tmdb.js | 129 +++++++-------- 6 files changed, 525 insertions(+), 621 deletions(-) diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index ebf9349..59be7fc 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -2,121 +2,108 @@ import express from 'express'; import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; import { query } from '../db/index.js'; +import { asyncHandler, AppError } from '../middleware/errorHandler.js'; const router = express.Router(); // Register -router.post('/register', async (req, res) => { - try { - const { email, username, password } = req.body; +router.post('/register', asyncHandler(async (req, res) => { + const { email, username, password } = req.body; - if (!email || !username || !password) { - return res.status(400).json({ error: 'All fields required' }); - } - - // Check if user exists - const existing = await query( - 'SELECT id FROM users WHERE email = ? OR username = ?', - [email, username] - ); - - if (existing.length > 0) { - return res.status(400).json({ error: 'User already exists' }); - } - - // Hash password - const password_hash = await bcrypt.hash(password, 10); - - // Create user - const result = await query( - 'INSERT INTO users (email, username, password_hash) VALUES (?, ?, ?)', - [email, username, password_hash] - ); - - const userId = result.insertId; - - // Generate token - const token = jwt.sign( - { userId, email, username }, - process.env.JWT_SECRET, - { expiresIn: '30d' } - ); - - res.json({ token, user: { id: userId, email, username } }); - } catch (error) { - console.error('Register error:', error); - res.status(500).json({ error: 'Registration failed' }); + if (!email || !username || !password) { + throw new AppError('All fields required', 400); } -}); + + // Check if user exists + const existing = await query( + 'SELECT id FROM users WHERE email = ? OR username = ?', + [email, username] + ); + + if (existing.length > 0) { + throw new AppError('User already exists', 400); + } + + // Hash password + const password_hash = await bcrypt.hash(password, 10); + + // Create user + const result = await query( + 'INSERT INTO users (email, username, password_hash) VALUES (?, ?, ?)', + [email, username, password_hash] + ); + + const userId = result.insertId; + + // Generate token + const token = jwt.sign( + { userId, email, username }, + process.env.JWT_SECRET, + { expiresIn: '30d' } + ); + + res.json({ token, user: { id: userId, email, username } }); +})); // Login -router.post('/login', async (req, res) => { - try { - const { email, password } = req.body; +router.post('/login', asyncHandler(async (req, res) => { + const { email, password } = req.body; - if (!email || !password) { - return res.status(400).json({ error: 'Email and password required' }); - } - - // Find user - const users = await query( - 'SELECT id, email, username, password_hash FROM users WHERE email = ?', - [email] - ); - - if (users.length === 0) { - return res.status(401).json({ error: 'Invalid credentials' }); - } - - const user = users[0]; - - // Check password - const validPassword = await bcrypt.compare(password, user.password_hash); - if (!validPassword) { - return res.status(401).json({ error: 'Invalid credentials' }); - } - - // Generate token - const token = jwt.sign( - { userId: user.id, email: user.email, username: user.username }, - process.env.JWT_SECRET, - { expiresIn: '30d' } - ); - - res.json({ - token, - user: { id: user.id, email: user.email, username: user.username } - }); - } catch (error) { - console.error('Login error:', error); - res.status(500).json({ error: 'Login failed' }); + if (!email || !password) { + throw new AppError('Email and password required', 400); } -}); + + // Find user + const users = await query( + 'SELECT id, email, username, password_hash FROM users WHERE email = ?', + [email] + ); + + if (users.length === 0) { + throw new AppError('Invalid credentials', 401); + } + + const user = users[0]; + + // Check password + const validPassword = await bcrypt.compare(password, user.password_hash); + if (!validPassword) { + throw new AppError('Invalid credentials', 401); + } + + // Generate token + const token = jwt.sign( + { userId: user.id, email: user.email, username: user.username }, + process.env.JWT_SECRET, + { expiresIn: '30d' } + ); + + res.json({ + token, + user: { id: user.id, email: user.email, username: user.username } + }); +})); // Get current user -router.get('/me', async (req, res) => { - try { - const authHeader = req.headers.authorization; - if (!authHeader || !authHeader.startsWith('Bearer ')) { - return res.status(401).json({ error: 'No token provided' }); - } - - const token = authHeader.substring(7); - const decoded = jwt.verify(token, process.env.JWT_SECRET); - - const users = await query( - 'SELECT id, email, username, created_at FROM users WHERE id = ?', - [decoded.userId] - ); - - if (users.length === 0) { - return res.status(404).json({ error: 'User not found' }); - } - - res.json({ user: users[0] }); - } catch (error) { - res.status(401).json({ error: 'Invalid token' }); +router.get('/me', asyncHandler(async (req, res) => { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + throw new AppError('No token provided', 401); } -}); + + const token = authHeader.substring(7); + const decoded = jwt.verify(token, process.env.JWT_SECRET); + + const users = await query( + 'SELECT id, email, username, created_at FROM users WHERE id = ?', + [decoded.userId] + ); + + if (users.length === 0) { + throw new AppError('User not found', 404); + } + + res.json({ user: users[0] }); +})); export default router; diff --git a/backend/src/routes/challenges.js b/backend/src/routes/challenges.js index 00d2c6f..cb6dd48 100644 --- a/backend/src/routes/challenges.js +++ b/backend/src/routes/challenges.js @@ -1,63 +1,58 @@ import express from 'express'; import { query } from '../db/index.js'; import { authMiddleware } from '../middleware/auth.js'; +import { asyncHandler, AppError } from '../middleware/errorHandler.js'; const router = express.Router(); // Get all challenges for the current user -router.get('/', authMiddleware, async (req, res) => { - try { - const challenges = await query( - `SELECT - c.*, - u.username as creator_username, - cp.status as participation_status, - (SELECT COUNT(*) FROM predictions WHERE challenge_id = c.id AND status = 'validated' AND user_id = ?) as my_points - FROM challenges c - INNER JOIN users u ON c.created_by = u.id - LEFT JOIN challenge_participants cp ON cp.challenge_id = c.id AND cp.user_id = ? - WHERE c.created_by = ? OR cp.user_id IS NOT NULL - ORDER BY c.created_at DESC`, - [req.user.userId, req.user.userId, req.user.userId] - ); +router.get('/', authMiddleware, asyncHandler(async (req, res) => { + const challenges = await query( + `SELECT + c.*, + u.username as creator_username, + cp.status as participation_status, + (SELECT COUNT(*) FROM predictions WHERE challenge_id = c.id AND status = 'validated' AND user_id = ?) as my_points + FROM challenges c + INNER JOIN users u ON c.created_by = u.id + LEFT JOIN challenge_participants cp ON cp.challenge_id = c.id AND cp.user_id = ? + WHERE c.created_by = ? OR cp.user_id IS NOT NULL + ORDER BY c.created_at DESC`, + [req.user.userId, req.user.userId, req.user.userId] + ); - res.json({ challenges }); - } catch (error) { - console.error('Get challenges error:', error); - res.status(500).json({ error: 'Failed to fetch challenges' }); - } -}); + res.json({ challenges }); +})); // Get a single challenge with details -router.get('/:id', authMiddleware, async (req, res) => { - try { - const challengeId = req.params.id; +router.get('/:id', authMiddleware, asyncHandler(async (req, res) => { + const challengeId = req.params.id; - // Get challenge details - const challenges = await query( - `SELECT c.*, u.username as creator_username - FROM challenges c - INNER JOIN users u ON c.created_by = u.id - WHERE c.id = ?`, - [challengeId] - ); + // Get challenge details + const challenges = await query( + `SELECT c.*, u.username as creator_username + FROM challenges c + INNER JOIN users u ON c.created_by = u.id + WHERE c.id = ?`, + [challengeId] + ); - if (challenges.length === 0) { - return res.status(404).json({ error: 'Challenge not found' }); - } + if (challenges.length === 0) { + throw new AppError('Challenge not found', 404); + } - const challenge = challenges[0]; + const challenge = challenges[0]; - // Check if user has access - const access = await query( - `SELECT * FROM challenge_participants - WHERE challenge_id = ? AND user_id = ? AND status = 'accepted'`, - [challengeId, req.user.userId] - ); + // Check if user has access + const access = await query( + `SELECT * FROM challenge_participants + WHERE challenge_id = ? AND user_id = ? AND status = 'accepted'`, + [challengeId, req.user.userId] + ); - if (challenge.created_by !== req.user.userId && access.length === 0) { - return res.status(403).json({ error: 'Access denied' }); - } + if (challenge.created_by !== req.user.userId && access.length === 0) { + throw new AppError('Access denied', 403); + } // Get participants with their points const participants = await query( @@ -79,25 +74,20 @@ router.get('/:id', authMiddleware, async (req, res) => { [challengeId, challenge.created_by] ); - res.json({ - challenge, - participants, - creator_points: creatorPoints[0].points - }); - } catch (error) { - console.error('Get challenge error:', error); - res.status(500).json({ error: 'Failed to fetch challenge' }); - } -}); + res.json({ + challenge, + participants, + creator_points: creatorPoints[0].points + }); +})); // Create a new challenge -router.post('/', authMiddleware, async (req, res) => { - try { - const { title, cover_image_url, tmdb_id, media_type } = req.body; +router.post('/', authMiddleware, asyncHandler(async (req, res) => { + const { title, cover_image_url, tmdb_id, media_type } = req.body; - if (!title) { - return res.status(400).json({ error: 'Title is required' }); - } + if (!title) { + throw new AppError('Title is required', 400); + } const result = await query( 'INSERT INTO challenges (title, cover_image_url, tmdb_id, media_type, created_by) VALUES (?, ?, ?, ?, ?)', @@ -114,42 +104,37 @@ router.post('/', authMiddleware, async (req, res) => { creator_username: req.user.username }; - res.json({ challenge }); - } catch (error) { - console.error('Create challenge error:', error); - res.status(500).json({ error: 'Failed to create challenge' }); - } -}); + res.json({ challenge }); +})); // Invite users to a challenge -router.post('/:id/invite', authMiddleware, async (req, res) => { - try { - const challengeId = req.params.id; - const { user_ids, emails } = req.body; +router.post('/:id/invite', authMiddleware, asyncHandler(async (req, res) => { + const challengeId = req.params.id; + const { user_ids, emails } = req.body; - // Verify user owns the challenge or is a participant - const challenges = await query( - 'SELECT * FROM challenges WHERE id = ?', - [challengeId] + // Verify user owns the challenge or is a participant + const challenges = await query( + 'SELECT * FROM challenges WHERE id = ?', + [challengeId] + ); + + if (challenges.length === 0) { + throw new AppError('Challenge not found', 404); + } + + const challenge = challenges[0]; + + if (challenge.created_by !== req.user.userId) { + // Check if user is an accepted participant + const participation = await query( + 'SELECT * FROM challenge_participants WHERE challenge_id = ? AND user_id = ? AND status = "accepted"', + [challengeId, req.user.userId] ); - if (challenges.length === 0) { - return res.status(404).json({ error: 'Challenge not found' }); - } - - const challenge = challenges[0]; - - if (challenge.created_by !== req.user.userId) { - // Check if user is an accepted participant - const participation = await query( - 'SELECT * FROM challenge_participants WHERE challenge_id = ? AND user_id = ? AND status = "accepted"', - [challengeId, req.user.userId] - ); - - if (participation.length === 0) { - return res.status(403).json({ error: 'Only challenge participants can invite others' }); - } + if (participation.length === 0) { + throw new AppError('Only challenge participants can invite others', 403); } + } const invitedUsers = []; @@ -186,33 +171,24 @@ router.post('/:id/invite', authMiddleware, async (req, res) => { } } - res.json({ invited: invitedUsers.length }); - } catch (error) { - console.error('Invite error:', error); - res.status(500).json({ error: 'Failed to send invites' }); - } -}); + res.json({ invited: invitedUsers.length }); +})); // Accept/reject challenge invitation -router.post('/:id/respond', authMiddleware, async (req, res) => { - try { - const challengeId = req.params.id; - const { status } = req.body; // 'accepted' or 'rejected' +router.post('/:id/respond', authMiddleware, asyncHandler(async (req, res) => { + const challengeId = req.params.id; + const { status } = req.body; // 'accepted' or 'rejected' - if (!['accepted', 'rejected'].includes(status)) { - return res.status(400).json({ error: 'Invalid status' }); - } - - await query( - 'UPDATE challenge_participants SET status = ?, responded_at = NOW() WHERE challenge_id = ? AND user_id = ?', - [status, challengeId, req.user.userId] - ); - - res.json({ status }); - } catch (error) { - console.error('Respond error:', error); - res.status(500).json({ error: 'Failed to respond to invitation' }); + if (!['accepted', 'rejected'].includes(status)) { + throw new AppError('Invalid status', 400); } -}); + + await query( + 'UPDATE challenge_participants SET status = ?, responded_at = NOW() WHERE challenge_id = ? AND user_id = ?', + [status, challengeId, req.user.userId] + ); + + res.json({ status }); +})); export default router; diff --git a/backend/src/routes/friends.js b/backend/src/routes/friends.js index b3feac0..3205e8e 100644 --- a/backend/src/routes/friends.js +++ b/backend/src/routes/friends.js @@ -1,169 +1,145 @@ import express from 'express'; import { query } from '../db/index.js'; import { authMiddleware } from '../middleware/auth.js'; +import { asyncHandler, AppError } from '../middleware/errorHandler.js'; const router = express.Router(); // Search for users by username or email -router.get('/search', authMiddleware, async (req, res) => { - try { - const { q } = req.query; +router.get('/search', authMiddleware, asyncHandler(async (req, res) => { + const { q } = req.query; - if (!q || q.trim().length < 2) { - return res.json({ users: [] }); - } - - const searchTerm = `%${q.trim()}%`; - - const users = await query( - `SELECT id, username, email - FROM users - WHERE (username LIKE ? OR email LIKE ?) AND id != ? - LIMIT 20`, - [searchTerm, searchTerm, req.user.userId] - ); - - res.json({ users }); - } catch (error) { - console.error('User search error:', error); - res.status(500).json({ error: 'Search failed' }); + if (!q || q.trim().length < 2) { + return res.json({ users: [] }); } -}); + + const searchTerm = `%${q.trim()}%`; + + const users = await query( + `SELECT id, username, email + FROM users + WHERE (username LIKE ? OR email LIKE ?) AND id != ? + LIMIT 20`, + [searchTerm, searchTerm, req.user.userId] + ); + + res.json({ users }); +})); // Get all friends -router.get('/', authMiddleware, async (req, res) => { - try { - // Get accepted friendships (bidirectional) - const friends = await query( - `SELECT DISTINCT - u.id, u.username, u.email, - (SELECT COUNT(*) FROM predictions WHERE user_id = u.id AND status = 'validated') as total_points - FROM users u - WHERE u.id IN ( - SELECT friend_id FROM friendships WHERE user_id = ? AND status = 'accepted' - UNION - SELECT user_id FROM friendships WHERE friend_id = ? AND status = 'accepted' - ) - ORDER BY u.username`, - [req.user.userId, req.user.userId] - ); +router.get('/', authMiddleware, asyncHandler(async (req, res) => { + // Get accepted friendships (bidirectional) + const friends = await query( + `SELECT DISTINCT + u.id, u.username, u.email, + (SELECT COUNT(*) FROM predictions WHERE user_id = u.id AND status = 'validated') as total_points + FROM users u + WHERE u.id IN ( + SELECT friend_id FROM friendships WHERE user_id = ? AND status = 'accepted' + UNION + SELECT user_id FROM friendships WHERE friend_id = ? AND status = 'accepted' + ) + ORDER BY u.username`, + [req.user.userId, req.user.userId] + ); - // Also get people who have shared challenges with me (auto-friends from challenges) - const challengeFriends = await query( - `SELECT DISTINCT - u.id, u.username, u.email, - (SELECT COUNT(*) FROM predictions WHERE user_id = u.id AND status = 'validated') as total_points - FROM users u - WHERE u.id IN ( - SELECT DISTINCT cp.user_id - FROM challenge_participants cp - INNER JOIN challenge_participants my_cp ON cp.challenge_id = my_cp.challenge_id - WHERE my_cp.user_id = ? AND cp.user_id != ? AND cp.status = 'accepted' AND my_cp.status = 'accepted' - UNION - SELECT DISTINCT c.created_by - FROM challenges c - INNER JOIN challenge_participants cp ON cp.challenge_id = c.id - WHERE cp.user_id = ? AND c.created_by != ? AND cp.status = 'accepted' - ) - AND u.id NOT IN (${friends.map(() => '?').join(',') || 'NULL'}) - ORDER BY u.username`, - [req.user.userId, req.user.userId, req.user.userId, req.user.userId, ...friends.map(f => f.id)] - ); + // Also get people who have shared challenges with me (auto-friends from challenges) + const challengeFriends = await query( + `SELECT DISTINCT + u.id, u.username, u.email, + (SELECT COUNT(*) FROM predictions WHERE user_id = u.id AND status = 'validated') as total_points + FROM users u + WHERE u.id IN ( + SELECT DISTINCT cp.user_id + FROM challenge_participants cp + INNER JOIN challenge_participants my_cp ON cp.challenge_id = my_cp.challenge_id + WHERE my_cp.user_id = ? AND cp.user_id != ? AND cp.status = 'accepted' AND my_cp.status = 'accepted' + UNION + SELECT DISTINCT c.created_by + FROM challenges c + INNER JOIN challenge_participants cp ON cp.challenge_id = c.id + WHERE cp.user_id = ? AND c.created_by != ? AND cp.status = 'accepted' + ) + AND u.id NOT IN (${friends.map(() => '?').join(',') || 'NULL'}) + ORDER BY u.username`, + [req.user.userId, req.user.userId, req.user.userId, req.user.userId, ...friends.map(f => f.id)] + ); - res.json({ - friends, - challenge_friends: challengeFriends - }); - } catch (error) { - console.error('Get friends error:', error); - res.status(500).json({ error: 'Failed to fetch friends' }); - } -}); + res.json({ + friends, + challenge_friends: challengeFriends + }); +})); // Send friend request -router.post('/request', authMiddleware, async (req, res) => { - try { - const { user_id } = req.body; +router.post('/request', authMiddleware, asyncHandler(async (req, res) => { + const { user_id } = req.body; - if (!user_id) { - return res.status(400).json({ error: 'User ID required' }); - } - - if (user_id === req.user.userId) { - return res.status(400).json({ error: 'Cannot add yourself as friend' }); - } - - // Check if already friends or request exists - const existing = await query( - `SELECT * FROM friendships - WHERE (user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?)`, - [req.user.userId, user_id, user_id, req.user.userId] - ); - - if (existing.length > 0) { - return res.status(400).json({ error: 'Friend request already exists or you are already friends' }); - } - - await query( - 'INSERT INTO friendships (user_id, friend_id, status) VALUES (?, ?, "pending")', - [req.user.userId, user_id] - ); - - res.json({ success: true }); - } catch (error) { - console.error('Friend request error:', error); - res.status(500).json({ error: 'Failed to send friend request' }); + if (!user_id) { + throw new AppError('User ID required', 400); } -}); + + if (user_id === req.user.userId) { + throw new AppError('Cannot add yourself as friend', 400); + } + + // Check if already friends or request exists + const existing = await query( + `SELECT * FROM friendships + WHERE (user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?)`, + [req.user.userId, user_id, user_id, req.user.userId] + ); + + if (existing.length > 0) { + throw new AppError('Friend request already exists or you are already friends', 400); + } + + await query( + 'INSERT INTO friendships (user_id, friend_id, status) VALUES (?, ?, "pending")', + [req.user.userId, user_id] + ); + + res.json({ success: true }); +})); // Accept/reject friend request -router.post('/respond', authMiddleware, async (req, res) => { - try { - const { friendship_id, status } = req.body; +router.post('/respond', authMiddleware, asyncHandler(async (req, res) => { + const { friendship_id, status } = req.body; - if (!['accepted', 'rejected'].includes(status)) { - return res.status(400).json({ error: 'Invalid status' }); - } - - // Verify the request is for the current user - const friendships = await query( - 'SELECT * FROM friendships WHERE id = ? AND friend_id = ?', - [friendship_id, req.user.userId] - ); - - if (friendships.length === 0) { - return res.status(404).json({ error: 'Friend request not found' }); - } - - await query( - 'UPDATE friendships SET status = ? WHERE id = ?', - [status, friendship_id] - ); - - res.json({ status }); - } catch (error) { - console.error('Respond to friend request error:', error); - res.status(500).json({ error: 'Failed to respond to friend request' }); + if (!['accepted', 'rejected'].includes(status)) { + throw new AppError('Invalid status', 400); } -}); + + // Verify the request is for the current user + const friendships = await query( + 'SELECT * FROM friendships WHERE id = ? AND friend_id = ?', + [friendship_id, req.user.userId] + ); + + if (friendships.length === 0) { + throw new AppError('Friend request not found', 404); + } + + await query( + 'UPDATE friendships SET status = ? WHERE id = ?', + [status, friendship_id] + ); + + res.json({ status }); +})); // Get pending friend requests -router.get('/requests', authMiddleware, async (req, res) => { - try { - const requests = await query( - `SELECT f.id, f.created_at, u.id as user_id, u.username, u.email - FROM friendships f - INNER JOIN users u ON f.user_id = u.id - WHERE f.friend_id = ? AND f.status = 'pending' - ORDER BY f.created_at DESC`, - [req.user.userId] - ); +router.get('/requests', authMiddleware, asyncHandler(async (req, res) => { + const requests = await query( + `SELECT f.id, f.created_at, u.id as user_id, u.username, u.email + FROM friendships f + INNER JOIN users u ON f.user_id = u.id + WHERE f.friend_id = ? AND f.status = 'pending' + ORDER BY f.created_at DESC`, + [req.user.userId] + ); - res.json({ requests }); - } catch (error) { - console.error('Get friend requests error:', error); - res.status(500).json({ error: 'Failed to fetch friend requests' }); - } -}); + res.json({ requests }); +})); export default router; diff --git a/backend/src/routes/leaderboard.js b/backend/src/routes/leaderboard.js index bde305e..192a5ff 100644 --- a/backend/src/routes/leaderboard.js +++ b/backend/src/routes/leaderboard.js @@ -1,25 +1,25 @@ import express from 'express'; import { query } from '../db/index.js'; import { authMiddleware } from '../middleware/auth.js'; +import { asyncHandler, AppError } from '../middleware/errorHandler.js'; const router = express.Router(); // Get leaderboard for a specific challenge -router.get('/challenge/:challengeId', authMiddleware, async (req, res) => { - try { - const { challengeId } = req.params; +router.get('/challenge/:challengeId', authMiddleware, asyncHandler(async (req, res) => { + const { challengeId } = req.params; - // Verify access - const access = await query( - `SELECT * FROM challenges WHERE id = ? AND (created_by = ? OR id IN ( - SELECT challenge_id FROM challenge_participants WHERE user_id = ? AND status = 'accepted' - ))`, - [challengeId, req.user.userId, req.user.userId] - ); + // Verify access + const access = await query( + `SELECT * FROM challenges WHERE id = ? AND (created_by = ? OR id IN ( + SELECT challenge_id FROM challenge_participants WHERE user_id = ? AND status = 'accepted' + ))`, + [challengeId, req.user.userId, req.user.userId] + ); - if (access.length === 0) { - return res.status(403).json({ error: 'Access denied' }); - } + if (access.length === 0) { + throw new AppError('Access denied', 403); + } // Get leaderboard const leaderboard = await query( @@ -53,72 +53,58 @@ router.get('/challenge/:challengeId', authMiddleware, async (req, res) => { [challengeId, challengeId, challengeId, challengeId] ); - res.json({ leaderboard }); - } catch (error) { - console.error('Challenge leaderboard error:', error); - res.status(500).json({ error: 'Failed to fetch leaderboard' }); - } -}); + res.json({ leaderboard }); +})); // Get global leaderboard (all users) -router.get('/global', authMiddleware, async (req, res) => { - try { - const leaderboard = await query( - `SELECT - u.id, - u.username, - COUNT(CASE WHEN p.status = 'validated' THEN 1 END) as total_points, - COUNT(CASE WHEN p.status = 'pending' THEN 1 END) as pending_predictions, - COUNT(DISTINCT p.challenge_id) as challenges_participated - FROM users u - LEFT JOIN predictions p ON p.user_id = u.id - GROUP BY u.id, u.username - HAVING total_points > 0 OR pending_predictions > 0 - ORDER BY total_points DESC, username - LIMIT 100` - ); +router.get('/global', authMiddleware, asyncHandler(async (req, res) => { + const leaderboard = await query( + `SELECT + u.id, + u.username, + COUNT(CASE WHEN p.status = 'validated' THEN 1 END) as total_points, + COUNT(CASE WHEN p.status = 'pending' THEN 1 END) as pending_predictions, + COUNT(DISTINCT p.challenge_id) as challenges_participated + FROM users u + LEFT JOIN predictions p ON p.user_id = u.id + GROUP BY u.id, u.username + HAVING total_points > 0 OR pending_predictions > 0 + ORDER BY total_points DESC, username + LIMIT 100` + ); - res.json({ leaderboard }); - } catch (error) { - console.error('Global leaderboard error:', error); - res.status(500).json({ error: 'Failed to fetch leaderboard' }); - } -}); + res.json({ leaderboard }); +})); // Get user profile stats -router.get('/profile/:userId?', authMiddleware, async (req, res) => { - try { - const userId = req.params.userId || req.user.userId; +router.get('/profile/:userId?', authMiddleware, asyncHandler(async (req, res) => { + const userId = req.params.userId || req.user.userId; - const stats = await query( - `SELECT - u.id, - u.username, - u.email, - u.created_at, - COUNT(DISTINCT c.id) as challenges_created, - COUNT(DISTINCT cp.challenge_id) as challenges_joined, - COUNT(CASE WHEN p.status = 'validated' THEN 1 END) as total_points, - COUNT(CASE WHEN p.status = 'pending' THEN 1 END) as pending_predictions, - COUNT(CASE WHEN p.status = 'invalidated' THEN 1 END) as invalidated_predictions - FROM users u - LEFT JOIN challenges c ON c.created_by = u.id - LEFT JOIN challenge_participants cp ON cp.user_id = u.id AND cp.status = 'accepted' - LEFT JOIN predictions p ON p.user_id = u.id - WHERE u.id = ? - GROUP BY u.id`, - [userId] - ); + const stats = await query( + `SELECT + u.id, + u.username, + u.email, + u.created_at, + COUNT(DISTINCT c.id) as challenges_created, + COUNT(DISTINCT cp.challenge_id) as challenges_joined, + COUNT(CASE WHEN p.status = 'validated' THEN 1 END) as total_points, + COUNT(CASE WHEN p.status = 'pending' THEN 1 END) as pending_predictions, + COUNT(CASE WHEN p.status = 'invalidated' THEN 1 END) as invalidated_predictions + FROM users u + LEFT JOIN challenges c ON c.created_by = u.id + LEFT JOIN challenge_participants cp ON cp.user_id = u.id AND cp.status = 'accepted' + LEFT JOIN predictions p ON p.user_id = u.id + WHERE u.id = ? + GROUP BY u.id`, + [userId] + ); - if (stats.length === 0) { - return res.status(404).json({ error: 'User not found' }); - } - - res.json({ profile: stats[0] }); - } catch (error) { - console.error('Profile stats error:', error); - res.status(500).json({ error: 'Failed to fetch profile' }); + if (stats.length === 0) { + throw new AppError('User not found', 404); } -}); + + res.json({ profile: stats[0] }); +})); export default router; diff --git a/backend/src/routes/predictions.js b/backend/src/routes/predictions.js index cbc1fa4..c15c6e2 100644 --- a/backend/src/routes/predictions.js +++ b/backend/src/routes/predictions.js @@ -1,140 +1,126 @@ import express from 'express'; import { query } from '../db/index.js'; import { authMiddleware } from '../middleware/auth.js'; +import { asyncHandler, AppError } from '../middleware/errorHandler.js'; const router = express.Router(); // Get all predictions for a challenge -router.get('/challenge/:challengeId', authMiddleware, async (req, res) => { - try { - const { challengeId } = req.params; +router.get('/challenge/:challengeId', authMiddleware, asyncHandler(async (req, res) => { + const { challengeId } = req.params; - // Verify access - const access = await query( - `SELECT * FROM challenges WHERE id = ? AND (created_by = ? OR id IN ( - SELECT challenge_id FROM challenge_participants WHERE user_id = ? AND status = 'accepted' - ))`, - [challengeId, req.user.userId, req.user.userId] - ); + // Verify access + const access = await query( + `SELECT * FROM challenges WHERE id = ? AND (created_by = ? OR id IN ( + SELECT challenge_id FROM challenge_participants WHERE user_id = ? AND status = 'accepted' + ))`, + [challengeId, req.user.userId, req.user.userId] + ); - if (access.length === 0) { - return res.status(403).json({ error: 'Access denied' }); - } - - // Get predictions - const predictions = await query( - `SELECT - p.*, - u.username, - v.username as validated_by_username - FROM predictions p - INNER JOIN users u ON p.user_id = u.id - LEFT JOIN users v ON p.validated_by = v.id - WHERE p.challenge_id = ? - ORDER BY p.created_at DESC`, - [challengeId] - ); - - res.json({ predictions }); - } catch (error) { - console.error('Get predictions error:', error); - res.status(500).json({ error: 'Failed to fetch predictions' }); + if (access.length === 0) { + throw new AppError('Access denied', 403); } -}); + + // Get predictions + const predictions = await query( + `SELECT + p.*, + u.username, + v.username as validated_by_username + FROM predictions p + INNER JOIN users u ON p.user_id = u.id + LEFT JOIN users v ON p.validated_by = v.id + WHERE p.challenge_id = ? + ORDER BY p.created_at DESC`, + [challengeId] + ); + + res.json({ predictions }); +})); // Create a new prediction -router.post('/', authMiddleware, async (req, res) => { - try { - const { challenge_id, content } = req.body; +router.post('/', authMiddleware, asyncHandler(async (req, res) => { + const { challenge_id, content } = req.body; - if (!challenge_id || !content || !content.trim()) { - return res.status(400).json({ error: 'Challenge ID and content are required' }); - } - - // Verify access - const access = await query( - `SELECT * FROM challenges WHERE id = ? AND (created_by = ? OR id IN ( - SELECT challenge_id FROM challenge_participants WHERE user_id = ? AND status = 'accepted' - ))`, - [challenge_id, req.user.userId, req.user.userId] - ); - - if (access.length === 0) { - return res.status(403).json({ error: 'Access denied' }); - } - - const result = await query( - 'INSERT INTO predictions (challenge_id, user_id, content) VALUES (?, ?, ?)', - [challenge_id, req.user.userId, content.trim()] - ); - - const prediction = { - id: result.insertId, - challenge_id, - user_id: req.user.userId, - username: req.user.username, - content: content.trim(), - status: 'pending', - created_at: new Date() - }; - - res.json({ prediction }); - } catch (error) { - console.error('Create prediction error:', error); - res.status(500).json({ error: 'Failed to create prediction' }); + if (!challenge_id || !content || !content.trim()) { + throw new AppError('Challenge ID and content are required', 400); } -}); + + // Verify access + const access = await query( + `SELECT * FROM challenges WHERE id = ? AND (created_by = ? OR id IN ( + SELECT challenge_id FROM challenge_participants WHERE user_id = ? AND status = 'accepted' + ))`, + [challenge_id, req.user.userId, req.user.userId] + ); + + if (access.length === 0) { + throw new AppError('Access denied', 403); + } + + const result = await query( + 'INSERT INTO predictions (challenge_id, user_id, content) VALUES (?, ?, ?)', + [challenge_id, req.user.userId, content.trim()] + ); + + const prediction = { + id: result.insertId, + challenge_id, + user_id: req.user.userId, + username: req.user.username, + content: content.trim(), + status: 'pending', + created_at: new Date() + }; + + res.json({ prediction }); +})); // Validate/invalidate a prediction (approve someone else's) -router.post('/:id/validate', authMiddleware, async (req, res) => { - try { - const predictionId = req.params.id; - const { status } = req.body; // 'validated' or 'invalidated' +router.post('/:id/validate', authMiddleware, asyncHandler(async (req, res) => { + const predictionId = req.params.id; + const { status } = req.body; // 'validated' or 'invalidated' - if (!['validated', 'invalidated'].includes(status)) { - return res.status(400).json({ error: 'Invalid status' }); - } - - // Get the prediction - const predictions = await query( - 'SELECT * FROM predictions WHERE id = ?', - [predictionId] - ); - - if (predictions.length === 0) { - return res.status(404).json({ error: 'Prediction not found' }); - } - - const prediction = predictions[0]; - - // Cannot validate own prediction - if (prediction.user_id === req.user.userId) { - return res.status(403).json({ error: 'Cannot validate your own prediction' }); - } - - // Verify access to the challenge - const access = await query( - `SELECT * FROM challenges WHERE id = ? AND (created_by = ? OR id IN ( - SELECT challenge_id FROM challenge_participants WHERE user_id = ? AND status = 'accepted' - ))`, - [prediction.challenge_id, req.user.userId, req.user.userId] - ); - - if (access.length === 0) { - return res.status(403).json({ error: 'Access denied' }); - } - - // Update prediction - await query( - 'UPDATE predictions SET status = ?, validated_by = ?, validated_at = NOW() WHERE id = ?', - [status, req.user.userId, predictionId] - ); - - res.json({ status }); - } catch (error) { - console.error('Validate prediction error:', error); - res.status(500).json({ error: 'Failed to validate prediction' }); + if (!['validated', 'invalidated'].includes(status)) { + throw new AppError('Invalid status', 400); } -}); + + // Get the prediction + const predictions = await query( + 'SELECT * FROM predictions WHERE id = ?', + [predictionId] + ); + + if (predictions.length === 0) { + throw new AppError('Prediction not found', 404); + } + + const prediction = predictions[0]; + + // Cannot validate own prediction + if (prediction.user_id === req.user.userId) { + throw new AppError('Cannot validate your own prediction', 403); + } + + // Verify access to the challenge + const access = await query( + `SELECT * FROM challenges WHERE id = ? AND (created_by = ? OR id IN ( + SELECT challenge_id FROM challenge_participants WHERE user_id = ? AND status = 'accepted' + ))`, + [prediction.challenge_id, req.user.userId, req.user.userId] + ); + + if (access.length === 0) { + throw new AppError('Access denied', 403); + } + + // Update prediction + await query( + 'UPDATE predictions SET status = ?, validated_by = ?, validated_at = NOW() WHERE id = ?', + [status, req.user.userId, predictionId] + ); + + res.json({ status }); +})); export default router; diff --git a/backend/src/routes/tmdb.js b/backend/src/routes/tmdb.js index 8fffe10..a2db906 100644 --- a/backend/src/routes/tmdb.js +++ b/backend/src/routes/tmdb.js @@ -1,79 +1,72 @@ import express from 'express'; import { query } from '../db/index.js'; import { authMiddleware } from '../middleware/auth.js'; +import { asyncHandler, AppError } from '../middleware/errorHandler.js'; const router = express.Router(); // Search for shows/movies via TMDB with caching -router.get('/search', authMiddleware, async (req, res) => { - try { - const { q } = req.query; +router.get('/search', authMiddleware, asyncHandler(async (req, res) => { + const { q } = req.query; - if (!q || q.trim().length < 2) { - return res.json({ results: [] }); - } - - const searchQuery = q.trim(); - - // Check cache first - const cached = await query( - 'SELECT response_data FROM tmdb_cache WHERE query = ? AND media_type = ? AND cached_at > DATE_SUB(NOW(), INTERVAL 7 DAY)', - [searchQuery, 'multi'] - ); - - if (cached.length > 0) { - return res.json(JSON.parse(cached[0].response_data)); - } - - // Fetch from TMDB - const apiKey = process.env.TMDB_API_KEY; - if (!apiKey) { - return res.status(500).json({ error: 'TMDB API key not configured' }); - } - - const fetch = (await import('node-fetch')).default; - const response = await fetch( - `https://api.themoviedb.org/3/search/multi?api_key=${apiKey}&query=${encodeURIComponent(searchQuery)}` - ); - - if (!response.ok) { - const errorText = await response.text(); - console.error('TMDB API error:', response.status, errorText); - return res.status(500).json({ - error: `TMDB API error: ${response.status}`, - details: errorText - }); - } - - const data = await response.json(); - - // Filter to only movies and TV shows - const filtered = data.results - .filter(item => item.media_type === 'movie' || item.media_type === 'tv') - .map(item => ({ - id: item.id, - title: item.media_type === 'movie' ? item.title : item.name, - media_type: item.media_type, - poster_path: item.poster_path, - backdrop_path: item.backdrop_path, - release_date: item.release_date || item.first_air_date, - overview: item.overview - })) - .slice(0, 10); - - const result = { results: filtered }; - - // Cache the result - await query( - 'INSERT INTO tmdb_cache (query, media_type, response_data) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE response_data = ?, cached_at = NOW()', - [searchQuery, 'multi', JSON.stringify(result), JSON.stringify(result)] - ); - - res.json(result); - } catch (error) { - console.error('TMDB search error:', error); - res.status(500).json({ error: 'Search failed' }); + if (!q || q.trim().length < 2) { + return res.json({ results: [] }); } -}); + + const searchQuery = q.trim(); + + // Check cache first + const cached = await query( + 'SELECT response_data FROM tmdb_cache WHERE query = ? AND media_type = ? AND cached_at > DATE_SUB(NOW(), INTERVAL 7 DAY)', + [searchQuery, 'multi'] + ); + + if (cached.length > 0) { + return res.json(JSON.parse(cached[0].response_data)); + } + + // Fetch from TMDB + const apiKey = process.env.TMDB_API_KEY; + if (!apiKey) { + throw new AppError('TMDB API key not configured', 500); + } + + const fetch = (await import('node-fetch')).default; + const response = await fetch( + `https://api.themoviedb.org/3/search/multi?api_key=${apiKey}&query=${encodeURIComponent(searchQuery)}` + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error('TMDB API error:', response.status, errorText); + throw new AppError(`TMDB API error: ${response.status}`, 500); + } + + const data = await response.json(); + + // Filter to only movies and TV shows + const filtered = data.results + .filter(item => item.media_type === 'movie' || item.media_type === 'tv') + .map(item => ({ + id: item.id, + title: item.media_type === 'movie' ? item.title : item.name, + media_type: item.media_type, + poster_path: item.poster_path, + backdrop_path: item.backdrop_path, + release_date: item.release_date || item.first_air_date, + overview: item.overview + })) + .slice(0, 10); + + const result = { results: filtered }; + + // Cache the result + await query( + 'INSERT INTO tmdb_cache (query, media_type, response_data) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE response_data = ?, cached_at = NOW()', + [searchQuery, 'multi', JSON.stringify(result), JSON.stringify(result)] + ); + + res.json(result); +})); export default router;