s22-poll/server/server.js

262 lines
7.6 KiB
JavaScript

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: true,
saveUninitialized: false,
rolling: true,
cookie: {
secure: process.env.NODE_ENV === 'production',
maxAge: 24 * 60 * 60 * 1000, // 24 hours
httpOnly: true,
sameSite: process.env.NODE_ENV === 'production' ? 'lax' : 'lax'
},
name: 's22poll.sid'
}));
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'));
// 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 });
}
});
// Vote submission endpoint
app.post('/api/submit-vote', async (req, res) => {
if (!req.isAuthenticated()) {
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'}`);
});