Files
questionnaire/server/index.ts

186 lines
5.7 KiB
TypeScript

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 `
<meta property="og:type" content="website" />
<meta property="og:url" content="${pageUrl}" />
<meta property="og:title" content="${title}" />
<meta property="og:description" content="${description}" />
${ogImageUrl ? `<meta property="og:image" content="${ogImageUrl}" />` : ''}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="${title}" />
<meta name="twitter:description" content="${description}" />
${ogImageUrl ? `<meta name="twitter:image" content="${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 </head>
html = html.replace('</head>', `${ogTags}</head>`);
// Update title
html = html.replace(/<title>.*?<\/title>/, `<title>${title} - Activiteiten Inventaris</title>`);
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 </head>
html = html.replace('</head>', `${ogTags}</head>`);
// Update title
html = html.replace(/<title>.*?<\/title>/, `<title>${title} - Activiteiten Inventaris</title>`);
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}`);
});