worldplay-website/server.js

209 lines
6.5 KiB
JavaScript

const express = require('express');
const fs = require('fs').promises;
const path = require('path');
const app = express();
const PORT = process.env.PORT || 3000;
const DATA_DIR = process.env.DATA_DIR || './data';
const REGISTRATIONS_FILE = path.join(DATA_DIR, 'registrations.json');
// Middleware
app.use(express.json());
app.use(express.static('.'));
// Ensure data directory exists
async function ensureDataDir() {
try {
await fs.mkdir(DATA_DIR, { recursive: true });
try {
await fs.access(REGISTRATIONS_FILE);
} catch {
await fs.writeFile(REGISTRATIONS_FILE, '[]');
}
} catch (error) {
console.error('Error creating data directory:', error);
}
}
// Load registrations
async function loadRegistrations() {
try {
const data = await fs.readFile(REGISTRATIONS_FILE, 'utf8');
return JSON.parse(data);
} catch {
return [];
}
}
// Save registrations
async function saveRegistrations(registrations) {
await fs.writeFile(REGISTRATIONS_FILE, JSON.stringify(registrations, null, 2));
}
// Registration endpoint
app.post('/api/register', async (req, res) => {
try {
const { firstName, lastName, email, location, role, interests, contribute, message } = req.body;
// Validation
if (!firstName || !lastName || !email) {
return res.status(400).json({ error: 'First name, last name, and email are required' });
}
if (!email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
return res.status(400).json({ error: 'Please provide a valid email address' });
}
// Load existing registrations
const registrations = await loadRegistrations();
// Check for duplicate email
if (registrations.some(r => r.email.toLowerCase() === email.toLowerCase())) {
return res.status(400).json({ error: 'This email is already registered' });
}
// Create new registration
const registration = {
id: Date.now().toString(36) + Math.random().toString(36).substr(2),
firstName: firstName.trim(),
lastName: lastName.trim(),
email: email.toLowerCase().trim(),
location: location?.trim() || '',
role: role || '',
interests: interests || [],
contribute: contribute || '',
message: message?.trim() || '',
registeredAt: new Date().toISOString(),
ipAddress: req.ip || req.connection.remoteAddress
};
// Save registration
registrations.push(registration);
await saveRegistrations(registrations);
console.log(`New registration: ${registration.firstName} ${registration.lastName} <${registration.email}>`);
res.json({
success: true,
message: 'Registration successful',
id: registration.id
});
} catch (error) {
console.error('Registration error:', error);
res.status(500).json({ error: 'An error occurred. Please try again.' });
}
});
// Admin endpoint to view registrations (protected by simple token)
app.get('/api/registrations', async (req, res) => {
const token = req.headers['x-admin-token'] || req.query.token;
const adminToken = process.env.ADMIN_TOKEN || 'worldplay-admin-2026';
if (token !== adminToken) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
const registrations = await loadRegistrations();
res.json({
count: registrations.length,
registrations: registrations.map(r => ({
...r,
ipAddress: undefined // Don't expose IP in admin view
}))
});
} catch (error) {
res.status(500).json({ error: 'Failed to load registrations' });
}
});
// Export registrations as CSV
app.get('/api/registrations/export', async (req, res) => {
const token = req.headers['x-admin-token'] || req.query.token;
const adminToken = process.env.ADMIN_TOKEN || 'worldplay-admin-2026';
if (token !== adminToken) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
const registrations = await loadRegistrations();
const headers = ['ID', 'First Name', 'Last Name', 'Email', 'Location', 'Role', 'Interests', 'Contribute', 'Message', 'Registered At'];
const rows = registrations.map(r => [
r.id,
r.firstName,
r.lastName,
r.email,
r.location,
r.role,
Array.isArray(r.interests) ? r.interests.join('; ') : r.interests,
r.contribute,
r.message.replace(/"/g, '""'),
r.registeredAt
]);
const csv = [
headers.join(','),
...rows.map(row => row.map(cell => `"${cell}"`).join(','))
].join('\n');
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename=worldplay-registrations.csv');
res.send(csv);
} catch (error) {
res.status(500).json({ error: 'Failed to export registrations' });
}
});
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Stats endpoint
app.get('/api/stats', async (req, res) => {
try {
const registrations = await loadRegistrations();
const stats = {
totalRegistrations: registrations.length,
byRole: {},
byInterest: {},
byContribute: {}
};
registrations.forEach(r => {
// Count by role
if (r.role) {
stats.byRole[r.role] = (stats.byRole[r.role] || 0) + 1;
}
// Count by interest
if (Array.isArray(r.interests)) {
r.interests.forEach(interest => {
stats.byInterest[interest] = (stats.byInterest[interest] || 0) + 1;
});
}
// Count by contribute
if (r.contribute) {
stats.byContribute[r.contribute] = (stats.byContribute[r.contribute] || 0) + 1;
}
});
res.json(stats);
} catch (error) {
res.status(500).json({ error: 'Failed to calculate stats' });
}
});
// Start server
ensureDataDir().then(() => {
app.listen(PORT, '0.0.0.0', () => {
console.log(`WORLDPLAY server running on port ${PORT}`);
console.log(`Admin token: ${process.env.ADMIN_TOKEN || 'worldplay-admin-2026'}`);
});
});