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

View File

@@ -1,12 +1,12 @@
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 {
router.get('/', authMiddleware, asyncHandler(async (req, res) => {
const challenges = await query(
`SELECT
c.*,
@@ -22,15 +22,10 @@ router.get('/', authMiddleware, async (req, res) => {
);
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
router.get('/:id', authMiddleware, async (req, res) => {
try {
router.get('/:id', authMiddleware, asyncHandler(async (req, res) => {
const challengeId = req.params.id;
// Get challenge details
@@ -43,7 +38,7 @@ router.get('/:id', authMiddleware, async (req, res) => {
);
if (challenges.length === 0) {
return res.status(404).json({ error: 'Challenge not found' });
throw new AppError('Challenge not found', 404);
}
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) {
return res.status(403).json({ error: 'Access denied' });
throw new AppError('Access denied', 403);
}
// Get participants with their points
@@ -84,19 +79,14 @@ router.get('/:id', authMiddleware, async (req, res) => {
participants,
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
router.post('/', authMiddleware, async (req, res) => {
try {
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' });
throw new AppError('Title is required', 400);
}
const result = await query(
@@ -115,15 +105,10 @@ router.post('/', authMiddleware, async (req, res) => {
};
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
router.post('/:id/invite', authMiddleware, async (req, res) => {
try {
router.post('/:id/invite', authMiddleware, asyncHandler(async (req, res) => {
const challengeId = req.params.id;
const { user_ids, emails } = req.body;
@@ -134,7 +119,7 @@ router.post('/:id/invite', authMiddleware, async (req, res) => {
);
if (challenges.length === 0) {
return res.status(404).json({ error: 'Challenge not found' });
throw new AppError('Challenge not found', 404);
}
const challenge = challenges[0];
@@ -147,7 +132,7 @@ router.post('/:id/invite', authMiddleware, async (req, res) => {
);
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 });
} catch (error) {
console.error('Invite error:', error);
res.status(500).json({ error: 'Failed to send invites' });
}
});
}));
// Accept/reject challenge invitation
router.post('/:id/respond', authMiddleware, async (req, res) => {
try {
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' });
throw new AppError('Invalid status', 400);
}
await query(
@@ -209,10 +189,6 @@ router.post('/:id/respond', authMiddleware, async (req, res) => {
);
res.json({ status });
} catch (error) {
console.error('Respond error:', error);
res.status(500).json({ error: 'Failed to respond to invitation' });
}
});
}));
export default router;

View File

@@ -1,12 +1,12 @@
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 {
router.get('/search', authMiddleware, asyncHandler(async (req, res) => {
const { q } = req.query;
if (!q || q.trim().length < 2) {
@@ -24,15 +24,10 @@ router.get('/search', authMiddleware, async (req, res) => {
);
res.json({ users });
} catch (error) {
console.error('User search error:', error);
res.status(500).json({ error: 'Search failed' });
}
});
}));
// Get all friends
router.get('/', authMiddleware, async (req, res) => {
try {
router.get('/', authMiddleware, asyncHandler(async (req, res) => {
// Get accepted friendships (bidirectional)
const friends = await query(
`SELECT DISTINCT
@@ -74,23 +69,18 @@ router.get('/', authMiddleware, async (req, res) => {
friends,
challenge_friends: challengeFriends
});
} catch (error) {
console.error('Get friends error:', error);
res.status(500).json({ error: 'Failed to fetch friends' });
}
});
}));
// Send friend request
router.post('/request', authMiddleware, async (req, res) => {
try {
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' });
throw new AppError('User ID required', 400);
}
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
@@ -101,7 +91,7 @@ router.post('/request', authMiddleware, async (req, res) => {
);
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(
@@ -110,19 +100,14 @@ router.post('/request', authMiddleware, async (req, res) => {
);
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
router.post('/respond', authMiddleware, async (req, res) => {
try {
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' });
throw new AppError('Invalid status', 400);
}
// Verify the request is for the current user
@@ -132,7 +117,7 @@ router.post('/respond', authMiddleware, async (req, res) => {
);
if (friendships.length === 0) {
return res.status(404).json({ error: 'Friend request not found' });
throw new AppError('Friend request not found', 404);
}
await query(
@@ -141,15 +126,10 @@ router.post('/respond', authMiddleware, async (req, res) => {
);
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
router.get('/requests', authMiddleware, async (req, res) => {
try {
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
@@ -160,10 +140,6 @@ router.get('/requests', authMiddleware, async (req, res) => {
);
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;

View File

@@ -1,12 +1,12 @@
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 {
router.get('/challenge/:challengeId', authMiddleware, asyncHandler(async (req, res) => {
const { challengeId } = req.params;
// Verify access
@@ -18,7 +18,7 @@ router.get('/challenge/:challengeId', authMiddleware, async (req, res) => {
);
if (access.length === 0) {
return res.status(403).json({ error: 'Access denied' });
throw new AppError('Access denied', 403);
}
// Get leaderboard
@@ -54,15 +54,10 @@ router.get('/challenge/:challengeId', authMiddleware, async (req, res) => {
);
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)
router.get('/global', authMiddleware, async (req, res) => {
try {
router.get('/global', authMiddleware, asyncHandler(async (req, res) => {
const leaderboard = await query(
`SELECT
u.id,
@@ -79,15 +74,10 @@ router.get('/global', authMiddleware, async (req, res) => {
);
res.json({ leaderboard });
} catch (error) {
console.error('Global leaderboard error:', error);
res.status(500).json({ error: 'Failed to fetch leaderboard' });
}
});
}));
// Get user profile stats
router.get('/profile/:userId?', authMiddleware, async (req, res) => {
try {
router.get('/profile/:userId?', authMiddleware, asyncHandler(async (req, res) => {
const userId = req.params.userId || req.user.userId;
const stats = await query(
@@ -111,14 +101,10 @@ router.get('/profile/:userId?', authMiddleware, async (req, res) => {
);
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' });
}
});
}));
export default router;

View File

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

View File

@@ -1,12 +1,12 @@
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 {
router.get('/search', authMiddleware, asyncHandler(async (req, res) => {
const { q } = req.query;
if (!q || q.trim().length < 2) {
@@ -28,7 +28,7 @@ router.get('/search', authMiddleware, async (req, res) => {
// Fetch from TMDB
const apiKey = process.env.TMDB_API_KEY;
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;
@@ -39,10 +39,7 @@ router.get('/search', authMiddleware, async (req, res) => {
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
});
throw new AppError(`TMDB API error: ${response.status}`, 500);
}
const data = await response.json();
@@ -70,10 +67,6 @@ router.get('/search', authMiddleware, async (req, res) => {
);
res.json(result);
} catch (error) {
console.error('TMDB search error:', error);
res.status(500).json({ error: 'Search failed' });
}
});
}));
export default router;