// admin.js // Logique d'administration pour la gestion des messages (ES Module) - AVEC SUPPORT RÔLES /** * Utilitaire de sélection raccourci * @param {string} selector * @returns {Element|null} */ function $(selector) { return document.querySelector(selector); } /** * Affiche une notification toast * @param {string} message * @param {'success'|'error'} type */ function showToast(message, type = 'success') { const toastEl = $('#toast'); if (!toastEl) return; toastEl.textContent = message; toastEl.className = `toast ${type} show`; setTimeout(() => { toastEl.classList.remove('show'); }, 3000); } /** * Échappe le HTML pour prévenir les XSS * @param {string} unsafe * @returns {string} */ function escapeHtml(unsafe) { return String(unsafe).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } /** * Formate un timestamp Unix (secondes) en date locale FR * @param {number} timestamp * @returns {string} */ function formatDate(timestamp) { return new Date(timestamp * 1000).toLocaleString('fr-FR'); } // --- Logique Principale --- const SLUG = new URLSearchParams(window.location.search).get('link'); const container = $('#messagesContainer'); const slugTitle = $('#slugTitle'); // Vérification immédiate de l'ID lien if (!SLUG) { document.body.innerHTML = '
ID lien manquant (?link=ID)
'; throw new Error('No Link ID provided in URL'); } /** * Charge les messages via l'API */ async function loadMessages() { try { const res = await fetch(`/api/admin/links/${SLUG}/messages`, { credentials: 'include' }); if (!res.ok) { if (res.status === 401 || res.status === 403) { container.innerHTML = `
🚫 Accès refusé : Vous n'êtes pas collaborateur sur ce lien.
`; throw new Error('Non autorisé (propriétaire/collaborateur uniquement)'); } throw new Error(`Erreur serveur (${res.status})`); } // Nouveau format : { messages: [...], myRole: 'owner'|'moderator'|'viewer' } const data = await res.json(); const messages = data.messages || []; const myRole = data.myRole || 'viewer'; // Mise à jour du titre avec rôle slugTitle.textContent = messages.length ? `ID ${SLUG} (${messages.length} messages) • Rôle: ${myRole}` : `ID ${SLUG} • Rôle: ${myRole}`; renderMessages(messages, myRole); } catch (error) { console.error('Load error:', error); if (!container.innerHTML.includes('Accès refusé')) { container.innerHTML = `
${escapeHtml(error.message)}
`; } } } /** * Rend la liste des messages dans le DOM * @param {Array} messages * @param {'owner'|'moderator'|'viewer'} myRole */ function renderMessages(messages, myRole) { const canBan = ['owner', 'moderator'].includes(myRole); if (!messages.length) { container.innerHTML = '
Aucun message.
'; return; } container.innerHTML = messages.map(msg => `
${msg.content ? escapeHtml(msg.content) : '[Image seulement]'}
${msg.images?.length ? `
${msg.images.map(img => `${escapeHtml(img.filename)}` ).join('')}
` : ''}
🕒 ${formatDate(msg.created_at)} 🔒 IP: ${escapeHtml(msg.ip_hash.slice(0, 12))}...
${canBan ? ` ` : ''}
`).join(''); attachEventListeners(); } /** * Attache les écouteurs d'événements sur les boutons générés */ function attachEventListeners() { // Boutons Ban (seulement s'ils existent dans le DOM grâce au canBan) container.querySelectorAll('[data-action="ban"]').forEach(btn => { btn.onclick = async (e) => { const row = e.target.closest('.msg-row'); const ipHash = row.dataset.ip; if (!confirm(`Bannir cet IP (${ipHash.slice(0,12)}...) de ce lien ?`)) return; try { const res = await fetch(`/api/admin/links/${SLUG}/ban`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ ip_hash: ipHash }) }); if (!res.ok) { const errData = await res.json().catch(() => ({ error: 'Erreur inconnue' })); throw new Error(errData.error || 'Échec du bannissement'); } showToast('IP bannie avec succès'); // Mise à jour UI optimiste e.target.textContent = '✅ Banni'; e.target.classList.add('banned'); e.target.disabled = true; } catch (error) { console.error('Ban error:', error); showToast(error.message, 'error'); } }; }); // Boutons Copy IP container.querySelectorAll('[data-action="copyip"]').forEach(btn => { btn.onclick = () => { const text = btn.textContent.trim(); navigator.clipboard.writeText(text).then(() => { showToast('Hash IP copié dans le presse-papiers'); }).catch(() => { showToast('Échec de la copie', 'error'); }); }; }); } // Initialisation loadMessages();