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,16 +2,16 @@ 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 // Check if user exists
@@ -21,7 +21,7 @@ router.post('/register', async (req, res) => {
); );
if (existing.length > 0) { if (existing.length > 0) {
return res.status(400).json({ error: 'User already exists' }); throw new AppError('User already exists', 400);
} }
// Hash password // Hash password
@@ -43,19 +43,14 @@ router.post('/register', async (req, res) => {
); );
res.json({ token, user: { id: userId, email, username } }); res.json({ token, user: { id: userId, email, username } });
} catch (error) { }));
console.error('Register error:', error);
res.status(500).json({ error: 'Registration failed' });
}
});
// 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 // Find user
@@ -65,7 +60,7 @@ router.post('/login', async (req, res) => {
); );
if (users.length === 0) { if (users.length === 0) {
return res.status(401).json({ error: 'Invalid credentials' }); throw new AppError('Invalid credentials', 401);
} }
const user = users[0]; const user = users[0];
@@ -73,7 +68,7 @@ router.post('/login', async (req, res) => {
// Check password // Check password
const validPassword = await bcrypt.compare(password, user.password_hash); const validPassword = await bcrypt.compare(password, user.password_hash);
if (!validPassword) { if (!validPassword) {
return res.status(401).json({ error: 'Invalid credentials' }); throw new AppError('Invalid credentials', 401);
} }
// Generate token // Generate token
@@ -87,18 +82,13 @@ router.post('/login', async (req, res) => {
token, token,
user: { id: user.id, email: user.email, username: user.username } user: { id: user.id, email: user.email, username: user.username }
}); });
} catch (error) { }));
console.error('Login error:', error);
res.status(500).json({ error: 'Login failed' });
}
});
// 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 ')) {
return res.status(401).json({ error: 'No token provided' }); throw new AppError('No token provided', 401);
} }
const token = authHeader.substring(7); const token = authHeader.substring(7);
@@ -110,13 +100,10 @@ router.get('/me', async (req, res) => {
); );
if (users.length === 0) { if (users.length === 0) {
return res.status(404).json({ error: 'User not found' }); throw new AppError('User not found', 404);
} }
res.json({ user: users[0] }); res.json({ user: users[0] });
} catch (error) { }));
res.status(401).json({ error: 'Invalid token' });
}
});
export default router; export default router;

View File

@@ -1,12 +1,12 @@
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.*,
@@ -22,15 +22,10 @@ router.get('/', authMiddleware, async (req, res) => {
); );
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
@@ -43,7 +38,7 @@ router.get('/:id', authMiddleware, async (req, res) => {
); );
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];
@@ -56,7 +51,7 @@ router.get('/:id', authMiddleware, async (req, res) => {
); );
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
@@ -84,19 +79,14 @@ router.get('/:id', authMiddleware, async (req, res) => {
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(
@@ -115,15 +105,10 @@ router.post('/', authMiddleware, async (req, res) => {
}; };
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;
@@ -134,7 +119,7 @@ router.post('/:id/invite', authMiddleware, async (req, res) => {
); );
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];
@@ -147,7 +132,7 @@ router.post('/:id/invite', authMiddleware, async (req, res) => {
); );
if (participation.length === 0) { if (participation.length === 0) {
return res.status(403).json({ error: 'Only challenge participants can invite others' }); throw new AppError('Only challenge participants can invite others', 403);
} }
} }
@@ -187,20 +172,15 @@ 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( await query(
@@ -209,10 +189,6 @@ router.post('/:id/respond', authMiddleware, async (req, res) => {
); );
res.json({ status }); res.json({ status });
} catch (error) { }));
console.error('Respond error:', error);
res.status(500).json({ error: 'Failed to respond to invitation' });
}
});
export default router; export default router;

View File

@@ -1,12 +1,12 @@
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) {
@@ -24,15 +24,10 @@ router.get('/search', authMiddleware, async (req, res) => {
); );
res.json({ users }); res.json({ users });
} catch (error) { }));
console.error('User search error:', error);
res.status(500).json({ error: 'Search failed' });
}
});
// 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
@@ -74,23 +69,18 @@ router.get('/', authMiddleware, async (req, res) => {
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) { if (user_id === req.user.userId) {
return res.status(400).json({ error: 'Cannot add yourself as friend' }); throw new AppError('Cannot add yourself as friend', 400);
} }
// Check if already friends or request exists // Check if already friends or request exists
@@ -101,7 +91,7 @@ router.post('/request', authMiddleware, async (req, res) => {
); );
if (existing.length > 0) { if (existing.length > 0) {
return res.status(400).json({ error: 'Friend request already exists or you are already friends' }); throw new AppError('Friend request already exists or you are already friends', 400);
} }
await query( await query(
@@ -110,19 +100,14 @@ router.post('/request', authMiddleware, async (req, res) => {
); );
res.json({ success: true }); res.json({ success: true });
} catch (error) { }));
console.error('Friend request error:', error);
res.status(500).json({ error: 'Failed to send friend request' });
}
});
// 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 // Verify the request is for the current user
@@ -132,7 +117,7 @@ router.post('/respond', authMiddleware, async (req, res) => {
); );
if (friendships.length === 0) { if (friendships.length === 0) {
return res.status(404).json({ error: 'Friend request not found' }); throw new AppError('Friend request not found', 404);
} }
await query( await query(
@@ -141,15 +126,10 @@ router.post('/respond', authMiddleware, async (req, res) => {
); );
res.json({ status }); res.json({ status });
} catch (error) { }));
console.error('Respond to friend request error:', error);
res.status(500).json({ error: 'Failed to respond to friend request' });
}
});
// 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
@@ -160,10 +140,6 @@ router.get('/requests', authMiddleware, async (req, res) => {
); );
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,12 +1,12 @@
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
@@ -18,7 +18,7 @@ router.get('/challenge/:challengeId', authMiddleware, async (req, res) => {
); );
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
@@ -54,15 +54,10 @@ router.get('/challenge/:challengeId', authMiddleware, async (req, res) => {
); );
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,
@@ -79,15 +74,10 @@ router.get('/global', authMiddleware, async (req, res) => {
); );
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(
@@ -111,14 +101,10 @@ router.get('/profile/:userId?', authMiddleware, async (req, res) => {
); );
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] }); res.json({ profile: stats[0] });
} catch (error) { }));
console.error('Profile stats error:', error);
res.status(500).json({ error: 'Failed to fetch profile' });
}
});
export default router; export default router;

