feat(cms): version history panel with diff view and one-click restore
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

- GET /versions lists the automatic backups of the selected file (timestamp,
  size); ?show=<backup> renders a line diff against the current content
- POST /restore validates the backup against the content schema and restores
  it atomically; the pre-restore state gets a fresh backup first, so a
  restore itself is reversible; audited as version_restored
- dependency-free LCS line diff (scripts/cms-diff.js) with add/del
  highlighting and context trimming; backup names validated against a strict
  pattern (path traversal impossible)
- new 🕘 Verziók entry in the CMS bottom bar
- refactor: security/infra helpers extracted to scripts/cms-core.js to keep
  content-editor.js under the 400-line hard limit
- integration test: list, diff, restore + reversibility backup, traversal
  rejection, CSRF enforcement, auth

Closes MITHOME-64
This commit is contained in:
Do Siki
2026-08-18 23:45:39 +02:00
parent d15cc05c36
commit a55ce53768
7 changed files with 549 additions and 77 deletions
+82 -1
View File
@@ -113,6 +113,7 @@ ${message ? `<div class="toast ${message.type === 'ok' ? 'ok' : 'err'}">${messag
<span class="save-status" id="saveStatus"></span>
<a href="${isStaging() ? 'https://stage.mozdit.hu' : 'http://localhost:3000'}" target="_blank" class="preview-link">🔗 Előnézet →</a>
<a href="/guide" target="_blank" class="preview-link">❓ Súgó</a>
<a href="/versions?file=${activeFile}" target="_blank" class="preview-link">🕘 Verziók</a>
<span class="version-tag" title="Futó kód verziója (git SHA)">v${deployVersion}</span>
<button class="btn-logout" onclick="logout()">🚪 Kilépés</button>
</div>
@@ -237,4 +238,84 @@ async function login(e) {
</body>
</html>`;
module.exports = { HTML, GUIDE_PAGE, LOGIN_PAGE };
// Version history page: lists automatic backups of the selected file with a
// diff view (?show=) and one-click restore (POST /restore).
const VERSIONS_PAGE = (fileKey, fileLabel, versions, diff, csrfToken) => `<!DOCTYPE html>
<html lang="hu">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>mozdIT — Verziók: ${fileLabel}</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f1117; color: #e2e8f0; line-height: 1.6; padding-bottom: 64px; }
header { background: linear-gradient(135deg,#1a1f2e,#252d40); border-bottom: 1px solid #2d3748; padding: 14px 32px; display: flex; align-items: center; gap: 12px; position: sticky; top: 0; z-index: 10; }
header h1 { font-size: 17px; font-weight: 700; background: linear-gradient(135deg,#60a5fa,#a78bfa); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
header a { color: #94a3b8; text-decoration: none; font-size: 14px; margin-left: auto; }
header a:hover { color: #e2e8f0; }
main { max-width: 860px; margin: 0 auto; padding: 28px 24px; }
.note { color: #94a3b8; font-size: 14px; margin-bottom: 18px; }
.ver { background: #1a2035; border: 1px solid #2d3748; border-radius: 10px; padding: 14px 18px; margin-bottom: 10px; display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.ver .when { font-family: monospace; font-size: 14px; color: #93c5fd; }
.ver .size { color: #64748b; font-size: 13px; }
.ver .actions { margin-left: auto; display: flex; gap: 8px; }
.btn { background: #1f2937; color: #e2e8f0; border: 1px solid #374151; border-radius: 8px; padding: 8px 14px; font-size: 13px; cursor: pointer; text-decoration: none; }
.btn:hover { background: #374151; }
.btn-restore { background: #14532d; border-color: #10b981; color: #6ee7b7; }
.btn-restore:hover { background: #166534; }
h2 { font-size: 16px; margin: 26px 0 10px; color: #93c5fd; }
.diff { background: #0f1420; border: 1px solid #2d3748; border-radius: 10px; padding: 14px; font-family: monospace; font-size: 13px; overflow-x: auto; }
.diff div { padding: 1px 10px; white-space: pre-wrap; word-break: break-all; }
.diff-add { background: #064e3b; color: #6ee7b7; }
.diff-del { background: #450a0a; color: #fca5a5; text-decoration: line-through; }
.diff-skip { color: #475569; }
.diff-ctx { color: #94a3b8; }
.empty { color: #64748b; padding: 24px; text-align: center; }
</style>
</head>
<body>
<header>
<h1>🕘 Verziók — ${fileLabel}</h1>
<a href="/?file=${fileKey}">← Vissza a szerkesztőhöz</a>
</header>
<main>
<p class="note">Minden Mentés automatikus másolatot készít. A ⚖ Összehasonlítás megmutatja az adott mentés és a <strong>jelenlegi</strong> tartalom különbségét (piros = mentésben volt, zöld = most van). A visszaállítás előtt a jelenlegi állapot is mentésre kerül, tehát a visszaállítás is visszavonható.</p>
${versions.length === 0 ? '<div class="empty">Ehhez a fájlhoz még nincs mentés.</div>' : versions.map(v => `
<div class="ver">
<span class="when">${v.when}</span>
<span class="size">${v.size} B</span>
<span class="actions">
<a class="btn" href="/versions?file=${fileKey}&show=${v.name}">⚖ Összehasonlítás</a>
<button class="btn btn-restore" onclick="restore('${v.name}')">↩ Visszaállítás</button>
</span>
</div>`).join('')}
${diff ? `
<h2>Különbség: mentés (${diff.when}) → jelenlegi tartalom</h2>
<div class="diff">${diff.diffHtml}</div>` : ''}
</main>
<script>
const CSRF_TOKEN = "${csrfToken}";
const FILE = "${fileKey}";
async function restore(name) {
if (!confirm('Biztosan visszaállítod ezt a mentést?\\nA jelenlegi tartalom mentésre kerül, így ez később is visszavonható.')) return;
try {
const res = await fetch('/restore?file=' + FILE + '&backup=' + encodeURIComponent(name), {
method: 'POST',
headers: { 'X-CSRF-Token': CSRF_TOKEN }
});
if (res.status === 401) { location.href = '/login'; return; }
const json = await res.json();
if (json.ok) { alert('✅ Visszaállítva.'); location.href = '/?file=' + FILE; }
else alert('❌ Hiba: ' + json.error);
} catch (e) { alert('❌ Hálózati hiba'); }
}
</script>
</body>
</html>`;
module.exports = { HTML, GUIDE_PAGE, LOGIN_PAGE, VERSIONS_PAGE };