import express from 'express'; import cors from 'cors'; import session from 'express-session'; import passport from 'passport'; import { Strategy as SteamStrategy } from 'passport-steam'; import dotenv from 'dotenv'; import { promises as fs } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); dotenv.config(); const app = express(); const PORT = process.env.PORT || 3001; const FRONTEND_URL = process.env.FRONTEND_URL || 'http://localhost:5173'; const VOTES_FILE = path.join(process.cwd(), 'votes.json'); const PRODUCTION_DOMAIN = process.env.DOMAIN || 'https://s22.ethanf.gg'; // Poll ends at 11:59 PM Eastern Time on 8/21/25 const POLL_END_DATE = new Date("2025-08-21T23:59:59-04:00"); function isPollEnded() { return new Date() > POLL_END_DATE; } // Helper functions for vote storage async function loadVotes() { try { const data = await fs.readFile(VOTES_FILE, 'utf8'); return JSON.parse(data); } catch (error) { // File doesn't exist or is empty, return empty object return {}; } } async function saveVotes(votes) { await fs.writeFile(VOTES_FILE, JSON.stringify(votes, null, 2)); } // Middleware app.use(cors({ origin: process.env.NODE_ENV === 'production' ? PRODUCTION_DOMAIN : FRONTEND_URL, credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'Cookie'], exposedHeaders: ['Set-Cookie'] })); app.use(express.json()); app.use(session({ secret: process.env.SESSION_SECRET || 'your-secret-key-change-this', resave: false, saveUninitialized: false, rolling: true, cookie: { secure: process.env.NODE_ENV === 'production', maxAge: 24 * 60 * 60 * 1000, // 24 hours httpOnly: true, sameSite: 'lax' }, name: 's22poll.sid', proxy: process.env.NODE_ENV === 'production' })); app.use(passport.initialize()); app.use(passport.session()); // Serve static files from React build in production if (process.env.NODE_ENV === 'production') { app.use(express.static(path.join(__dirname, '../dist'))); } // Passport Steam Strategy passport.use(new SteamStrategy({ returnURL: process.env.NODE_ENV === 'production' ? `${PRODUCTION_DOMAIN}/auth/steam/return` : 'http://localhost:3001/auth/steam/return', realm: process.env.NODE_ENV === 'production' ? PRODUCTION_DOMAIN : 'http://localhost:3001/', apiKey: process.env.STEAM_API_KEY }, function(identifier, profile, done) { const user = { steamId: profile.id, displayName: profile.displayName, avatar: profile.photos[0].value, profileUrl: profile._json.profileurl }; return done(null, user); } )); passport.serializeUser((user, done) => { done(null, user); }); passport.deserializeUser((user, done) => { done(null, user); }); // Routes app.get('/auth/steam', passport.authenticate('steam')); app.get('/auth/steam/return', passport.authenticate('steam', { failureRedirect: '/' }), (req, res) => { console.log('User authenticated:', req.user); console.log('Session ID:', req.sessionID); console.log('Session:', req.session); console.log('Request host:', req.get('host')); console.log('Request headers:', req.headers); console.log('Response will set cookie for domain:', req.get('host')); // Explicitly save the session before redirecting req.session.save((err) => { if (err) { console.error('Session save error:', err); return res.redirect('/'); } console.log('Session saved successfully'); // In production, redirect to root since frontend and backend are on same domain const redirectUrl = process.env.NODE_ENV === 'production' ? '/' : FRONTEND_URL; console.log('Redirecting to:', redirectUrl); res.redirect(redirectUrl); }); } ); app.post('/auth/logout', (req, res) => { req.logout((err) => { if (err) { return res.status(500).json({ error: 'Logout failed' }); } res.json({ message: 'Logged out successfully' }); }); }); app.get('/auth/user', (req, res) => { console.log('Auth check - Session ID:', req.sessionID); console.log('Auth check - Is authenticated:', req.isAuthenticated()); console.log('Auth check - User:', req.user); console.log('Auth check - Session:', req.session); console.log('Auth check - Request host:', req.get('host')); console.log('Auth check - Cookies:', req.headers.cookie); if (req.isAuthenticated()) { res.json({ user: req.user }); } else { res.json({ user: null }); } }); // Debug endpoint to test cookies app.get('/debug/cookies', (req, res) => { res.json({ sessionID: req.sessionID, cookies: req.headers.cookie, session: req.session, isAuthenticated: req.isAuthenticated(), user: req.user }); }); // Vote submission endpoint app.post('/api/submit-vote', async (req, res) => { if (!req.isAuthenticated()) { console.error('User not authenticated'); return res.status(401).json({ error: 'Must be logged in to vote' }); } // Check if poll has ended if (isPollEnded()) { return res.status(400).json({ error: 'The poll has ended', pollEndDate: POLL_END_DATE.toISOString() }); } const { vote } = req.body; const user = req.user; try { // Load existing votes const votes = await loadVotes(); // Check if user has already voted if (votes[user.steamId]) { return res.status(400).json({ error: 'You have already submitted a vote' }); } // Save the vote votes[user.steamId] = { steamId: user.steamId, displayName: user.displayName, vote: vote, timestamp: new Date().toISOString() }; await saveVotes(votes); console.log(`Vote submitted by ${user.displayName} (${user.steamId}):`, vote); res.json({ success: true, message: 'Vote submitted successfully', user: user.displayName }); } catch (error) { console.error('Error saving vote:', error); res.status(500).json({ error: 'Failed to save vote' }); } }); // Check if user has already voted app.get('/api/vote-status', async (req, res) => { if (!req.isAuthenticated()) { return res.status(401).json({ error: 'Not authenticated' }); } try { const votes = await loadVotes(); const userVote = votes[req.user.steamId]; res.json({ hasVoted: !!userVote, vote: userVote ? userVote.vote : null, timestamp: userVote ? userVote.timestamp : null, isPollEnded: isPollEnded(), pollEndDate: POLL_END_DATE.toISOString() }); } catch (error) { console.error('Error checking vote status:', error); res.status(500).json({ error: 'Failed to check vote status' }); } }); // Get vote results (admin endpoint - you might want to add authentication) app.get('/api/results', async (req, res) => { try { const votes = await loadVotes(); const voteArray = Object.values(votes); // Calculate results const mapCounts = {}; const totalVotes = voteArray.length; voteArray.forEach(voteData => { voteData.vote.forEach((map, index) => { if (!mapCounts[map.name]) { mapCounts[map.name] = { name: map.name, positions: [0, 0, 0, 0], totalScore: 0 }; } mapCounts[map.name].positions[index]++; // Higher position = higher score (4 points for 1st, 3 for 2nd, etc.) mapCounts[map.name].totalScore += (4 - index); }); }); res.json({ totalVotes, results: Object.values(mapCounts).sort((a, b) => b.totalScore - a.totalScore) }); } catch (error) { console.error('Error getting results:', error); res.status(500).json({ error: 'Failed to get results' }); } }); if (process.env.NODE_ENV === 'production') { app.get('*', (req, res) => { res.sendFile(path.join(__dirname, '../dist/index.html')); }); } app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); console.log(`Environment: ${process.env.NODE_ENV || 'development'}`); });