View File

@@ -1,12 +1,12 @@
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
@@ -18,7 +18,7 @@ router.get('/challenge/:challengeId', authMiddleware, async (req, res) => {
); );
if (access.length === 0) { if (access.length === 0) {
return res.status(403).json({ error: 'Access denied' }); throw new AppError('Access denied', 403);
} }
// Get predictions // Get predictions
@@ -36,19 +36,14 @@ router.get('/challenge/:challengeId', authMiddleware, async (req, res) => {
); );
res.json({ predictions }); res.json({ predictions });
} catch (error) { }));
console.error('Get predictions error:', error);
res.status(500).json({ error: 'Failed to fetch 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 // Verify access
@@ -60,7 +55,7 @@ router.post('/', authMiddleware, async (req, res) => {
); );
if (access.length === 0) { if (access.length === 0) {
return res.status(403).json({ error: 'Access denied' }); throw new AppError('Access denied', 403);
} }
const result = await query( const result = await query(
@@ -79,20 +74,15 @@ router.post('/', authMiddleware, async (req, res) => {
}; };
res.json({ prediction }); res.json({ prediction });
} catch (error) { }));
console.error('Create prediction error:', error);
res.status(500).json({ error: 'Failed to create 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 // Get the prediction
@@ -102,14 +92,14 @@ router.post('/:id/validate', authMiddleware, async (req, res) => {
); );
if (predictions.length === 0) { if (predictions.length === 0) {
return res.status(404).json({ error: 'Prediction not found' }); throw new AppError('Prediction not found', 404);
} }
const prediction = predictions[0]; const prediction = predictions[0];
// Cannot validate own prediction // Cannot validate own prediction
if (prediction.user_id === req.user.userId) { if (prediction.user_id === req.user.userId) {
return res.status(403).json({ error: 'Cannot validate your own prediction' }); throw new AppError('Cannot validate your own prediction', 403);
} }
// Verify access to the challenge // Verify access to the challenge
@@ -121,7 +111,7 @@ router.post('/:id/validate', authMiddleware, async (req, res) => {
); );
if (access.length === 0) { if (access.length === 0) {
return res.status(403).json({ error: 'Access denied' }); throw new AppError('Access denied', 403);
} }
// Update prediction // Update prediction
@@ -131,10 +121,6 @@ router.post('/:id/validate', authMiddleware, async (req, res) => {
); );
res.json({ status }); res.json({ status });
} catch (error) { }));
console.error('Validate prediction error:', error);
res.status(500).json({ error: 'Failed to validate prediction' });
}
});
export default router; export default router;

View File

@@ -1,12 +1,12 @@
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) {
@@ -28,7 +28,7 @@ router.get('/search', authMiddleware, async (req, res) => {
// Fetch from TMDB // Fetch from TMDB
const apiKey = process.env.TMDB_API_KEY; const apiKey = process.env.TMDB_API_KEY;
if (!apiKey) { if (!apiKey) {
return res.status(500).json({ error: 'TMDB API key not configured' }); throw new AppError('TMDB API key not configured', 500);
} }
const fetch = (await import('node-fetch')).default; const fetch = (await import('node-fetch')).default;
@@ -39,10 +39,7 @@ router.get('/search', authMiddleware, async (req, res) => {
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
console.error('TMDB API error:', response.status, errorText); console.error('TMDB API error:', response.status, errorText);
return res.status(500).json({ throw new AppError(`TMDB API error: ${response.status}`, 500);
error: `TMDB API error: ${response.status}`,
details: errorText
});
} }
const data = await response.json(); const data = await response.json();
@@ -70,10 +67,6 @@ router.get('/search', authMiddleware, async (req, res) => {
); );
res.json(result); res.json(result);
} catch (error) { }));
console.error('TMDB search error:', error);
res.status(500).json({ error: 'Search failed' });
}
});
export default router; export default router;