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
- 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
66 lines
3.3 KiB
JavaScript
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 };
|