dynamic köpfe
This commit is contained in:
@@ -13482,7 +13482,6 @@ span.gchat-post-card--loading {
|
|||||||
z-index: -1;
|
z-index: -1;
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-drag: none;
|
-webkit-user-drag: none;
|
||||||
will-change: opacity;
|
will-change: opacity;
|
||||||
|
|||||||
@@ -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;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
// Bypass middleware for hall image uploads (multipart — needs raw body)
|
||||||
app.use(async (req, res) => {
|
app.use(async (req, res) => {
|
||||||
if (cfg.websrv.halls_enabled === false) return;
|
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_item_title: cfg.websrv.enable_item_title !== false,
|
||||||
enable_global_chat: !!cfg.websrv.enable_global_chat,
|
enable_global_chat: !!cfg.websrv.enable_global_chat,
|
||||||
embed_youtube_in_comments: cfg.websrv.embed_youtube_in_comments !== false,
|
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 || []),
|
custom_brand_images_json: JSON.stringify(cfg.websrv.custom_brand_image || []),
|
||||||
allowed_comment_images: cfg.websrv.allowed_comment_images || [],
|
allowed_comment_images: cfg.websrv.allowed_comment_images || [],
|
||||||
allowed_comment_images_json: JSON.stringify(cfg.websrv.allowed_comment_images || []),
|
allowed_comment_images_json: JSON.stringify(cfg.websrv.allowed_comment_images || []),
|
||||||
|
|||||||
92
src/koepfe_handler.mjs
Normal file
92
src/koepfe_handler.mjs
Normal 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 }));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
<li><a href="/admin/users">User Manager</a></li>
|
<li><a href="/admin/users">User Manager</a></li>
|
||||||
<li><a href="/admin/emojis">Emoji Manager</a></li>
|
<li><a href="/admin/emojis">Emoji Manager</a></li>
|
||||||
<li><a href="/admin/memes">Meme Manager</a></li>
|
<li><a href="/admin/memes">Meme Manager</a></li>
|
||||||
|
<li><a href="/admin/koepfe">Köpfe Manager</a></li>
|
||||||
<li><a href="/admin/halls">Hall Manager</a></li>
|
<li><a href="/admin/halls">Hall Manager</a></li>
|
||||||
<li><a href="/admin/motd">MOTD Manager</a></li>
|
<li><a href="/admin/motd">MOTD Manager</a></li>
|
||||||
<li><a href="/admin/wordfilter">Wordfilter Manager</a></li>
|
<li><a href="/admin/wordfilter">Wordfilter Manager</a></li>
|
||||||
|
|||||||
110
views/admin/koepfe.html
Normal file
110
views/admin/koepfe.html
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
@include(snippets/header)
|
||||||
|
<div class="pagewrapper">
|
||||||
|
<div id="main" class="admin-container">
|
||||||
|
<div class="container">
|
||||||
|
<h2>Köpfe Manager</h2>
|
||||||
|
|
||||||
|
<div class="admin-form-container" style="margin-bottom: 20px; text-align: left; background: var(--dropdown-bg); padding: 15px; border: 1px solid var(--nav-border-color); border-radius: 8px;">
|
||||||
|
<h4 style="margin: 0 0 12px;">Upload Neuer Kopf</h4>
|
||||||
|
<div style="display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end;">
|
||||||
|
<div>
|
||||||
|
<label style="display: block; font-size: 0.8em; margin-bottom: 5px; opacity: 0.7;">Bild (PNG, JPG, GIF, WebP, AVIF)</label>
|
||||||
|
<input type="file" id="koepfe-file" style="background: var(--bg); border: 1px solid var(--black); padding: 4px; color: var(--white);">
|
||||||
|
</div>
|
||||||
|
<button id="add-koepfe" class="btn-upload" style="width: auto; padding: 7px 20px; border: 1px solid var(--nav-border-color); background: var(--bg); color: var(--white); cursor: pointer;">Upload</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="koepfe-list" class="emoji-grid"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
var csrf = '{{ csrf_token }}';
|
||||||
|
|
||||||
|
function esc(s) {
|
||||||
|
return (s || '').toString()
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadAll() {
|
||||||
|
fetch('/api/v2/admin/koepfe', { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
renderKoepfe(data.koepfe || []);
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
console.error('[KOEPFE] Load Error:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderKoepfe(koepfe) {
|
||||||
|
var grid = document.getElementById('koepfe-list');
|
||||||
|
if (!grid) return;
|
||||||
|
if (koepfe.length === 0) {
|
||||||
|
grid.innerHTML = '<p style="opacity:0.5;font-size:0.85em;">Keine Köpfe vorhanden.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
grid.innerHTML = koepfe.map(function(k) {
|
||||||
|
return '<div class="emoji-card">' +
|
||||||
|
'<button class="emoji-delete" onclick="deleteKoepfe("' + esc(k.filename) + '")" title="Delete">x</button>' +
|
||||||
|
'<img class="emoji-preview" src="' + esc(k.url) + '" alt="">' +
|
||||||
|
'<span class="emoji-label" style="font-size: 0.6em;">' + esc(k.filename) + '</span>' +
|
||||||
|
'</div>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function addKoepfe(e) {
|
||||||
|
if (e) e.preventDefault();
|
||||||
|
var fileInput = document.getElementById('koepfe-file');
|
||||||
|
if (!fileInput.files[0]) return alert('Bitte eine Datei auswählen.');
|
||||||
|
|
||||||
|
var btn = document.getElementById('add-koepfe');
|
||||||
|
var oldText = btn.textContent;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Lädt...';
|
||||||
|
|
||||||
|
var formData = new FormData();
|
||||||
|
formData.append('file', fileInput.files[0]);
|
||||||
|
formData.append('csrf_token', csrf);
|
||||||
|
|
||||||
|
var headers = { 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-Token': csrf };
|
||||||
|
|
||||||
|
fetch('/api/v2/admin/koepfe/upload', { method: 'POST', headers: headers, body: formData })
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (data.success) {
|
||||||
|
document.getElementById('koepfe-file').value = '';
|
||||||
|
loadAll();
|
||||||
|
} else {
|
||||||
|
alert('Fehler: ' + (data.message || data.msg || 'Unbekannter Fehler'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(err) { alert('Error: ' + err.message); })
|
||||||
|
.finally(function() { btn.disabled = false; btn.textContent = oldText; });
|
||||||
|
}
|
||||||
|
|
||||||
|
window.deleteKoepfe = function(filename) {
|
||||||
|
if (!confirm('Diesen Kopf wirklich löschen?')) return;
|
||||||
|
fetch('/api/v2/admin/koepfe/delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'X-CSRF-Token': csrf, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ filename: filename })
|
||||||
|
})
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(data) { if (data.success) loadAll(); else alert('Löschen fehlgeschlagen: ' + (data.msg || '')); })
|
||||||
|
.catch(function(err) { console.error(err); });
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('add-koepfe').addEventListener('click', addKoepfe);
|
||||||
|
|
||||||
|
loadAll();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@include(snippets/footer)
|
||||||
Reference in New Issue
Block a user