updating meme manager to update memes
This commit is contained in:
@@ -11,7 +11,7 @@ import flummpress from "flummpress";
|
|||||||
import { handleUpload } from "./upload_handler.mjs";
|
import { handleUpload } from "./upload_handler.mjs";
|
||||||
import { handleAvatarUpload, handleAvatarDelete } from "./avatar_handler.mjs";
|
import { handleAvatarUpload, handleAvatarDelete } from "./avatar_handler.mjs";
|
||||||
import { handleRethumbUpload } from "./rethumb_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 { handleEmojiUpload } from "./emoji_upload_handler.mjs";
|
||||||
import { handleHallImageUpload, handleHallImageDelete, handleHallDelete, handleHallUpdate, handleHallCreate } from "./hall_image_handler.mjs";
|
import { handleHallImageUpload, handleHallImageDelete, handleHallDelete, handleHallUpdate, handleHallCreate } from "./hall_image_handler.mjs";
|
||||||
import { handleMetaExtract } from "./meta_extract_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
|
// Bypass middleware for emoji uploads
|
||||||
app.use(async (req, res) => {
|
app.use(async (req, res) => {
|
||||||
if (req.method === 'POST' && req.url.pathname === '/api/v2/admin/emojis') {
|
if (req.method === 'POST' && req.url.pathname === '/api/v2/admin/emojis') {
|
||||||
|
|||||||
@@ -107,3 +107,117 @@ export const handleMemeUpload = async (req, res) => {
|
|||||||
return sendJson(res, { success: false, message: err.message }, 500);
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,49 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Meme Modal -->
|
||||||
|
<div id="edit-meme-modal" class="modal-overlay" style="display: none;">
|
||||||
|
<div class="modal-content" style="max-width: 500px; background: var(--dropdown-bg, #222); border: 1px solid var(--nav-border-color, #444); border-radius: 8px; padding: 20px;">
|
||||||
|
<h3 style="margin-top: 0; margin-bottom: 15px; border-bottom: 1px solid var(--nav-border-color, rgba(255,255,255,0.1)); padding-bottom: 10px; color: var(--white);">Edit Meme Template</h3>
|
||||||
|
|
||||||
|
<input type="hidden" id="edit-meme-id-db">
|
||||||
|
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 12px; text-align: left;">
|
||||||
|
<div>
|
||||||
|
<label style="display: block; font-size: 0.85em; margin-bottom: 4px; color: rgba(255,255,255,0.7);">Template ID (lowercase a-z, 0-9, - only)</label>
|
||||||
|
<input type="text" id="edit-meme-template-id" style="width: 100%; background: var(--bg, #111); border: 1px solid var(--black, #000); padding: 8px; color: var(--white); border-radius: 4px;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style="display: block; font-size: 0.85em; margin-bottom: 4px; color: rgba(255,255,255,0.7);">Display Name</label>
|
||||||
|
<input type="text" id="edit-meme-name" style="width: 100%; background: var(--bg, #111); border: 1px solid var(--black, #000); padding: 8px; color: var(--white); border-radius: 4px;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style="display: block; font-size: 0.85em; margin-bottom: 4px; color: rgba(255,255,255,0.7);">Category</label>
|
||||||
|
<input type="text" id="edit-meme-category" style="width: 100%; background: var(--bg, #111); border: 1px solid var(--black, #000); padding: 8px; color: var(--white); border-radius: 4px;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style="display: block; font-size: 0.85em; margin-bottom: 4px; color: rgba(255,255,255,0.7);">Image URL</label>
|
||||||
|
<input type="text" id="edit-meme-url" style="width: 100%; background: var(--bg, #111); border: 1px solid var(--black, #000); padding: 8px; color: var(--white); border-radius: 4px;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align: center; margin: 4px 0; font-size: 0.8em; opacity: 0.5; color: var(--white);">- OR -</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style="display: block; font-size: 0.85em; margin-bottom: 4px; color: rgba(255,255,255,0.7);">Upload New Image File</label>
|
||||||
|
<input type="file" id="edit-meme-file" accept="image/*" style="width: 100%; background: var(--bg, #111); border: 1px solid var(--black, #000); padding: 8px; color: var(--white); border-radius: 4px;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-actions" style="margin-top: 20px; display: flex; justify-content: flex-end; gap: 10px;">
|
||||||
|
<button onclick="window.memeAdmin.closeEditModal()" class="btn-cancel" style="padding: 8px 16px; background: rgba(255,255,255,0.1); color: var(--white); border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; cursor: pointer;">Cancel</button>
|
||||||
|
<button onclick="window.memeAdmin.saveMeme()" class="btn-save" style="padding: 8px 16px; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer;">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -47,6 +90,7 @@
|
|||||||
const res = await fetch('/api/v2/memes');
|
const res = await fetch('/api/v2/memes');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
|
window.memeAdmin.memes = data.memes;
|
||||||
const tbody = document.getElementById('meme-list');
|
const tbody = document.getElementById('meme-list');
|
||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
tbody.innerHTML = data.memes.map(m =>
|
tbody.innerHTML = data.memes.map(m =>
|
||||||
@@ -56,6 +100,7 @@
|
|||||||
'<td style="padding: 10px;"><span style="background: rgba(255,255,255,0.1); padding: 2px 8px; border-radius: 10px; font-size: 0.85em;">' + esc(m.category || 'General') + '</span></td>' +
|
'<td style="padding: 10px;"><span style="background: rgba(255,255,255,0.1); padding: 2px 8px; border-radius: 10px; font-size: 0.85em;">' + esc(m.category || 'General') + '</span></td>' +
|
||||||
'<td style="padding: 10px; font-family: monospace; opacity: 0.7;">' + esc(m.template_id) + '</td>' +
|
'<td style="padding: 10px; font-family: monospace; opacity: 0.7;">' + esc(m.template_id) + '</td>' +
|
||||||
'<td style="padding: 10px;">' +
|
'<td style="padding: 10px;">' +
|
||||||
|
'<button onclick="window.memeAdmin.openEditModal(' + m.id + ')" class="btn-edit" style="padding: 5px 15px; font-size: 0.8em; background: #28a745; color: white; border: none; cursor: pointer; border-radius: 2px; margin-right: 5px;">Edit</button>' +
|
||||||
'<button onclick="window.memeAdmin.deleteMeme(' + m.id + ')" class="btn-remove" style="padding: 5px 15px; font-size: 0.8em; background: #c00; color: white; border: none; cursor: pointer; border-radius: 2px;">Delete</button>' +
|
'<button onclick="window.memeAdmin.deleteMeme(' + m.id + ')" class="btn-remove" style="padding: 5px 15px; font-size: 0.8em; background: #c00; color: white; border: none; cursor: pointer; border-radius: 2px;">Delete</button>' +
|
||||||
'</td>' +
|
'</td>' +
|
||||||
'</tr>'
|
'</tr>'
|
||||||
@@ -141,8 +186,88 @@
|
|||||||
} catch (e) { console.error(e); }
|
} catch (e) { console.error(e); }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openEditModal = (id) => {
|
||||||
|
const meme = (window.memeAdmin.memes || []).find(m => m.id === id);
|
||||||
|
if (!meme) return;
|
||||||
|
|
||||||
|
document.getElementById('edit-meme-id-db').value = meme.id;
|
||||||
|
document.getElementById('edit-meme-template-id').value = meme.template_id;
|
||||||
|
document.getElementById('edit-meme-name').value = meme.name;
|
||||||
|
document.getElementById('edit-meme-category').value = meme.category || 'General';
|
||||||
|
document.getElementById('edit-meme-url').value = meme.url;
|
||||||
|
document.getElementById('edit-meme-file').value = '';
|
||||||
|
|
||||||
|
const modal = document.getElementById('edit-meme-modal');
|
||||||
|
if (modal) modal.style.display = 'flex';
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeEditModal = () => {
|
||||||
|
const modal = document.getElementById('edit-meme-modal');
|
||||||
|
if (modal) modal.style.display = 'none';
|
||||||
|
document.getElementById('edit-meme-file').value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveMeme = async () => {
|
||||||
|
const id = document.getElementById('edit-meme-id-db').value;
|
||||||
|
const template_id = document.getElementById('edit-meme-template-id').value.trim().toLowerCase();
|
||||||
|
const name = document.getElementById('edit-meme-name').value.trim();
|
||||||
|
const category = document.getElementById('edit-meme-category').value.trim() || 'General';
|
||||||
|
const url = document.getElementById('edit-meme-url').value.trim();
|
||||||
|
const fileInput = document.getElementById('edit-meme-file');
|
||||||
|
|
||||||
|
if (!template_id || !name) {
|
||||||
|
return alert('Template ID and Display Name are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^[a-z0-9-]+$/.test(template_id)) {
|
||||||
|
return alert('Invalid ID. Use lowercase a-z, 0-9, - only.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = document.querySelector('#edit-meme-modal .btn-save');
|
||||||
|
const oldText = btn.textContent;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Saving...';
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('template_id', template_id);
|
||||||
|
formData.append('name', name);
|
||||||
|
formData.append('category', category);
|
||||||
|
formData.append('url', url);
|
||||||
|
if (fileInput.files[0]) {
|
||||||
|
formData.append('file', fileInput.files[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const headers = {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
};
|
||||||
|
const csrf = '{{ csrf_token }}';
|
||||||
|
if (csrf) headers['X-CSRF-Token'] = csrf;
|
||||||
|
|
||||||
|
const res = await fetch('/api/v2/admin/memes/' + id + '/edit', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: headers,
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
closeEditModal();
|
||||||
|
loadMemes();
|
||||||
|
} else {
|
||||||
|
alert('Server Error: ' + (data.message || 'Unknown error'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[MEME_ADMIN] Edit Error:', e);
|
||||||
|
alert('Save failed: ' + e.message);
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = oldText;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Global scope for onclick handlers
|
// Global scope for onclick handlers
|
||||||
window.memeAdmin = { deleteMeme };
|
window.memeAdmin = { deleteMeme, openEditModal, closeEditModal, saveMeme, memes: [] };
|
||||||
|
|
||||||
const btnAddMeme = document.getElementById('add-meme');
|
const btnAddMeme = document.getElementById('add-meme');
|
||||||
if (btnAddMeme) {
|
if (btnAddMeme) {
|
||||||
|
|||||||
Reference in New Issue
Block a user