This commit is contained in:
2026-01-29 02:00:55 -05:00
parent 3e3f37a570
commit 864cbaece9
6 changed files with 525 additions and 621 deletions

View File

@@ -2,121 +2,108 @@ import express from 'express';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { query } from '../db/index.js'; import { query } from '../db/index.js';
import { asyncHandler, AppError } from '../middleware/errorHandler.js';
const router = express.Router(); const router = express.Router();
// Register // Register
router.post('/register', async (req, res) => { router.post('/register', asyncHandler(async (req, res) => {
try { const { email, username, password } = req.body;
const { email, username, password } = req.body;
if (!email || !username || !password) { if (!email || !username || !password) {
return res.status(400).json({ error: 'All fields required' }); 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) {
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' });
} }
});
// 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 // Login
router.post('/login', async (req, res) => { router.post('/login', asyncHandler(async (req, res) => {
try { const { email, password } = req.body;
const { email, password } = req.body;
if (!email || !password) { if (!email || !password) {
return res.status(400).json({ error: 'Email and password required' }); 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) {
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' });
} }
});
// 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 // Get current user
router.get('/me', async (req, res) => { router.get('/me', asyncHandler(async (req, res) => {
try { const authHeader = req.headers.authorization;
const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) {
if (!authHeader || !authHeader.startsWith('Bearer ')) { throw new AppError('No token provided', 401);
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' });
} }
});
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; export default router;

View File

@@ -1,63 +1,58 @@
import express from 'express'; import express from 'express';
import { query } from '../db/index.js'; import { query } from '../db/index.js';
import { authMiddleware } from '../middleware/auth.js'; import { authMiddleware } from '../middleware/auth.js';
import { asyncHandler, AppError } from '../middleware/errorHandler.js';
const router = express.Router(); const router = express.Router();
// Get all challenges for the current user // Get all challenges for the current user
router.get('/', authMiddleware, async (req, res) => { router.get('/', authMiddleware, asyncHandler(async (req, res) => {
try { const challenges = await query(
const challenges = await query( `SELECT
`SELECT c.*,
c.*, u.username as creator_username,
u.username as creator_username, cp.status as participation_status,
cp.status as participation_status, (SELECT COUNT(*) FROM predictions WHERE challenge_id = c.id AND status = 'validated' AND user_id = ?) as my_points
(SELECT COUNT(*) FROM predictions WHERE challenge_id = c.id AND status = 'validated' AND user_id = ?) as my_points FROM challenges c
FROM challenges c INNER JOIN users u ON c.created_by = u.id
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 = ?
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
WHERE c.created_by = ? OR cp.user_id IS NOT NULL ORDER BY c.created_at DESC`,
ORDER BY c.created_at DESC`, [req.user.userId, req.user.userId, req.user.userId]
[req.user.userId, req.user.userId, req.user.userId] );
);
res.json({ challenges }); res.json({ challenges });
} catch (error) { }));
console.error('Get challenges error:', error);
res.status(500).json({ error: 'Failed to fetch challenges' });
}
});
// Get a single challenge with details // Get a single challenge with details
router.get('/:id', authMiddleware, async (req, res) => { router.get('/:id', authMiddleware, asyncHandler(async (req, res) => {
try { const challengeId = req.params.id;
const challengeId = req.params.id;
// Get challenge details // Get challenge details
const challenges = await query( const challenges = await query(
`SELECT c.*, u.username as creator_username `SELECT c.*, u.username as creator_username
FROM challenges c FROM challenges c
INNER JOIN users u ON c.created_by = u.id INNER JOIN users u ON c.created_by = u.id
WHERE c.id = ?`, WHERE c.id = ?`,
[challengeId] [challengeId]
); );
if (challenges.length === 0) { if (challenges.length === 0) {
return res.status(404).json({ error: 'Challenge not found' }); throw new AppError('Challenge not found', 404);
} }
const challenge = challenges[0]; const challenge = challenges[0];
// Check if user has access // Check if user has access
const access = await query( const access = await query(
`SELECT * FROM challenge_participants `SELECT * FROM challenge_participants
WHERE challenge_id = ? AND user_id = ? AND status = 'accepted'`, WHERE challenge_id = ? AND user_id = ? AND status = 'accepted'`,
[challengeId, req.user.userId] [challengeId, req.user.userId]
); );
if (challenge.created_by !== req.user.userId && access.length === 0) { if (challenge.created_by !== req.user.userId && access.length === 0) {
return res.status(403).json({ error: 'Access denied' }); throw new AppError('Access denied', 403);
} }
// Get participants with their points // Get participants with their points
const participants = await query( const participants = await query(
@@ -79,25 +74,20 @@ router.get('/:id', authMiddleware, async (req, res) => {
[challengeId, challenge.created_by] [challengeId, challenge.created_by]
); );
res.json({ res.json({
challenge, challenge,
participants, participants,
creator_points: creatorPoints[0].points creator_points: creatorPoints[0].points
}); });
} catch (error) { }));
console.error('Get challenge error:', error);
res.status(500).json({ error: 'Failed to fetch challenge' });
}
});
// Create a new challenge // Create a new challenge
router.post('/', authMiddleware, async (req, res) => { router.post('/', authMiddleware, asyncHandler(async (req, res) => {
try { const { title, cover_image_url, tmdb_id, media_type } = req.body;
const { title, cover_image_url, tmdb_id, media_type } = req.body;
if (!title) { if (!title) {
return res.status(400).json({ error: 'Title is required' }); throw new AppError('Title is required', 400);
} }
const result = await query( const result = await query(
'INSERT INTO challenges (title, cover_image_url, tmdb_id, media_type, created_by) VALUES (?, ?, ?, ?, ?)', '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 creator_username: req.user.username
}; };
res.json({ challenge }); res.json({ challenge });
} catch (error) { }));
console.error('Create challenge error:', error);
res.status(500).json({ error: 'Failed to create challenge' });
}
});
// Invite users to a challenge // Invite users to a challenge
router.post('/:id/invite', authMiddleware, async (req, res) => { router.post('/:id/invite', authMiddleware, asyncHandler(async (req, res) => {
try { const challengeId = req.params.id;
const challengeId = req.params.id; const { user_ids, emails } = req.body;
const { user_ids, emails } = req.body;
// Verify user owns the challenge or is a participant // Verify user owns the challenge or is a participant
const challenges = await query( const challenges = await query(
'SELECT * FROM challenges WHERE id = ?', 'SELECT * FROM challenges WHERE id = ?',
[challengeId] [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) { if (participation.length === 0) {
return res.status(404).json({ error: 'Challenge not found' }); throw new AppError('Only challenge participants can invite others', 403);
}
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' });
}
} }
}
const invitedUsers = []; const invitedUsers = [];
@@ -186,33 +171,24 @@ router.post('/:id/invite', authMiddleware, async (req, res) => {
} }
} }
res.json({ invited: invitedUsers.length }); res.json({ invited: invitedUsers.length });
} catch (error) { }));
console.error('Invite error:', error);
res.status(500).json({ error: 'Failed to send invites' });
}
});
// Accept/reject challenge invitation // Accept/reject challenge invitation
router.post('/:id/respond', authMiddleware, async (req, res) => { router.post('/:id/respond', authMiddleware, asyncHandler(async (req, res) => {
try { const challengeId = req.params.id;
const challengeId = req.params.id; const { status } = req.body; // 'accepted' or 'rejected'
const { status } = req.body; // 'accepted' or 'rejected'
if (!['accepted', 'rejected'].includes(status)) { if (!['accepted', 'rejected'].includes(status)) {
return res.status(400).json({ error: 'Invalid 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 });
} catch (error) {
console.error('Respond error:', error);
res.status(500).json({ error: 'Failed to respond to invitation' });
} }
});
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; export default router;

View File

@@ -1,169 +1,145 @@
import express from 'express'; import express from 'express';
import { query } from '../db/index.js'; import { query } from '../db/index.js';
import { authMiddleware } from '../middleware/auth.js'; import { authMiddleware } from '../middleware/auth.js';
import { asyncHandler, AppError } from '../middleware/errorHandler.js';
const router = express.Router(); const router = express.Router();
// Search for users by username or email // Search for users by username or email
router.get('/search', authMiddleware, async (req, res) => { router.get('/search', authMiddleware, asyncHandler(async (req, res) => {
try { const { q } = req.query;
const { q } = req.query;
if (!q || q.trim().length < 2) { if (!q || q.trim().length < 2) {
return res.json({ users: [] }); 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' });
} }
});
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 // Get all friends
router.get('/', authMiddleware, async (req, res) => { router.get('/', authMiddleware, asyncHandler(async (req, res) => {
try { // Get accepted friendships (bidirectional)
// Get accepted friendships (bidirectional) const friends = await query(
const friends = await query( `SELECT DISTINCT
`SELECT DISTINCT u.id, u.username, u.email,
u.id, u.username, u.email, (SELECT COUNT(*) FROM predictions WHERE user_id = u.id AND status = 'validated') as total_points
(SELECT COUNT(*) FROM predictions WHERE user_id = u.id AND status = 'validated') as total_points FROM users u
FROM users u WHERE u.id IN (
WHERE u.id IN ( SELECT friend_id FROM friendships WHERE user_id = ? AND status = 'accepted'
SELECT friend_id FROM friendships WHERE user_id = ? AND status = 'accepted' UNION
UNION SELECT user_id FROM friendships WHERE friend_id = ? AND status = 'accepted'
SELECT user_id FROM friendships WHERE friend_id = ? AND status = 'accepted' )
) ORDER BY u.username`,
ORDER BY u.username`, [req.user.userId, req.user.userId]
[req.user.userId, req.user.userId] );
);
// Also get people who have shared challenges with me (auto-friends from challenges) // Also get people who have shared challenges with me (auto-friends from challenges)
const challengeFriends = await query( const challengeFriends = await query(
`SELECT DISTINCT `SELECT DISTINCT
u.id, u.username, u.email, u.id, u.username, u.email,
(SELECT COUNT(*) FROM predictions WHERE user_id = u.id AND status = 'validated') as total_points (SELECT COUNT(*) FROM predictions WHERE user_id = u.id AND status = 'validated') as total_points
FROM users u FROM users u
WHERE u.id IN ( WHERE u.id IN (
SELECT DISTINCT cp.user_id SELECT DISTINCT cp.user_id
FROM challenge_participants cp FROM challenge_participants cp
INNER JOIN challenge_participants my_cp ON cp.challenge_id = my_cp.challenge_id 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' WHERE my_cp.user_id = ? AND cp.user_id != ? AND cp.status = 'accepted' AND my_cp.status = 'accepted'
UNION UNION
SELECT DISTINCT c.created_by SELECT DISTINCT c.created_by
FROM challenges c FROM challenges c
INNER JOIN challenge_participants cp ON cp.challenge_id = c.id INNER JOIN challenge_participants cp ON cp.challenge_id = c.id
WHERE cp.user_id = ? AND c.created_by != ? AND cp.status = 'accepted' WHERE cp.user_id = ? AND c.created_by != ? AND cp.status = 'accepted'
) )
AND u.id NOT IN (${friends.map(() => '?').join(',') || 'NULL'}) AND u.id NOT IN (${friends.map(() => '?').join(',') || 'NULL'})
ORDER BY u.username`, ORDER BY u.username`,
[req.user.userId, req.user.userId, req.user.userId, req.user.userId, ...friends.map(f => f.id)] [req.user.userId, req.user.userId, req.user.userId, req.user.userId, ...friends.map(f => f.id)]
); );
res.json({ res.json({
friends, friends,
challenge_friends: challengeFriends challenge_friends: challengeFriends
}); });
} catch (error) { }));
console.error('Get friends error:', error);
res.status(500).json({ error: 'Failed to fetch friends' });
}
});
// Send friend request // Send friend request
router.post('/request', authMiddleware, async (req, res) => { router.post('/request', authMiddleware, asyncHandler(async (req, res) => {
try { const { user_id } = req.body;
const { user_id } = req.body;
if (!user_id) { if (!user_id) {
return res.status(400).json({ error: 'User ID required' }); throw new AppError('User ID required', 400);
}
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 === 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 // Accept/reject friend request
router.post('/respond', authMiddleware, async (req, res) => { router.post('/respond', authMiddleware, asyncHandler(async (req, res) => {
try { const { friendship_id, status } = req.body;
const { friendship_id, status } = req.body;
if (!['accepted', 'rejected'].includes(status)) { if (!['accepted', 'rejected'].includes(status)) {
return res.status(400).json({ error: 'Invalid 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) {
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' });
} }
});
// 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 // Get pending friend requests
router.get('/requests', authMiddleware, async (req, res) => { router.get('/requests', authMiddleware, asyncHandler(async (req, res) => {
try { const requests = await query(
const requests = await query( `SELECT f.id, f.created_at, u.id as user_id, u.username, u.email
`SELECT f.id, f.created_at, u.id as user_id, u.username, u.email FROM friendships f
FROM friendships f INNER JOIN users u ON f.user_id = u.id
INNER JOIN users u ON f.user_id = u.id WHERE f.friend_id = ? AND f.status = 'pending'
WHERE f.friend_id = ? AND f.status = 'pending' ORDER BY f.created_at DESC`,
ORDER BY f.created_at DESC`, [req.user.userId]
[req.user.userId] );
);
res.json({ requests }); res.json({ requests });
} catch (error) { }));
console.error('Get friend requests error:', error);
res.status(500).json({ error: 'Failed to fetch friend requests' });
}
});
export default router; export default router;

View File

@@ -1,25 +1,25 @@
import express from 'express'; import express from 'express';
import { query } from '../db/index.js'; import { query } from '../db/index.js';
import { authMiddleware } from '../middleware/auth.js'; import { authMiddleware } from '../middleware/auth.js';
import { asyncHandler, AppError } from '../middleware/errorHandler.js';
const router = express.Router(); const router = express.Router();
// Get leaderboard for a specific challenge // Get leaderboard for a specific challenge
router.get('/challenge/:challengeId', authMiddleware, async (req, res) => { router.get('/challenge/:challengeId', authMiddleware, asyncHandler(async (req, res) => {
try { const { challengeId } = req.params;
const { challengeId } = req.params;
// Verify access // Verify access
const access = await query( const access = await query(
`SELECT * FROM challenges WHERE id = ? AND (created_by = ? OR id IN ( `SELECT * FROM challenges WHERE id = ? AND (created_by = ? OR id IN (
SELECT challenge_id FROM challenge_participants WHERE user_id = ? AND status = 'accepted' SELECT challenge_id FROM challenge_participants WHERE user_id = ? AND status = 'accepted'
))`, ))`,
[challengeId, req.user.userId, req.user.userId] [challengeId, req.user.userId, req.user.userId]
); );
if (access.length === 0) { if (access.length === 0) {
return res.status(403).json({ error: 'Access denied' }); throw new AppError('Access denied', 403);
} }
// Get leaderboard // Get leaderboard
const leaderboard = await query( const leaderboard = await query(
@@ -53,72 +53,58 @@ router.get('/challenge/:challengeId', authMiddleware, async (req, res) => {
[challengeId, challengeId, challengeId, challengeId] [challengeId, challengeId, challengeId, challengeId]
); );
res.json({ leaderboard }); res.json({ leaderboard });
} catch (error) { }));
console.error('Challenge leaderboard error:', error);
res.status(500).json({ error: 'Failed to fetch leaderboard' });
}
});
// Get global leaderboard (all users) // Get global leaderboard (all users)
router.get('/global', authMiddleware, async (req, res) => { router.get('/global', authMiddleware, asyncHandler(async (req, res) => {
try { const leaderboard = await query(
const leaderboard = await query( `SELECT
`SELECT u.id,
u.id, u.username,
u.username, COUNT(CASE WHEN p.status = 'validated' THEN 1 END) as total_points,
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 = 'pending' THEN 1 END) as pending_predictions, COUNT(DISTINCT p.challenge_id) as challenges_participated
COUNT(DISTINCT p.challenge_id) as challenges_participated FROM users u
FROM users u LEFT JOIN predictions p ON p.user_id = u.id
LEFT JOIN predictions p ON p.user_id = u.id GROUP BY u.id, u.username
GROUP BY u.id, u.username HAVING total_points > 0 OR pending_predictions > 0
HAVING total_points > 0 OR pending_predictions > 0 ORDER BY total_points DESC, username
ORDER BY total_points DESC, username LIMIT 100`
LIMIT 100` );
);
res.json({ leaderboard }); res.json({ leaderboard });
} catch (error) { }));
console.error('Global leaderboard error:', error);
res.status(500).json({ error: 'Failed to fetch leaderboard' });
}
});
// Get user profile stats // Get user profile stats
router.get('/profile/:userId?', authMiddleware, async (req, res) => { router.get('/profile/:userId?', authMiddleware, asyncHandler(async (req, res) => {
try { const userId = req.params.userId || req.user.userId;
const userId = req.params.userId || req.user.userId;
const stats = await query( const stats = await query(
`SELECT `SELECT
u.id, u.id,
u.username, u.username,
u.email, u.email,
u.created_at, u.created_at,
COUNT(DISTINCT c.id) as challenges_created, COUNT(DISTINCT c.id) as challenges_created,
COUNT(DISTINCT cp.challenge_id) as challenges_joined, 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 = 'validated' THEN 1 END) as total_points,
COUNT(CASE WHEN p.status = 'pending' THEN 1 END) as pending_predictions, COUNT(CASE WHEN p.status = 'pending' THEN 1 END) as pending_predictions,
COUNT(CASE WHEN p.status = 'invalidated' THEN 1 END) as invalidated_predictions COUNT(CASE WHEN p.status = 'invalidated' THEN 1 END) as invalidated_predictions
FROM users u FROM users u
LEFT JOIN challenges c ON c.created_by = u.id 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 challenge_participants cp ON cp.user_id = u.id AND cp.status = 'accepted'
LEFT JOIN predictions p ON p.user_id = u.id LEFT JOIN predictions p ON p.user_id = u.id
WHERE u.id = ? WHERE u.id = ?
GROUP BY u.id`, GROUP BY u.id`,
[userId] [userId]
); );
if (stats.length === 0) { if (stats.length === 0) {
return res.status(404).json({ error: 'User not found' }); throw new AppError('User not found', 404);
}
res.json({ profile: stats[0] });
} catch (error) {
console.error('Profile stats error:', error);
res.status(500).json({ error: 'Failed to fetch profile' });
} }
});
res.json({ profile: stats[0] });
}));
export default router; export default router;

View File

@@ -1,140 +1,126 @@
import express from 'express'; import express from 'express';
import { query } from '../db/index.js'; import { query } from '../db/index.js';
import { authMiddleware } from '../middleware/auth.js'; import { authMiddleware } from '../middleware/auth.js';
import { asyncHandler, AppError } from '../middleware/errorHandler.js';
const router = express.Router(); const router = express.Router();
// Get all predictions for a challenge // Get all predictions for a challenge
router.get('/challenge/:challengeId', authMiddleware, async (req, res) => { router.get('/challenge/:challengeId', authMiddleware, asyncHandler(async (req, res) => {
try { const { challengeId } = req.params;
const { challengeId } = req.params;
// Verify access // Verify access
const access = await query( const access = await query(
`SELECT * FROM challenges WHERE id = ? AND (created_by = ? OR id IN ( `SELECT * FROM challenges WHERE id = ? AND (created_by = ? OR id IN (
SELECT challenge_id FROM challenge_participants WHERE user_id = ? AND status = 'accepted' SELECT challenge_id FROM challenge_participants WHERE user_id = ? AND status = 'accepted'
))`, ))`,
[challengeId, req.user.userId, req.user.userId] [challengeId, req.user.userId, req.user.userId]
); );
if (access.length === 0) { if (access.length === 0) {
return res.status(403).json({ error: 'Access denied' }); 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 });
} catch (error) {
console.error('Get predictions error:', error);
res.status(500).json({ error: 'Failed to fetch predictions' });
} }
});
// 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 // Create a new prediction
router.post('/', authMiddleware, async (req, res) => { router.post('/', authMiddleware, asyncHandler(async (req, res) => {
try { const { challenge_id, content } = req.body;
const { challenge_id, content } = req.body;
if (!challenge_id || !content || !content.trim()) { if (!challenge_id || !content || !content.trim()) {
return res.status(400).json({ error: 'Challenge ID and content are required' }); 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) {
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' });
} }
});
// 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) // Validate/invalidate a prediction (approve someone else's)
router.post('/:id/validate', authMiddleware, async (req, res) => { router.post('/:id/validate', authMiddleware, asyncHandler(async (req, res) => {
try { const predictionId = req.params.id;
const predictionId = req.params.id; const { status } = req.body; // 'validated' or 'invalidated'
const { status } = req.body; // 'validated' or 'invalidated'
if (!['validated', 'invalidated'].includes(status)) { if (!['validated', 'invalidated'].includes(status)) {
return res.status(400).json({ error: 'Invalid status' }); throw new AppError('Invalid status', 400);
}
// 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' });
} }
});
// 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; export default router;

View File

@@ -1,79 +1,72 @@
import express from 'express'; import express from 'express';
import { query } from '../db/index.js'; import { query } from '../db/index.js';
import { authMiddleware } from '../middleware/auth.js'; import { authMiddleware } from '../middleware/auth.js';
import { asyncHandler, AppError } from '../middleware/errorHandler.js';
const router = express.Router(); const router = express.Router();
// Search for shows/movies via TMDB with caching // Search for shows/movies via TMDB with caching
router.get('/search', authMiddleware, async (req, res) => { router.get('/search', authMiddleware, asyncHandler(async (req, res) => {
try { const { q } = req.query;
const { q } = req.query;
if (!q || q.trim().length < 2) { if (!q || q.trim().length < 2) {
return res.json({ results: [] }); 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' });
} }
});
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; export default router;