import express from 'express'; import session from 'express-session'; import cookieParser from 'cookie-parser'; import cors from 'cors'; import path from 'path'; import fs from 'fs'; import { fileURLToPath } from 'url'; import { initializeDatabase, questionnaireOps, participantOps } from './database.js'; import authRoutes from './routes/auth.js'; import adminRoutes from './routes/admin.js'; import questionnaireRoutes from './routes/questionnaire.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Initialize database initializeDatabase(); const app = express(); const PORT = process.env.PORT || 4000; const isProduction = process.env.NODE_ENV === 'production'; // Middleware app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cookieParser()); // CORS for development if (!isProduction) { app.use(cors({ origin: 'http://localhost:5173', credentials: true, })); } // Session configuration declare module 'express-session' { interface SessionData { user?: { id: number; username: string }; returnTo?: string; } } app.use(session({ secret: process.env.SESSION_SECRET || 'dev-secret-change-in-production', resave: false, saveUninitialized: false, cookie: { secure: isProduction && process.env.HTTPS === 'true', httpOnly: true, maxAge: 24 * 60 * 60 * 1000, // 24 hours sameSite: isProduction ? 'strict' : 'lax', }, })); // API Routes app.use('/api/auth', authRoutes); app.use('/api/admin', adminRoutes); app.use('/api/q', questionnaireRoutes); // Serve uploaded files const uploadsDir = isProduction ? '/app/data/uploads' : path.join(__dirname, '..', 'data', 'uploads'); // Ensure uploads directory exists if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir, { recursive: true }); } app.use('/uploads', express.static(uploadsDir)); // Serve static files in production if (isProduction) { const clientPath = path.join(__dirname, '../client'); app.use(express.static(clientPath)); // Helper function to generate OG tags HTML function generateOgTags(pageUrl: string, title: string, description: string, ogImageUrl: string | null): string { return ` ${ogImageUrl ? `` : ''} ${ogImageUrl ? `` : ''} `; } // Dynamic OG meta tags for questionnaire pages app.get('/q/:slug', (req, res) => { const questionnaire = questionnaireOps.findBySlug(req.params.slug); const indexPath = path.join(clientPath, 'index.html'); if (!questionnaire) { res.sendFile(indexPath); return; } // Read the index.html template let html = fs.readFileSync(indexPath, 'utf-8'); // Build meta tags const baseUrl = `${req.protocol}://${req.get('host')}`; const pageUrl = `${baseUrl}/q/${questionnaire.slug}`; const title = questionnaire.title; const description = questionnaire.description || 'Activiteiten Inventaris - Voeg activiteiten toe en stem!'; // Make image URL absolute if it's a relative path let ogImageUrl = questionnaire.og_image; if (ogImageUrl && ogImageUrl.startsWith('/')) { ogImageUrl = `${baseUrl}${ogImageUrl}`; } const ogTags = generateOgTags(pageUrl, title, description, ogImageUrl); // Insert OG tags before html = html.replace('', `${ogTags}`); // Update title html = html.replace(/.*?<\/title>/, `<title>${title} - Activiteiten Inventaris`); res.send(html); }); // Dynamic OG meta tags for participant access pages app.get('/q/access/:token', (req, res) => { const participant = participantOps.findByToken(req.params.token); const indexPath = path.join(clientPath, 'index.html'); if (!participant) { res.sendFile(indexPath); return; } const questionnaire = questionnaireOps.findById(participant.questionnaire_id); if (!questionnaire) { res.sendFile(indexPath); return; } // Read the index.html template let html = fs.readFileSync(indexPath, 'utf-8'); // Build meta tags const baseUrl = `${req.protocol}://${req.get('host')}`; const pageUrl = `${baseUrl}/q/access/${participant.token}`; const title = questionnaire.title; // Get first name from participant name const firstName = participant.name.trim().split(/\s+/)[0] || participant.name; const description = questionnaire.description || `Hoi ${firstName}! Voeg je ideeën toe en stem op activiteiten.`; // Make image URL absolute if it's a relative path let ogImageUrl = questionnaire.og_image; if (ogImageUrl && ogImageUrl.startsWith('/')) { ogImageUrl = `${baseUrl}${ogImageUrl}`; } const ogTags = generateOgTags(pageUrl, title, description, ogImageUrl); // Insert OG tags before html = html.replace('', `${ogTags}`); // Update title html = html.replace(/.*?<\/title>/, `<title>${title} - Activiteiten Inventaris`); res.send(html); }); // SPA fallback for other routes app.get('*', (_req, res) => { res.sendFile(path.join(clientPath, 'index.html')); }); } app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); });