diff --git a/src/index.mjs b/src/index.mjs index a837541..28f718d 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -11,7 +11,7 @@ import flummpress from "flummpress"; import { handleUpload } from "./upload_handler.mjs"; import { handleAvatarUpload, handleAvatarDelete } from "./avatar_handler.mjs"; import { handleRethumbUpload } from "./rethumb_handler.mjs"; -import { handleMemeUpload } from "./meme_upload_handler.mjs"; +import { handleMemeUpload, handleMemeEdit } from "./meme_upload_handler.mjs"; import { handleEmojiUpload } from "./emoji_upload_handler.mjs"; import { handleHallImageUpload, handleHallImageDelete, handleHallDelete, handleHallUpdate, handleHallCreate } from "./hall_image_handler.mjs"; import { handleMetaExtract } from "./meta_extract_handler.mjs"; @@ -775,6 +775,16 @@ process.on('uncaughtException', err => { } }); + // Bypass middleware for meme template edits + app.use(async (req, res) => { + const editMatch = req.url.pathname.match(/^\/api\/v2\/admin\/memes\/(\d+)\/edit$/); + if (req.method === 'POST' && editMatch) { + req.params = { id: editMatch[1] }; + await handleMemeEdit(req, res); + req.url.pathname = '/handled_meme_edit_bypass'; + } + }); + // Bypass middleware for emoji uploads app.use(async (req, res) => { if (req.method === 'POST' && req.url.pathname === '/api/v2/admin/emojis') { diff --git a/src/meme_upload_handler.mjs b/src/meme_upload_handler.mjs index a8238a5..490aa9b 100644 --- a/src/meme_upload_handler.mjs +++ b/src/meme_upload_handler.mjs @@ -107,3 +107,117 @@ export const handleMemeUpload = async (req, res) => { return sendJson(res, { success: false, message: err.message }, 500); } }; + +export const handleMemeEdit = async (req, res) => { + console.log('[BOOT] [MEME HANDLER] Edit Started'); + + // Manual Session Lookup + let user = []; + if (req.cookies && req.cookies.session) { + user = await db` + select "user".id, "user".login, "user".user, "user".admin, "user".is_moderator, "user_sessions".id as sess_id, "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) { + console.log('[MEME HANDLER] Unauthorized'); + return sendJson(res, { success: false, message: 'Unauthorized' }, 403); + } + + req.session = user[0]; + + // CSRF validation + if (req.session.csrf_token) { + const csrfToken = req.headers['x-csrf-token']; + if (!csrfToken || csrfToken !== req.session.csrf_token) { + console.warn(`[CSRF] Blocked meme edit for user ${req.session.user}. Invalid token.`); + return sendJson(res, { success: false, message: 'Invalid CSRF token' }, 403); + } + } + + const id = req.params.id; + + try { + // Fetch existing template first + const currentMeme = await db`SELECT * FROM meme_templates WHERE id = ${id}`; + if (currentMeme.length === 0) { + return sendJson(res, { success: false, message: 'Meme template not found' }, 404); + } + + const contentType = req.headers['content-type'] || ''; + const boundaryMatch = contentType.match(/boundary=([^;]+)/); + + if (!boundaryMatch) { + return sendJson(res, { success: false, message: 'Invalid content type' }, 400); + } + + let boundary = boundaryMatch[1].trim(); + if (boundary.startsWith('"') && boundary.endsWith('"')) { + boundary = boundary.substring(1, boundary.length - 1); + } + + const bodyBuffer = await collectBody(req); + const parts = parseMultipart(bodyBuffer, boundary); + + const template_id = (parts.template_id || '').trim().toLowerCase(); + const name = (parts.name || '').trim(); + const category = (parts.category || '').trim() || 'General'; + let url = (parts.url || '').trim(); + + if (!template_id || !name) { + return sendJson(res, { success: false, message: 'Template ID and Name are required' }, 400); + } + + if (!/^[a-z0-9-]+$/.test(template_id)) { + return sendJson(res, { success: false, message: 'Invalid ID. Use lowercase a-z, 0-9, - only.' }, 400); + } + + // Ensure template_id is unique + const existing = await db`SELECT id FROM meme_templates WHERE template_id = ${template_id} AND id != ${id}`; + if (existing.length > 0) { + return sendJson(res, { success: false, message: 'Template ID is already in use by another template' }, 400); + } + + const file = parts.file; + if (file && file.data && file.data.length > 0) { + const extMatch = file.filename.match(/\.([a-z0-9]+)$/i); + const ext = extMatch ? extMatch[1].toLowerCase() : 'jpg'; + const filename = `${template_id}_${Math.random().toString(36).substring(7)}.${ext}`; + + const filePath = path.join(cfg.paths.memes, filename); + console.error(`[BOOT] [MEME HANDLER] Writing file to: ${filePath} (Size: ${file.data.length})`); + await fs.writeFile(filePath, file.data); + + const exists = (await fs.stat(filePath)).size > 0; + console.error(`[BOOT] [MEME HANDLER] Write verify: ${exists ? 'SUCCESS' : 'FAILURE'}`); + + if (exists) { + url = `/memes/${filename}`; + } else { + throw new Error("File was written but verify failed (size 0 or not found)"); + } + } + + // If no file uploaded and no URL input provided, keep the existing one + if (!url) { + url = currentMeme[0].url; + } + + await db` + UPDATE meme_templates + SET template_id = ${template_id}, name = ${name}, category = ${category}, url = ${url} + WHERE id = ${id} + `; + + return sendJson(res, { success: true }); + + } catch (err) { + console.error('[MEME HANDLER ERROR]', err); + return sendJson(res, { success: false, message: err.message }, 500); + } +}; + diff --git a/views/admin/memes.html b/views/admin/memes.html index 8b44694..1f32877 100644 --- a/views/admin/memes.html +++ b/views/admin/memes.html @@ -34,6 +34,49 @@ + + +