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
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:
@@ -0,0 +1,59 @@
|
||||
// Version history helpers for the CMS: listing automatic backups from
|
||||
// .content-backups, safe backup-name validation and diff assembly.
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { renderDiffHtml } = require('./cms-diff');
|
||||
|
||||
// Backup files are named `<fileKey>.<ISO-ish timestamp>.json`
|
||||
const BACKUP_NAME_RE = /^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/;
|
||||
|
||||
// WHY: the backup name arrives as a query parameter — only allow the exact
|
||||
// `<fileKey>.<timestamp>.json` shape so path traversal (`../`) is impossible.
|
||||
function safeBackupName(fileKey, candidate) {
|
||||
if (typeof candidate !== 'string' || !candidate.startsWith(`${fileKey}.`) || !candidate.endsWith('.json')) return null;
|
||||
const ts = candidate.slice(fileKey.length + 1, -5);
|
||||
if (!BACKUP_NAME_RE.test(ts)) return null;
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function formatBackupTimestamp(fileKey, backupName) {
|
||||
const ts = backupName.slice(fileKey.length + 1, -5);
|
||||
const m = ts.match(BACKUP_NAME_RE);
|
||||
if (!m) return ts;
|
||||
return `${m[1]} ${m[2]}:${m[3]}:${m[4]}`;
|
||||
}
|
||||
|
||||
function listVersions(backupDir, fileKey) {
|
||||
try {
|
||||
return fs.readdirSync(backupDir)
|
||||
.filter(name => safeBackupName(fileKey, name))
|
||||
.map(name => {
|
||||
const full = path.join(backupDir, name);
|
||||
const stat = fs.statSync(full);
|
||||
return { name, size: stat.size, when: formatBackupTimestamp(fileKey, name) };
|
||||
})
|
||||
.sort((a, b) => b.name.localeCompare(a.name)); // newest first
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function readBackupContent(backupDir, backupName) {
|
||||
return fs.readFileSync(path.join(backupDir, backupName), 'utf8');
|
||||
}
|
||||
|
||||
// Compare a backup with the current file content; returns both pretty texts and
|
||||
// the rendered diff HTML (backup = old/left, current = new/right).
|
||||
function buildVersionDiff(backupDir, currentFilePath, fileKey, backupName) {
|
||||
const backupText = readBackupContent(backupDir, backupName);
|
||||
const currentText = fs.readFileSync(currentFilePath, 'utf8');
|
||||
return {
|
||||
backupName,
|
||||
when: formatBackupTimestamp(fileKey, backupName),
|
||||
backupText: backupText.trim(),
|
||||
currentText: currentText.trim(),
|
||||
diffHtml: renderDiffHtml(backupText, currentText),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { safeBackupName, listVersions, readBackupContent, buildVersionDiff, formatBackupTimestamp };
|
||||
Reference in New Issue
Block a user