Files
websitedev/scripts/cms-save.js
T
Do Siki 162e5782e9
CI Pipeline with Test Management / 🧪 Run Tests & Generate Reports (push) Waiting to run
CI Pipeline with Test Management / 🐳 Docker Integration Tests (push) Blocked by required conditions
CI Pipeline with Test Management / 🏗️ Build Docker Image (push) Blocked by required conditions
CI Pipeline with Test Management / 📊 Generate Test Summary (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🧪 Run Tests & Generate Reports (push) Waiting to run
Test Reporting & Gherkin Analysis / 📊 Analyze Test Coverage (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🔄 Sync with Linear (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / ⚡ Performance Monitoring (push) Blocked by required conditions
refactor(cms): split oversized modules below the 400-line limit
- content-editor.js: extract the /save handler to scripts/cms-save.js
- cms-logo-page.js: inline the ~280-line canvas editor script to
  scripts/cms-logo-client.js (page is now the HTML/CSS shell only)
- cms-editor-client.js: move the keyboard-shortcut section to
  scripts/cms-editor-shortcuts.js, inlined after the main client

All modules now under the hard limit; full pre-deploy suite green.

Closes MITHOME-80
2026-08-22 12:31:28 +02:00

66 lines
3.3 KiB
JavaScript

// 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 };