feat: partners section on the homepage (logo + URL, CMS upload)
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
- home.json gains a partners block (title/subtitle/items: name, url, logo),
rendered on the homepage under the services section (next/image logos
linking out with rel=noopener)
- CMS: partner logos uploadable from the 🎨 Logó page via POST /partner-logo
(PNG, 1 MiB cap, filename sanitized to a slug, written to public/partners/)
- schema + types extended; guide updated
Closes MITHOME-83
This commit is contained in:
+65
-1
@@ -34,6 +34,27 @@ function saveLogoAtomically(publicDir, targetKey, buffer, backupDir) {
|
||||
return { targetFile, backupName };
|
||||
}
|
||||
|
||||
// WHY: partner logos are a variable set — the filename comes from the editor,
|
||||
// so it must be sanitized to a safe slug (no traversal, no separators).
|
||||
function slugifyName(raw) {
|
||||
return String(raw)
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_-]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.slice(0, 64);
|
||||
}
|
||||
|
||||
function savePartnerLogo(publicDir, filename, buffer) {
|
||||
const slug = slugifyName(filename) || `partner-${Date.now()}`;
|
||||
const dir = path.join(publicDir, 'partners');
|
||||
fs.mkdirSync(dir, { recursive: true, mode: 0o755 });
|
||||
const targetFile = path.join(dir, `${slug}.png`);
|
||||
const tempFile = `${targetFile}.${process.pid}.${Date.now()}.tmp`;
|
||||
fs.writeFileSync(tempFile, buffer, { mode: 0o644 });
|
||||
fs.renameSync(tempFile, targetFile);
|
||||
return `/partners/${slug}.png`;
|
||||
}
|
||||
|
||||
// WHY: route handling lives here so content-editor.js stays under the
|
||||
// 400-line limit. Returns true when the request was handled.
|
||||
function handleLogoRoutes({ req, res, u, publicDir, backupDir, writeAudit, clientAddress, user, logoPage }) {
|
||||
@@ -86,7 +107,50 @@ function handleLogoRoutes({ req, res, u, publicDir, backupDir, writeAudit, clien
|
||||
return true;
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && u.pathname === '/partner-logo') {
|
||||
const filename = u.searchParams.get('name') || '';
|
||||
if (!slugifyName(filename)) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: false, error: 'Adj meg egy érvényes fájlnevet.' }));
|
||||
return true;
|
||||
}
|
||||
const chunks = [];
|
||||
let total = 0;
|
||||
let tooLarge = false;
|
||||
req.on('data', c => {
|
||||
total += c.length;
|
||||
if (total > MAX_LOGO_BYTES) { tooLarge = true; return; }
|
||||
chunks.push(c);
|
||||
});
|
||||
req.on('end', () => {
|
||||
const buffer = Buffer.concat(chunks);
|
||||
if (tooLarge) {
|
||||
writeAudit('partner_logo_upload', { clientAddress, user, result: 'request_too_large' });
|
||||
res.writeHead(413, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: false, error: `A fájl túl nagy (maximum ${MAX_LOGO_BYTES} byte).` }));
|
||||
return;
|
||||
}
|
||||
if (!isPng(buffer)) {
|
||||
writeAudit('partner_logo_upload', { clientAddress, user, result: 'invalid_type' });
|
||||
res.writeHead(415, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: false, error: 'Csak érvényes PNG fájl tölthető fel.' }));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const publicPath = savePartnerLogo(publicDir, filename, buffer);
|
||||
writeAudit('partner_logo_upload', { clientAddress, user, result: 'ok', path: publicPath });
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: true, path: publicPath }));
|
||||
} catch (e) {
|
||||
writeAudit('partner_logo_upload', { clientAddress, user, result: 'error' });
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: false, error: e.message }));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
module.exports = { MAX_LOGO_BYTES, LOGO_TARGETS, isPng, saveLogoAtomically, handleLogoRoutes };
|
||||
module.exports = { MAX_LOGO_BYTES, LOGO_TARGETS, isPng, saveLogoAtomically, savePartnerLogo, slugifyName, handleLogoRoutes };
|
||||
|
||||
Reference in New Issue
Block a user