#!/usr/bin/env node /** * mozdIT Content Editor Server v2 * Szerkesztő felület a JSON tartalom fájlokhoz * Támogatja: szöveg szerkesztés, tömbelem hozzáadás/törlés * Futtatás: node content-editor.js * Megnyitás: http://localhost:4001 */ const http = require('http'); const fs = require('fs'); const path = require('path'); const { exec } = require('child_process'); const crypto = require('crypto'); const { validateContent } = require('./proto/src/content/schema'); const { renderMarkdown } = require('./scripts/markdown-render'); const PORT = Number(process.env.CONTENT_EDITOR_PORT) || 4001; const CONTENT_DIR = path.join(__dirname, 'proto', 'src', 'content'); const BACKUP_DIR = path.join(__dirname, '.content-backups'); const MAX_REQUEST_BODY_BYTES = 256 * 1024; const AUDIT_LOG_FILE = process.env.CONTENT_EDITOR_AUDIT_FILE || path.join(__dirname, '.content-editor-audit.jsonl'); const GUIDE_FILE = process.env.CONTENT_EDITOR_GUIDE_FILE || path.join(__dirname, 'docs', 'felhasznaloi-utmutato.md'); const RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000; const AUTH_MAX_ATTEMPTS = 5; const PUBLISH_MAX_ATTEMPTS = 3; const FILES = { common: path.join(CONTENT_DIR, 'common.json'), home: path.join(CONTENT_DIR, 'pages', 'home.json'), about: path.join(CONTENT_DIR, 'pages', 'about.json'), services: path.join(CONTENT_DIR, 'pages', 'services.json'), contact: path.join(CONTENT_DIR, 'pages', 'contact.json'), adatvedelem: path.join(CONTENT_DIR, 'pages', 'adatvedelem.json'), hasznalatiFeltetelek: path.join(CONTENT_DIR, 'pages', 'hasznalati-feltetelek.json'), }; const FILE_LABELS = { common: '⚙️ Közös szövegek', home: '🏠 Kezdőlap', about: '👥 Rólunk', services: '🛠️ Szolgáltatások', contact: '📬 Kapcsolat', adatvedelem: '🔒 Adatvédelem', hasznalatiFeltetelek: '⚖️ ÁSZF', }; const HTML = (activeFile, jsonData, message, csrfToken) => `
📝 Szerkeszd a mezőket. Tömbökből elemet törölhetsz (❌) vagy hozzáadhatsz (➕). Mentés gomb menti a fájlt.
Az útmutató jelenleg nem elérhető. Kérlek, szólj a fejlesztőnek.
'; } res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(GUIDE_PAGE(contentHtml)); return; } // POST /save — JSON body if (req.method === 'POST' && u.pathname === '/save') { let body = ''; let bodyTooLarge = false; let bodySize = 0; req.on('data', c => { bodySize += c.length; if (bodySize > MAX_REQUEST_BODY_BYTES) { bodyTooLarge = true; return; } body += c; }); req.on('end', () => { try { if (bodyTooLarge) { writeAudit('content_saved', { clientAddress, user: CMS_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 ${MAX_REQUEST_BODY_BYTES} byte)` })); return; } const data = JSON.parse(body); const validation = validateContent(activeFile, data); if (!validation.ok) { writeAudit('content_saved', { clientAddress, user: CMS_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 = backupAndWriteAtomically(FILES[activeFile], data); writeAudit('content_saved', { clientAddress, user: CMS_USER, file: activeFile, result: 'ok' }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: true, backup: path.relative(__dirname, backupFile) })); } catch (e) { writeAudit('content_saved', { clientAddress, user: CMS_USER, file: activeFile, result: 'error' }); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: e.message })); } }); return; } // POST /publish — Git Commit & Push if (req.method === 'POST' && u.pathname === '/publish') { if (exceedsRateLimit(`publish:${clientAddress}`, PUBLISH_MAX_ATTEMPTS)) { writeAudit('publish_rate_limited', { clientAddress, user: CMS_USER }); res.writeHead(429, { 'Content-Type': 'application/json', 'Retry-After': String(RATE_LIMIT_WINDOW_MS / 1000) }); res.end(JSON.stringify({ ok: false, error: 'Túl sok publikálási kísérlet' })); return; } exec('git add . && git commit -m "content: frissítve a CMS-ből" && git push', { cwd: CONTENT_DIR }, (error, stdout, stderr) => { res.writeHead(200, { 'Content-Type': 'application/json' }); if (error) { // If there's nothing to commit, it's fine if (stdout.includes('nothing to commit') || stdout.includes('working tree clean')) { writeAudit('publish_finished', { clientAddress, user: CMS_USER, result: 'no_changes' }); res.end(JSON.stringify({ ok: true, output: 'No changes to commit' })); } else { writeAudit('publish_finished', { clientAddress, user: CMS_USER, result: 'error' }); res.end(JSON.stringify({ ok: false, error: stderr || stdout || error.message })); } } else { // Deploy only the explicitly configured environment; never default to production. exec(`cd ../../../ && ./deploy.sh ${CMS_DEPLOY_ENV} > deploy.log 2>&1 &`); writeAudit('publish_finished', { clientAddress, user: CMS_USER, result: 'ok' }); res.end(JSON.stringify({ ok: true, output: stdout })); } }); return; } // GET / — editor UI let message = null; let jsonData = '{}'; try { jsonData = fs.readFileSync(FILES[activeFile], 'utf8').trim(); } catch (e) { message = { type: 'err', text: 'Fájl olvasási hiba: ' + e.message }; } res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(HTML(activeFile, jsonData, message, CSRF_TOKEN)); }); if (require.main === module) { if (!securityConfigIsValid()) { throw new Error('CMS_USER, CMS_PASS és érvényes CMS_DEPLOY_ENV nélkül a Content Editor nem indítható el.'); } server.listen(PORT, '127.0.0.1', () => { console.log(`\n✅ mozdIT Content Editor fut: http://localhost:${PORT}\n`); console.log(' Szerkeszthető fájlok:'); Object.entries(FILE_LABELS).forEach(([k, l]) => { const rel = k === 'common' ? 'common.json' : `pages/${k}.json`; console.log(` • ${l}: proto/src/content/${rel}`); }); console.log('\n Ctrl+C a leállításhoz\n'); }); } module.exports = { backupAndWriteAtomically, validateContent, hasValidCredentials, hasValidCsrfToken, getClientAddress, securityConfigIsValid, csrfToken: CSRF_TOKEN, };