NathMen12's picture
Update public/admin.js
eed6a8b verified
Raw
History Blame Contribute Delete
6.18 kB
// 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) => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[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 = '<div style="padding:2rem; text-align:center; color:var(--danger)">ID lien manquant (?link=ID)</div>';
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 = `<div class="empty" style="color:var(--danger)">🚫 Accès refusé : Vous n'êtes pas collaborateur sur ce lien.</div>`;
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 = `<div class="empty" style="color:var(--danger)">${escapeHtml(error.message)}</div>`;
}
}
}
/**
* Rend la liste des messages dans le DOM
* @param {Array<Object>} messages
* @param {'owner'|'moderator'|'viewer'} myRole
*/
function renderMessages(messages, myRole) {
const canBan = ['owner', 'moderator'].includes(myRole);
if (!messages.length) {
container.innerHTML = '<div class="empty">Aucun message.</div>';
return;
}
container.innerHTML = messages.map(msg => `
<div class="msg-row" data-id="${escapeHtml(msg.id)}" data-ip="${escapeHtml(msg.ip_hash)}">
<div>
<div class="msg-content">
${msg.content ? escapeHtml(msg.content) : '<em style="color:var(--muted)">[Image seulement]</em>'}
</div>
${msg.images?.length ? `
<div class="msg-images">
${msg.images.map(img =>
`<img src="${escapeHtml(img.url)}" alt="${escapeHtml(img.filename)}" loading="lazy">`
).join('')}
</div>
` : ''}
</div>
<div class="msg-meta">
<span>🕒 ${formatDate(msg.created_at)}</span>
<span>🔒 IP: <code>${escapeHtml(msg.ip_hash.slice(0, 12))}...</code></span>
</div>
<div class="msg-actions">
${canBan ? `
<button
class="btn btn-danger"
data-action="ban"
${msg.banned ? 'disabled' : ''}
>
${msg.banned ? '✅ Banni' : '🚫 Bannir IP'}
</button>
` : ''}
<button
class="btn btn-secondary"
data-action="copyip"
title="Copier hash IP"
>
${escapeHtml(msg.ip_hash.slice(0, 8))}
</button>
</div>
</div>
`).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();