feat(cms): logo upload with preview, backup and audit
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
- 🎨 Logó page in the CMS bottom bar: replace the website header logo and
the CMS login icon with a PNG upload (magic-byte validation, 1 MiB cap)
- the replaced logo gets a timestamped backup in .content-backups; every
upload is audited (logo_updated)
- /logo.png?variant=header serves the header variant for the preview
- publish stages proto/public too, so logo changes ride the same
commit+deploy pipeline as content
- route handling extracted to scripts/cms-logo.js to stay under the
400-line limit
- integration test: upload+replace+backup, variant preview, 415/413/400,
CSRF, auth
Closes MITHOME-65
This commit is contained in:
+23
-50
@@ -16,7 +16,8 @@ const crypto = require('crypto');
|
||||
const { validateContent } = require('./proto/src/content/schema');
|
||||
const { renderMarkdown } = require('./scripts/markdown-render');
|
||||
const { buildPublishCommand, interpretPublishResult } = require('./scripts/cms-publish');
|
||||
const { safeBackupName, listVersions, readBackupContent, buildVersionDiff } = require('./scripts/cms-versions');
|
||||
const { handleVersionRoutes, listVersions } = require('./scripts/cms-versions');
|
||||
const { LOGO_TARGETS, handleLogoRoutes } = require('./scripts/cms-logo');
|
||||
|
||||
const PORT = Number(process.env.CONTENT_EDITOR_PORT) || 4001;
|
||||
// WHY: overridable so the publish integration test can run against a throwaway
|
||||
@@ -51,6 +52,7 @@ const FILE_LABELS = {
|
||||
};
|
||||
|
||||
const { HTML, GUIDE_PAGE, LOGIN_PAGE, VERSIONS_PAGE } = require('./scripts/cms-pages');
|
||||
const { LOGO_PAGE } = require('./scripts/cms-logo-page');
|
||||
const { validateLogin, createSessionCookie, clearSessionCookie, hasValidSession, deleteSession } = require('./scripts/cms-session');
|
||||
|
||||
// Browser script is kept in its own file and inlined into the HTML template at render time.
|
||||
@@ -104,7 +106,9 @@ const server = http.createServer(async (req, res) => {
|
||||
// Public: logo asset for the login page.
|
||||
if (req.method === 'GET' && u.pathname === '/logo.png') {
|
||||
try {
|
||||
const logo = fs.readFileSync(path.join(__dirname, 'proto', 'public', 'mozdit_logo.png'));
|
||||
// ?variant=header serves the website header logo (branding page preview).
|
||||
const file = u.searchParams.get('variant') === 'header' ? LOGO_TARGETS.header : LOGO_TARGETS.icon;
|
||||
const logo = fs.readFileSync(path.join(__dirname, 'proto', 'public', file));
|
||||
res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=3600' });
|
||||
res.end(logo);
|
||||
} catch {
|
||||
@@ -304,55 +308,24 @@ const server = http.createServer(async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// GET /versions — backup list + optional diff view (browser page, session-auth).
|
||||
if (req.method === 'GET' && u.pathname === '/versions') {
|
||||
const versions = listVersions(BACKUP_DIR, activeFile);
|
||||
let diff = null;
|
||||
const showRaw = u.searchParams.get('show');
|
||||
if (showRaw) {
|
||||
const safe = safeBackupName(activeFile, showRaw);
|
||||
if (safe) {
|
||||
try {
|
||||
diff = buildVersionDiff(BACKUP_DIR, FILES[activeFile], activeFile, safe);
|
||||
} catch { /* unreadable backup: render list only */ }
|
||||
}
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
||||
res.end(VERSIONS_PAGE(activeFile, FILE_LABELS[activeFile] || activeFile, versions, diff, CSRF_TOKEN));
|
||||
return;
|
||||
}
|
||||
// GET /versions + POST /restore — handled in scripts/cms-versions.js.
|
||||
if (handleVersionRoutes({
|
||||
req, res, u, activeFile,
|
||||
backupDir: BACKUP_DIR,
|
||||
currentFile: FILES[activeFile],
|
||||
validate: validateContent,
|
||||
writeAudit, clientAddress, user: CMS_USER,
|
||||
versionsPage: (fileKey, diff) => VERSIONS_PAGE(fileKey, FILE_LABELS[fileKey] || fileKey, listVersions(BACKUP_DIR, fileKey), diff, CSRF_TOKEN),
|
||||
})) return;
|
||||
|
||||
// POST /restore — restore a backup; the current state is backed up first,
|
||||
// so the restore itself is reversible. Schema validation guards against
|
||||
// restoring a structurally broken backup.
|
||||
if (req.method === 'POST' && u.pathname === '/restore') {
|
||||
const backup = safeBackupName(activeFile, u.searchParams.get('backup') || '');
|
||||
if (!backup) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: false, error: 'Érvénytelen mentésnév.' }));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const content = readBackupContent(BACKUP_DIR, backup);
|
||||
const data = JSON.parse(content);
|
||||
const validation = validateContent(activeFile, data);
|
||||
if (!validation.ok) {
|
||||
writeAudit('version_restored', { clientAddress, user: CMS_USER, file: activeFile, backup, result: 'validation_failed' });
|
||||
res.writeHead(422, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: false, error: 'A mentés nem felel meg a sémának: ' + validation.errors.join('; ') }));
|
||||
return;
|
||||
}
|
||||
backupAndWriteAtomically(FILES[activeFile], data, BACKUP_DIR);
|
||||
writeAudit('version_restored', { clientAddress, user: CMS_USER, file: activeFile, backup, result: 'ok' });
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: true }));
|
||||
} catch (e) {
|
||||
writeAudit('version_restored', { clientAddress, user: CMS_USER, file: activeFile, backup, result: 'error' });
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: false, error: e.message }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// GET /branding + POST /logo — handled in scripts/cms-logo.js.
|
||||
if (handleLogoRoutes({
|
||||
req, res, u,
|
||||
publicDir: path.join(__dirname, 'proto', 'public'),
|
||||
backupDir: BACKUP_DIR,
|
||||
writeAudit, clientAddress, user: CMS_USER,
|
||||
logoPage: () => LOGO_PAGE(CSRF_TOKEN),
|
||||
})) return;
|
||||
|
||||
// GET / — editor UI
|
||||
let message = null;
|
||||
|
||||
Reference in New Issue
Block a user