Files
websitedev/scripts/cms-pages.js
T
Do Siki 5fe36584dd
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
feat(cms): confirm-before-logout and branded login page
- logout asks for confirmation, then invalidates the server-side session
  and navigates to a public /login page (logo, form, error messages)
- POST /login validates credentials (timing-safe) and issues an HttpOnly
  SameSite=Strict session cookie (8h, Secure behind HTTPS); Basic Auth
  stays valid in parallel for curl/API use
- unauthenticated browser navigations redirect to /login; non-browser
  requests keep the 401 challenge
- failed form logins share the auth rate-limit budget with Basic attempts
- save/publish redirect to /login when the session expired
- refactor: templates and browser script extracted to scripts/cms-pages.js
  and scripts/cms-editor-client.js, session logic to scripts/cms-session.js
  (content-editor.js back under the 400-line limit)
- user guide updated (login page, confirmation, 8h session)

Closes MITHOME-58
2026-08-18 14:01:42 +02:00

238 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Page templates for the Content Editor. Kept separate so content-editor.js
// stays focused on routing/handling and below the file-size limits.
const isStaging = () => process.env.CMS_DEPLOY_ENV === 'staging';
// FILE_LABELS is injected to avoid a circular dependency with the main file.
const HTML = (activeFile, jsonData, message, csrfToken, fileLabels, clientJs) => `<!DOCTYPE html>
<html lang="hu">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${isStaging() ? 'STAGING — ' : ''}mozdIT Content Editor</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; min-height: 100vh; }
.environment-banner { background: #f59e0b; color: #111827; padding: 9px 32px; text-align: center; font-size: 13px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
header { background: linear-gradient(135deg,#1a1f2e,#252d40); border-bottom: 1px solid #2d3748; padding: 14px 32px; display: flex; align-items: center; gap: 12px; }
header h1 { font-size: 17px; font-weight: 700; background: linear-gradient(135deg,#60a5fa,#a78bfa); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
header span { color: #64748b; font-size: 13px; }
.tabs { display: flex; gap: 2px; padding: 14px 32px 0; border-bottom: 1px solid #2d3748; background: #13192a; }
.tab { text-decoration: none; color: #94a3b8; padding: 9px 16px; border-radius: 8px 8px 0 0; font-size: 13px; font-weight: 500; transition: all .2s; border: 1px solid transparent; border-bottom: none; margin-bottom: -1px; }
.tab:hover { color: #e2e8f0; background: #1e2535; }
.tab.active { color: #60a5fa; background: #0f1117; border-color: #2d3748; }
.page { max-width: 860px; margin: 28px auto 120px; padding: 0 24px; }
.hint { color: #475569; font-size: 12px; margin-bottom: 20px; }
/* Primitive field */
.field { background: #1a2035; border: 1px solid #2d3748; border-radius: 10px; padding: 14px 16px; transition: border-color .2s; margin-bottom: 10px; }
.field:focus-within { border-color: #60a5fa; }
.field label { display: block; font-size: 11px; font-weight: 600; color: #60a5fa; text-transform: uppercase; letter-spacing:.05em; margin-bottom: 7px; font-family: monospace; }
.field input, .field textarea { width: 100%; background: transparent; border: none; outline: none; color: #e2e8f0; font-size: 14px; line-height: 1.6; resize: vertical; font-family: inherit; }
.field textarea { min-height: 52px; }
/* Array section */
.array-section { margin-bottom: 20px; }
.array-label { font-size: 12px; font-weight: 700; color: #a78bfa; text-transform: uppercase; letter-spacing:.06em; font-family: monospace; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
.array-label::after { content:''; flex: 1; height: 1px; background: #2d3748; }
.array-items { display: flex; flex-direction: column; gap: 8px; }
/* Simple string array item */
.str-item { display: flex; gap: 8px; align-items: flex-start; }
.str-item textarea { flex: 1; background: #1a2035; border: 1px solid #2d3748; border-radius: 8px; padding: 10px 12px; color: #e2e8f0; font-size: 14px; font-family: inherit; outline: none; resize: vertical; min-height: 44px; transition: border-color .2s; }
.str-item textarea:focus { border-color: #60a5fa; }
/* Object array item (card) */
.obj-card { background: #1a2035; border: 1px solid #2d3748; border-radius: 10px; padding: 14px; position: relative; }
.obj-card .card-header { font-size: 11px; color: #64748b; font-family: monospace; margin-bottom: 10px; }
.obj-card .inner-field { margin-bottom: 8px; }
.obj-card .inner-field:last-child { margin-bottom: 0; }
.obj-card .inner-label { font-size: 10px; font-weight: 600; color: #94a3b8; text-transform: uppercase; letter-spacing:.05em; font-family: monospace; margin-bottom: 4px; }
.obj-card input, .obj-card textarea { width: 100%; background: #0f1420; border: 1px solid #2d3748; border-radius: 6px; padding: 8px 10px; color: #e2e8f0; font-size: 13px; font-family: inherit; outline: none; resize: vertical; transition: border-color .2s; }
.obj-card input:focus, .obj-card textarea:focus { border-color: #60a5fa; }
/* Buttons */
.btn-del { background: transparent; border: 1px solid #3f1c1c; color: #f87171; border-radius: 7px; padding: 6px 10px; cursor: pointer; font-size: 13px; transition: all .2s; white-space: nowrap; flex-shrink: 0; }
.btn-del:hover { background: #3f1c1c; }
.btn-del-card { position: absolute; top: 10px; right: 10px; background: transparent; border: 1px solid #3f1c1c; color: #f87171; border-radius: 6px; padding: 4px 8px; cursor: pointer; font-size: 12px; transition: all .2s; }
.btn-del-card:hover { background: #3f1c1c; }
.btn-add { background: transparent; border: 1px dashed #334155; color: #64748b; border-radius: 8px; padding: 9px 16px; cursor: pointer; font-size: 13px; width: 100%; text-align: center; transition: all .2s; margin-top: 6px; }
.btn-add:hover { border-color: #a78bfa; color: #a78bfa; background: #1a1535; }
/* Bottom bar */
.bottom-bar { position: fixed; bottom: 0; left: 0; right: 0; background: #0f1117; border-top: 1px solid #2d3748; padding: 14px 32px; display: flex; gap: 14px; align-items: center; z-index: 50; }
.btn-logout { margin-left: auto; background: #1f2937; color: #e2e8f0; border: 1px solid #374151; border-radius: 8px; padding: 9px 16px; font-size: 14px; cursor: pointer; }
.btn-logout:hover { background: #374151; }
.btn-save { background: linear-gradient(135deg,#3b82f6,#6366f1); color: #fff; border: none; padding: 11px 26px; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: opacity .2s, transform .1s; }
.btn-save:hover { opacity: .9; transform: translateY(-1px); }
.btn-save:active { transform: translateY(0); }
.btn-publish { background: linear-gradient(135deg,#10b981,#059669); color: #fff; border: none; padding: 11px 26px; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: opacity .2s, transform .1s; }
.btn-publish:hover { opacity: .9; transform: translateY(-1px); }
.btn-publish:active { transform: translateY(0); }
.preview-link { color: #64748b; font-size: 13px; text-decoration: none; margin-left: auto; }
.preview-link:hover { color: #94a3b8; }
.save-status { font-size: 13px; font-weight: 500; display: none; margin-left: 8px; }
/* Toast */
.toast { position: fixed; top: 20px; right: 20px; padding: 13px 18px; border-radius: 9px; font-size: 14px; font-weight: 500; z-index: 200; animation: slideIn .3s ease; }
.toast.ok { background: #064e3b; border: 1px solid #10b981; color: #6ee7b7; }
.toast.err { background: #450a0a; border: 1px solid #ef4444; color: #fca5a5; }
@keyframes slideIn { from { opacity:0; transform: translateX(20px); } to { opacity:1; transform: translateX(0); } }
</style>
</head>
<body>
${isStaging() ? '<div class="environment-banner">⚠ STAGING / TESZTKÖRNYEZET — itt végzett publikálás csak a staging oldalt frissíti</div>' : ''}
${message ? `<div class="toast ${message.type === 'ok' ? 'ok' : 'err'}">${message.text}</div>` : ''}
<header>
<h1>mozdIT Content Editor</h1>
<span>— JSON fájlok szerkesztése vizuálisan</span>
</header>
<nav class="tabs">
${Object.entries(fileLabels).map(([k, l]) =>
`<a href="/?file=${k}" class="tab ${activeFile === k ? 'active' : ''}">${l}</a>`
).join('')}
</nav>
<div class="page">
<p class="hint">📝 Szerkeszd a mezőket. Tömbökből elemet törölhetsz (❌) vagy hozzáadhatsz (). Mentés gomb menti a fájlt.</p>
<div id="editor"></div>
</div>
<div class="bottom-bar">
<button class="btn-save" onclick="save()">💾 Mentés</button>
<button class="btn-publish" onclick="publish()" id="publishBtn">🚀 Publikálás & ${isStaging() ? 'Staging deploy' : 'Élesítés'}</button>
<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>
<button class="btn-logout" onclick="logout()">🚪 Kilépés</button>
</div>
<script id="page-data" type="application/json">${jsonData.replace(/<\//g, '<\\/')}</script>
<script>
const DATA = JSON.parse(document.getElementById('page-data').textContent);
const FILE = "${activeFile}";
const CSRF_TOKEN = "${csrfToken}";
${clientJs}
</script>
</script>
</body>
</html>`;
// User guide page — renders docs/felhasznaloi-utmutato.md with the shared dark theme.
const GUIDE_PAGE = (contentHtml) => `<!DOCTYPE html>
<html lang="hu">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>mozdIT — Felhasználói útmutató</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.65; 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: 760px; margin: 0 auto; padding: 32px 24px; }
h1 { font-size: 24px; margin: 16px 0 12px; color: #f1f5f9; }
h2 { font-size: 20px; margin: 28px 0 10px; color: #93c5fd; border-bottom: 1px solid #2d3748; padding-bottom: 6px; }
h3 { font-size: 16px; margin: 20px 0 8px; color: #c4b5fd; }
h4 { font-size: 14px; margin: 16px 0 6px; color: #c4b5fd; }
p { margin: 8px 0; }
ul, ol { margin: 8px 0 8px 22px; }
li { margin: 4px 0; }
a { color: #7dd3fc; }
code { background: #1e293b; border-radius: 4px; padding: 1px 6px; font-size: 0.9em; color: #fbbf24; }
pre { background: #1e293b; border: 1px solid #2d3748; border-radius: 8px; padding: 12px 16px; overflow-x: auto; margin: 12px 0; }
pre code { background: none; padding: 0; color: #e2e8f0; }
hr { border: none; border-top: 1px solid #2d3748; margin: 24px 0; }
</style>
</head>
<body>
<header>
<h1>mozdIT — Felhasználói útmutató</h1>
<a href="/">← Vissza a szerkesztőhöz</a>
</header>
<main>
${contentHtml}
</main>
</body>
</html>`;
// Login page — simple logo page shown after logout (and for unauthenticated browser visits).
const LOGIN_PAGE = () => `<!DOCTYPE html>
<html lang="hu">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>mozdIT CMS — Belépés</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; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; }
.environment-banner { background: #f59e0b; color: #111827; padding: 9px 32px; text-align: center; font-size: 13px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; position: fixed; top: 0; left: 0; right: 0; }
.card { background: linear-gradient(160deg,#1a1f2e,#252d40); border: 1px solid #2d3748; border-radius: 16px; padding: 40px 36px; width: 100%; max-width: 380px; box-shadow: 0 20px 50px rgba(0,0,0,.45); }
.logo { text-align: center; margin-bottom: 28px; }
.logo img { height: 56px; }
h1 { font-size: 20px; font-weight: 700; text-align: center; margin-bottom: 4px; }
.subtitle { color: #94a3b8; font-size: 14px; text-align: center; margin-bottom: 26px; }
label { display: block; font-size: 13px; color: #94a3b8; margin: 14px 0 6px; }
input { width: 100%; background: #0f1117; border: 1px solid #2d3748; border-radius: 8px; color: #e2e8f0; padding: 11px 14px; font-size: 15px; }
input:focus { outline: none; border-color: #60a5fa; }
button { width: 100%; margin-top: 24px; background: linear-gradient(135deg,#3b82f6,#8b5cf6); color: #fff; border: none; border-radius: 8px; padding: 12px; font-size: 15px; font-weight: 700; cursor: pointer; }
button:hover { filter: brightness(1.1); }
button:disabled { opacity: .6; cursor: wait; }
.error { color: #f87171; font-size: 14px; text-align: center; margin-top: 14px; min-height: 20px; }
</style>
</head>
<body>
${isStaging() ? '<div class="environment-banner">⚠ STAGING / TESZTKÖRNYEZET</div>' : ''}
<div class="card">
<div class="logo"><img src="/logo.png" alt="mozdIT"></div>
<h1>Content Editor</h1>
<p class="subtitle">Belépés a tartalomszerkesztőbe</p>
<form onsubmit="return login(event)">
<label for="user">Felhasználónév</label>
<input id="user" name="user" autocomplete="username" autofocus required>
<label for="pass">Jelszó</label>
<input id="pass" name="pass" type="password" autocomplete="current-password" required>
<button type="submit" id="btn">Belépés</button>
</form>
<p class="error" id="err"></p>
</div>
<script>
async function login(e) {
e.preventDefault();
const btn = document.getElementById('btn');
const err = document.getElementById('err');
btn.disabled = true; err.textContent = '';
try {
const res = await fetch('/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user: document.getElementById('user').value, pass: document.getElementById('pass').value })
});
if (res.ok) { location.href = '/'; return; }
const json = await res.json().catch(() => ({}));
err.textContent = json.error || 'Sikertelen belépés — ellenőrizd a felhasználónevet és a jelszót.';
} catch (e2) {
err.textContent = 'Hálózati hiba — próbáld újra.';
}
btn.disabled = false;
}
</script>
</body>
</html>`;
module.exports = { HTML, GUIDE_PAGE, LOGIN_PAGE };