diff --git a/config_example.json b/config_example.json index f303e90..fd70df9 100644 --- a/config_example.json +++ b/config_example.json @@ -52,6 +52,7 @@ "private_messages": true, "halls_enabled": true, "userhalls_enabled": true, + "enable_userhall_image_upload": true, "abyss_enabled": true, "meme_creator": true, diff --git a/src/hall_image_handler.mjs b/src/hall_image_handler.mjs index 0fdcd27..377c63f 100644 --- a/src/hall_image_handler.mjs +++ b/src/hall_image_handler.mjs @@ -50,6 +50,12 @@ export const handleHallImageUpload = async (req, res) => { return sendJson(res, { success: false, msg: 'Unauthorized' }, 403); } + // CSRF check + const token = req.headers['x-csrf-token'] || req.url?.qs?.csrf_token; + if (!token || token !== session.csrf_token) { + return sendJson(res, { success: false, msg: 'Invalid CSRF token' }, 403); + } + const hallSlug = req.params && req.params.slug; if (!hallSlug) return sendJson(res, { success: false, msg: 'Missing hall slug' }, 400); @@ -118,9 +124,9 @@ export const handleHallImageUpload = async (req, res) => { // DELETE /api/v2/admin/halls/:slug/image — remove custom image export const handleHallImageDelete = async (req, res) => { const session = await lookupSession(req); - if (!session || (!session.admin && !session.is_moderator)) { - return sendJson(res, { success: false, msg: 'Unauthorized' }, 403); - } + if (!session || (!session.admin && !session.is_moderator)) return sendJson(res, { success: false, msg: 'Unauthorized' }, 403); + const token = req.headers['x-csrf-token'] || req.url?.qs?.csrf_token; + if (!token || token !== session.csrf_token) return sendJson(res, { success: false, msg: 'Invalid CSRF token' }, 403); const hallSlug = req.params && req.params.slug; if (!hallSlug) return sendJson(res, { success: false, msg: 'Missing hall slug' }, 400); @@ -156,9 +162,9 @@ export const handleHallImageDelete = async (req, res) => { // DELETE /api/v2/admin/halls/:slug — delete a hall entirely export const handleHallDelete = async (req, res) => { const session = await lookupSession(req); - if (!session || (!session.admin && !session.is_moderator)) { - return sendJson(res, { success: false, msg: 'Unauthorized' }, 403); - } + if (!session || (!session.admin && !session.is_moderator)) return sendJson(res, { success: false, msg: 'Unauthorized' }, 403); + const token = req.headers['x-csrf-token'] || req.url?.qs?.csrf_token; + if (!token || token !== session.csrf_token) return sendJson(res, { success: false, msg: 'Invalid CSRF token' }, 403); const hallSlug = req.params && req.params.slug; if (!hallSlug) return sendJson(res, { success: false, msg: 'Missing hall slug' }, 400); @@ -176,9 +182,9 @@ export const handleHallDelete = async (req, res) => { // PATCH /api/v2/admin/halls/:slug — update name/description/slug export const handleHallUpdate = async (req, res) => { const session = await lookupSession(req); - if (!session || (!session.admin && !session.is_moderator)) { - return sendJson(res, { success: false, msg: 'Unauthorized' }, 403); - } + if (!session || (!session.admin && !session.is_moderator)) return sendJson(res, { success: false, msg: 'Unauthorized' }, 403); + const token = req.headers['x-csrf-token'] || req.url?.qs?.csrf_token; + if (!token || token !== session.csrf_token) return sendJson(res, { success: false, msg: 'Invalid CSRF token' }, 403); const hallSlug = req.params && req.params.slug; if (!hallSlug) return sendJson(res, { success: false, msg: 'Missing slug' }, 400); @@ -263,9 +269,10 @@ export const handleHallUpdate = async (req, res) => { // POST /api/v2/admin/halls — create a new hall export const handleHallCreate = async (req, res) => { - const session = await lookupSession(req); - if (!session || (!session.admin && !session.is_moderator)) { - return sendJson(res, { success: false, msg: 'Unauthorized' }, 403); + // CSRF check + const token = req.headers['x-csrf-token'] || req.url?.qs?.csrf_token; + if (!token || token !== session.csrf_token) { + return sendJson(res, { success: false, msg: 'Invalid CSRF token' }, 403); } let body = {}; diff --git a/src/index.mjs b/src/index.mjs index 75f995c..0136667 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -443,7 +443,7 @@ process.on('uncaughtException', err => { // Hall manager routes are handled by bypass middleware with their own session auth if (cfg.websrv.halls_enabled !== false && req.url.pathname.match(/^\/api\/v2\/admin\/halls(\/|$)/)) return; // User hall image upload is handled by bypass middleware below - if (cfg.websrv.userhalls_enabled !== false && req.url.pathname.match(/^\/api\/v2\/me\/halls\/[^/]+\/image$/)) return; + if (cfg.websrv.userhalls_enabled !== false && cfg.websrv.enable_userhall_image_upload !== false && req.url.pathname.match(/^\/api\/v2\/me\/halls\/[^/]+\/image$/)) return; if (!validateCsrf(req, res)) return; }); @@ -544,7 +544,7 @@ process.on('uncaughtException', err => { // Bypass middleware for user hall image uploads (multipart — raw body needed) app.use(async (req, res) => { - if (cfg.websrv.userhalls_enabled === false) return; + if (cfg.websrv.userhalls_enabled === false || cfg.websrv.enable_userhall_image_upload === false) return; const userHallImgMatch = req.url.pathname.match(/^\/api\/v2\/me\/halls\/([^/]+)\/image$/); if (userHallImgMatch && req.method === 'POST') { console.error('[BOOT] [USER_HALL BYPASS] Image upload:', req.url.pathname); @@ -733,6 +733,7 @@ process.on('uncaughtException', err => { get halls() { return getHalls(); }, halls_enabled: cfg.websrv.halls_enabled !== false, userhalls_enabled: cfg.websrv.userhalls_enabled !== false, + enable_userhall_image_upload: cfg.websrv.enable_userhall_image_upload !== false, abyss_enabled: cfg.websrv.abyss_enabled !== false, smtp_enabled: !!(cfg.smtp && cfg.smtp.enabled && cfg.smtp.mail_reset_password), show_background_cfg: cfg.websrv.background !== false, diff --git a/src/user_hall_image_handler.mjs b/src/user_hall_image_handler.mjs index f8288bf..0e9b807 100644 --- a/src/user_hall_image_handler.mjs +++ b/src/user_hall_image_handler.mjs @@ -48,7 +48,7 @@ export const handleUserHallImageUpload = async (req, res, slug) => { if (!session) return sendJson(res, { success: false, msg: 'Unauthorized' }, 403); // CSRF check - const token = req.headers['x-csrf-token']; + const token = req.headers['x-csrf-token'] || req.url?.qs?.csrf_token; if (!token || token !== session.csrf_token) { return sendJson(res, { success: false, msg: 'Invalid CSRF token' }, 403); } diff --git a/views/snippets/footer.html b/views/snippets/footer.html index 13d0623..4cd7376 100644 --- a/views/snippets/footer.html +++ b/views/snippets/footer.html @@ -481,9 +481,16 @@ hall_name_empty: "{{ t('hall.enter_name_error') }}", hall_enter_name_error: "{{ t('hall.enter_name_error') }}", hall_slug_empty_error: "{{ t('hall.slug_empty_error') }}", + hall_delete_confirm: "{{ t('hall.delete_confirm') }}", hall_image_uploaded: "{{ t('hall.image_uploaded') }}", hall_image_removed: "{{ t('hall.image_removed') }}", hall_click_upload_hint: "{{ t('hall.click_upload_hint') }}", + common_save: "{{ t('common.save') }}", + common_delete: "{{ t('common.delete') }}", + common_view: "{{ t('common.view') }}", + common_name: "{{ t('common.name') }}", + common_description: "{{ t('common.description') }}", + common_private: "{{ t('common.private') }}", // notifications notif_upload_approved: "{{ t('notifications.upload_approved_short') }}", notif_upload_pending: "{{ t('notifications.upload_pending_short') }}", diff --git a/views/user-hall-cards.html b/views/user-hall-cards.html index 31c5c83..a24e93f 100644 --- a/views/user-hall-cards.html +++ b/views/user-hall-cards.html @@ -1,6 +1,6 @@ @each(hallsList as hall)