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:
parent
dd502eef65
commit
6b79f9a155
2 changed files with 48 additions and 11 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue