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'
};
// CSRF token (fetched on page load)
let csrfToken = null;
/**
* Initialize the blog page
*/
async function init() {
try {
// Fetch CSRF token first (required for newsletter subscription)
await fetchCsrfToken();
await loadPosts();
attachEventListeners();
} 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
*/
@ -571,17 +596,20 @@ function setupNewsletterModal() {
submitBtn.textContent = 'Subscribing...';
try {
// Get CSRF token from cookie
const csrfToken = document.cookie
.split('; ')
.find(row => row.startsWith('csrf-token='))
?.split('=')[1];
// Ensure we have a CSRF token
if (!csrfToken) {
await fetchCsrfToken();
}
if (!csrfToken) {
throw new Error('Unable to obtain CSRF token');
}
const response = await fetch('/api/newsletter/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken || ''
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
email,

View file

@ -93,15 +93,24 @@ function setCsrfToken(req, res, next) {
* Endpoint to get CSRF token for client-side usage
* 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) {
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) {
return res.status(400).json({
error: 'Bad Request',
message: 'No CSRF token found. Visit the site first to receive a token.'
token = generateCsrfToken();
// 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
});
}