fix(newsletter): resolve CSRF token issue for static HTML pages

Problem:
- nginx serves blog.html as static file, bypassing Express middleware
- setCsrfToken middleware never runs
- No CSRF cookie set
- Newsletter subscription fails with 403 Forbidden

Root cause:
nginx config: 'try_files $uri @proxy' serves static files directly
Location: /etc/nginx/sites-available/tractatus (line 54)

Solution:
1. blog.js now fetches CSRF token via /api/csrf-token on page load
2. getCsrfToken endpoint now creates token if missing (for static pages)
3. Newsletter form uses fetched token for subscription

Testing:
 Local test: CSRF token fetched successfully
 Newsletter subscription: Creates record in database
 Verified: test-fix@example.com subscribed via curl test

Impact:
- Newsletter subscriptions now work on production
- Fix applies to all static HTML pages (blog.html, etc.)
- Maintains CSRF protection security

Files:
- public/js/blog.js: Added fetchCsrfToken() + use in newsletter form
- src/middleware/csrf-protection.middleware.js: Enhanced getCsrfToken()

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
TheFlow 2025-10-25 09:37:16 +13:00
parent dd502eef65
commit 6b79f9a155
2 changed files with 48 additions and 11 deletions

View file

@ -16,11 +16,17 @@ const activeFilters = {
sort: 'date-desc' sort: 'date-desc'
}; };
// CSRF token (fetched on page load)
let csrfToken = null;
/** /**
* Initialize the blog page * Initialize the blog page
*/ */
async function init() { async function init() {
try { try {
// Fetch CSRF token first (required for newsletter subscription)
await fetchCsrfToken();
await loadPosts(); await loadPosts();
attachEventListeners(); attachEventListeners();
} catch (error) { } catch (error) {
@ -29,6 +35,25 @@ async function init() {
} }
} }
/**
* Fetch CSRF token from server
* Required because nginx serves blog.html as static file (bypasses setCsrfToken middleware)
*/
async function fetchCsrfToken() {
try {
const response = await fetch('/api/csrf-token');
const data = await response.json();
if (data.csrfToken) {
csrfToken = data.csrfToken;
console.log('CSRF token fetched successfully');
}
} catch (error) {
console.warn('Failed to fetch CSRF token:', error);
// Non-critical - newsletter subscription will fail but blog browsing still works
}
}
/** /**
* Load all published blog posts from API * Load all published blog posts from API
*/ */
@ -571,17 +596,20 @@ function setupNewsletterModal() {
submitBtn.textContent = 'Subscribing...'; submitBtn.textContent = 'Subscribing...';
try { try {
// Get CSRF token from cookie // Ensure we have a CSRF token
const csrfToken = document.cookie if (!csrfToken) {
.split('; ') await fetchCsrfToken();
.find(row => row.startsWith('csrf-token=')) }
?.split('=')[1];
if (!csrfToken) {
throw new Error('Unable to obtain CSRF token');
}
const response = await fetch('/api/newsletter/subscribe', { const response = await fetch('/api/newsletter/subscribe', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken || '' 'X-CSRF-Token': csrfToken
}, },
body: JSON.stringify({ body: JSON.stringify({
email, email,

View file

@ -93,15 +93,24 @@ function setCsrfToken(req, res, next) {
* Endpoint to get CSRF token for client-side usage * Endpoint to get CSRF token for client-side usage
* GET /api/csrf-token * GET /api/csrf-token
* *
* Returns the CSRF token from the cookie so client can include it in requests * Returns the CSRF token from the cookie (or creates one if missing)
* This is required for pages served as static HTML by nginx (bypassing setCsrfToken middleware)
*/ */
function getCsrfToken(req, res) { function getCsrfToken(req, res) {
const token = req.cookies['csrf-token']; let token = req.cookies['csrf-token'];
// If no token exists, create one (for static HTML pages served by nginx)
if (!token) { if (!token) {
return res.status(400).json({ token = generateCsrfToken();
error: 'Bad Request',
message: 'No CSRF token found. Visit the site first to receive a token.' // Check if we're behind a proxy (X-Forwarded-Proto header)
const isSecure = req.secure || req.headers['x-forwarded-proto'] === 'https';
res.cookie('csrf-token', token, {
httpOnly: false, // Must be false for double-submit pattern (client needs to read it)
secure: isSecure && process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}); });
} }