dynamic köpfe

This commit is contained in:
2026-07-17 19:36:12 +02:00
parent ba36316fcb
commit 298f12305e
6 changed files with 262 additions and 2 deletions

View File

@@ -1626,5 +1626,45 @@ export default (router, tpl) => {
});
});
// Koepfe Manager
router.get(/^\/admin\/koepfe\/?$/, lib.auth, async (req, res) => {
res.reply({
body: tpl.render('admin/koepfe', { session: req.session, tmp: null }, req)
});
});
router.get(/^\/api\/v2\/admin\/koepfe\/?$/, lib.auth, async (req, res) => {
try {
const fs = (await import('fs')).promises;
const path = await import('path');
const kDir = cfg.paths.koepfe;
const files = await fs.readdir(kDir).catch(() => []);
const koepfeFiles = files.filter(f => /\.(png|jpe?g|gif|webp|avif)$/i.test(f)).map(f => ({
filename: f,
url: '/s/koepfe/' + f
}));
if (res.json) return res.json({ koepfe: koepfeFiles });
return res.writeHead(200, {'Content-Type': 'application/json'}).end(JSON.stringify({ koepfe: koepfeFiles }));
} catch (e) {
if (res.json) return res.json({ success: false, msg: e.message });
return res.writeHead(500, {'Content-Type': 'application/json'}).end(JSON.stringify({ success: false, msg: e.message }));
}
});
router.post(/^\/api\/v2\/admin\/koepfe\/delete\/?$/, lib.auth, async (req, res) => {
try {
const { filename } = req.post || req.body || {};
if (!filename || filename.includes('..') || filename.includes('/')) throw new Error('Invalid filename');
const fs = (await import('fs')).promises;
const path = await import('path');
await fs.unlink(path.join(cfg.paths.koepfe, filename));
if (res.json) return res.json({ success: true });
return res.writeHead(200, {'Content-Type': 'application/json'}).end(JSON.stringify({ success: true }));
} catch (e) {
if (res.json) return res.json({ success: false, msg: e.message });
return res.writeHead(500, {'Content-Type': 'application/json'}).end(JSON.stringify({ success: false, msg: e.message }));
}
});
return router;
}

View File

@@ -901,6 +901,15 @@ process.on('uncaughtException', err => {
}
});
// Bypass middleware for koepfe uploads
app.use(async (req, res) => {
if (req.method === 'POST' && req.url.pathname === '/api/v2/admin/koepfe/upload') {
const { handleKoepfeUpload } = await import('./koepfe_handler.mjs');
await handleKoepfeUpload(req, res);
req.url.pathname = '/handled_koepfe_upload_bypass';
}
});
// Bypass middleware for hall image uploads (multipart — needs raw body)
app.use(async (req, res) => {
if (cfg.websrv.halls_enabled === false) return;
@@ -1268,7 +1277,16 @@ process.on('uncaughtException', err => {
enable_item_title: cfg.websrv.enable_item_title !== false,
enable_global_chat: !!cfg.websrv.enable_global_chat,
embed_youtube_in_comments: cfg.websrv.embed_youtube_in_comments !== false,
koepfe_json: JSON.stringify(cfg.websrv.koepfe || []),
get koepfe_json() {
try {
const kDir = cfg.paths.koepfe;
if (!fs.existsSync(kDir)) return JSON.stringify(cfg.websrv.koepfe || []);
const files = fs.readdirSync(kDir).filter(f => /\.(png|jpe?g|gif|webp|avif)$/i.test(f)).map(f => '/s/koepfe/' + f);
return JSON.stringify(files.length ? files : (cfg.websrv.koepfe || []));
} catch (e) {
return JSON.stringify(cfg.websrv.koepfe || []);
}
},
custom_brand_images_json: JSON.stringify(cfg.websrv.custom_brand_image || []),
allowed_comment_images: cfg.websrv.allowed_comment_images || [],
allowed_comment_images_json: JSON.stringify(cfg.websrv.allowed_comment_images || []),

92
src/koepfe_handler.mjs Normal file
View File

@@ -0,0 +1,92 @@
import fs from 'fs/promises';
import path from 'path';
import crypto from 'crypto';
import lib from './inc/lib.mjs';
import cfg from './inc/config.mjs';
function parseMultipart(req) {
return new Promise((resolve, reject) => {
let body = [];
req.on('data', chunk => body.push(chunk));
req.on('end', () => {
const buffer = Buffer.concat(body);
const boundaryMatch = req.headers['content-type']?.match(/boundary=(.+)$/i);
if (!boundaryMatch) return reject(new Error('No boundary'));
const boundary = '--' + boundaryMatch[1];
let start = buffer.indexOf(boundary) + boundary.length + 2;
let fileData = null;
let filename = null;
let csrfToken = null;
while (start < buffer.length) {
const headerEnd = buffer.indexOf('\r\n\r\n', start);
if (headerEnd === -1) break;
const headerStr = buffer.slice(start, headerEnd).toString();
const nextBoundary = buffer.indexOf(boundary, headerEnd);
if (nextBoundary === -1) break;
const chunkData = buffer.slice(headerEnd + 4, nextBoundary - 2);
if (headerStr.includes('name="file"')) {
const fnMatch = headerStr.match(/filename="(.+?)"/);
if (fnMatch) filename = fnMatch[1];
fileData = chunkData;
} else if (headerStr.includes('name="csrf_token"')) {
csrfToken = chunkData.toString();
}
start = nextBoundary + boundary.length + 2;
}
resolve({ fileData, filename, csrfToken });
});
req.on('error', reject);
});
}
import db from './inc/sql.mjs';
export async function handleKoepfeUpload(req, res) {
// Manual Session Lookup since this is a bypass middleware
let user = [];
if (req.cookies && req.cookies.session) {
user = await db`
select "user".id, "user".login, "user".user, "user".admin, "user_sessions".csrf_token
from "user_sessions"
left join "user" on "user".id = "user_sessions".user_id
where "user_sessions".session = ${lib.sha256(req.cookies.session)}
limit 1
`;
}
if (user.length === 0 || !user[0].admin) {
return res.reply ? res.reply({ code: 403, body: JSON.stringify({ success: false, msg: 'Forbidden' }) }) : res.writeHead(403, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg: 'Forbidden' }));
}
req.session = user[0];
try {
const { fileData, filename, csrfToken } = await parseMultipart(req);
if (!req.session.csrf_token || csrfToken !== req.session.csrf_token) {
return res.writeHead(403, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg: 'Invalid CSRF' }));
}
if (!fileData || fileData.length === 0) {
throw new Error('No file data');
}
const ext = path.extname(filename || '.png').toLowerCase();
if (!['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif'].includes(ext)) {
throw new Error('Unsupported format');
}
const newName = crypto.randomBytes(8).toString('hex') + ext;
const targetPath = path.join(cfg.paths.koepfe, newName);
await fs.writeFile(targetPath, fileData);
return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: true, file: '/s/koepfe/' + newName }));
} catch (e) {
console.error(e);
return res.writeHead(500, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg: e.message }));
}
}