fix(cms): implement v2 security and stability review findings
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
Resolves: - CSRF false positive checked (global POST protection) - Publish mutex to prevent git lock / double deploy - Basic Auth rate limit checked before credential evaluation - Memory leak in rate limiter (added GC interval) - XSS in Toast messages - XSS in data-path attribute - CI healthcheck port mismatch (3000 -> 8080) - Added security headers (X-Frame-Options, X-Content-Type-Options)
This commit is contained in:
@@ -29,13 +29,13 @@ function renderPrimitive(path, val, container) {
|
||||
const type = val === null ? 'null' : typeof val;
|
||||
let control;
|
||||
if (type === 'boolean') {
|
||||
control = `<input type="checkbox" data-path="${path}" data-type="boolean" ${val ? 'checked' : ''}>`;
|
||||
control = `<input type="checkbox" data-path="${esc(path)}" data-type="boolean" ${val ? 'checked' : ''}>`;
|
||||
} else if (type === 'number') {
|
||||
control = `<input type="number" data-path="${path}" data-type="number" value="${esc(val)}">`;
|
||||
control = `<input type="number" data-path="${esc(path)}" data-type="number" value="${esc(val)}">`;
|
||||
} else {
|
||||
control = isLong
|
||||
? `<textarea data-path="${path}" data-type="${type}" rows="${Math.min(8,Math.max(2,Math.ceil(String(val).length/80)))}">${esc(val ?? '')}<\/textarea>`
|
||||
: `<input type="text" data-path="${path}" data-type="${type}" value="${esc(val ?? '')}">`;
|
||||
? `<textarea data-path="${esc(path)}" data-type="${type}" rows="${Math.min(8,Math.max(2,Math.ceil(String(val).length/80)))}">${esc(val ?? '')}<\/textarea>`
|
||||
: `<input type="text" data-path="${esc(path)}" data-type="${type}" value="${esc(val ?? '')}">`;
|
||||
}
|
||||
div.innerHTML = `
|
||||
<label>${path}</label>
|
||||
@@ -242,41 +242,54 @@ function parsePath(path) {
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const data = collect();
|
||||
const res = await fetch('/save?file=' + FILE, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': CSRF_TOKEN, 'X-Content-Hash': CONTENT_HASH },
|
||||
body: JSON.stringify(data, null, 2)
|
||||
});
|
||||
if (res.status === 401) { location.href = '/login'; return; }
|
||||
if (res.status === 409) {
|
||||
if (confirm('A tartalom megváltozott, mióta ez a lap megnyílt (pl. deploy vagy másik fül mentett).\n\nOK = lap frissítése az új tartalommal (a szerkesztésed elvész)\nMégse = maradsz ezen a lapon, a mentés nem történt meg.')) {
|
||||
location.reload();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const json = await res.json();
|
||||
const status = document.getElementById('saveStatus');
|
||||
if (json.ok) {
|
||||
// Refresh the optimistic-lock fingerprint with the server-computed hash of
|
||||
// the saved content, so the user's own subsequent saves don't trip 409.
|
||||
if (json.contentHash) CONTENT_HASH = json.contentHash;
|
||||
status.textContent = '✅ Mentve!';
|
||||
status.style.color = '#10b981';
|
||||
} else {
|
||||
status.textContent = '❌ Hiba: ' + json.error;
|
||||
try {
|
||||
const data = collect();
|
||||
const res = await fetch('/save?file=' + FILE, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': CSRF_TOKEN, 'X-Content-Hash': CONTENT_HASH },
|
||||
body: JSON.stringify(data, null, 2)
|
||||
});
|
||||
if (res.status === 401) { location.href = '/login'; return false; }
|
||||
if (res.status === 409) {
|
||||
if (confirm('A tartalom megváltozott, mióta ez a lap megnyílt (pl. deploy vagy másik fül mentett).\n\nOK = lap frissítése az új tartalommal (a szerkesztésed elvész)\nMégse = maradsz ezen a lapon, a mentés nem történt meg.')) {
|
||||
location.reload();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const json = await res.json();
|
||||
if (json.ok) {
|
||||
// Refresh the optimistic-lock fingerprint with the server-computed hash of
|
||||
// the saved content, so the user's own subsequent saves don't trip 409.
|
||||
if (json.contentHash) CONTENT_HASH = json.contentHash;
|
||||
status.textContent = '✅ Mentve!';
|
||||
status.style.color = '#10b981';
|
||||
status.style.visibility = 'visible';
|
||||
setTimeout(() => status.style.visibility = 'hidden', 3000);
|
||||
return true;
|
||||
} else {
|
||||
status.textContent = '❌ Hiba: ' + json.error;
|
||||
status.style.color = '#f87171';
|
||||
status.style.visibility = 'visible';
|
||||
setTimeout(() => status.style.visibility = 'hidden', 5000);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
status.textContent = '❌ Hálózati hiba mentéskor';
|
||||
status.style.color = '#f87171';
|
||||
status.style.visibility = 'visible';
|
||||
setTimeout(() => status.style.visibility = 'hidden', 5000);
|
||||
return false;
|
||||
}
|
||||
status.style.visibility = 'visible';
|
||||
setTimeout(() => status.style.visibility = 'hidden', 3000);
|
||||
}
|
||||
|
||||
async function publish() {
|
||||
const btn = document.getElementById('publishBtn');
|
||||
const status = document.getElementById('saveStatus');
|
||||
|
||||
// Save first
|
||||
await save();
|
||||
// Save first — abort publish if save failed (e.g. 409 conflict, validation error)
|
||||
const saved = await save();
|
||||
if (!saved) return;
|
||||
|
||||
// WHY: lock the button width and remember the label so the running state
|
||||
// neither resizes the bottom bar nor permanently swaps the env-specific label.
|
||||
|
||||
Reference in New Issue
Block a user