// POST /save handler for the Content Editor — extracted to keep content-editor.js // under the 400-line hard limit. Returns true when the request was handled. const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); function handleSaveRoute({ req, res, u, activeFile, files, maxBodyBytes, validate, writeAudit, backupAndWrite, backupDir, user, clientAddress, cmsDirname, }) { if (req.method !== 'POST' || u.pathname !== '/save') return false; let body = ''; let bodyTooLarge = false; let bodySize = 0; req.on('data', c => { bodySize += c.length; if (bodySize > maxBodyBytes) { bodyTooLarge = true; return; } body += c; }); req.on('end', () => { try { if (bodyTooLarge) { writeAudit('content_saved', { clientAddress, user, file: activeFile, result: 'request_too_large' }); res.writeHead(413, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: `A kérés túl nagy (maximum ${maxBodyBytes} byte)` })); return; } const data = JSON.parse(body); // Optimistic locking: the editor echoes the fingerprint of the content it // loaded. If the file changed since (deploy, another tab, git), a blind // save would silently overwrite those changes — reject with 409 instead. const clientHash = req.headers['x-content-hash']; const currentOnDisk = fs.readFileSync(files[activeFile], 'utf8').trim(); const currentHash = crypto.createHash('sha256').update(currentOnDisk).digest('hex'); if (typeof clientHash !== 'string' || clientHash !== currentHash) { writeAudit('content_saved', { clientAddress, user, file: activeFile, result: 'conflict' }); res.writeHead(409, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: 'A tartalom megváltozott, mióta ezt a lapot megnyitottad (pl. deploy vagy másik fül mentett). Frissítsd az oldalt, és végezd el újra a módosításokat.' })); return; } const validation = validate(activeFile, data); if (!validation.ok) { writeAudit('content_saved', { clientAddress, user, file: activeFile, result: 'validation_failed' }); res.writeHead(422, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: validation.errors.join('; '), errors: validation.errors })); return; } const backupFile = backupAndWrite(files[activeFile], data, backupDir); // Return the hash of the written content so the editor tab can refresh its // fingerprint — otherwise the user's OWN next save would trip the lock. const newHash = crypto.createHash('sha256').update(JSON.stringify(data, null, 2).trim()).digest('hex'); writeAudit('content_saved', { clientAddress, user, file: activeFile, result: 'ok' }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: true, backup: path.relative(cmsDirname, backupFile), contentHash: newHash })); } catch (e) { writeAudit('content_saved', { clientAddress, user, file: activeFile, result: 'error' }); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: e.message })); } }); return true; } module.exports = { handleSaveRoute };