84/100 di schifo
Bellissimo: commenti che urlano 'la sicurezza ora è nel Backend', e poi `post.title` viene pompato in `innerHTML` senza un escape che sia uno. Il design pattern dominante qui è 'concatena stringhe e prega' – lo vedo nei pulsanti, nella paginazione, nei commenti, pure nella lista dei post. Escapi i commenti due volte con lo stesso blocco identico, ma per l'immagine uploadata usi direttamente `data.imageUrl` dentro un attributo senza controllo, perché tanto gli attributi non servono a niente. Stato globale moderazione, `currentModerationPage`, e un `loadAdminPosts()` che ti risucchia alla prima pagina dopo ogni salvataggio: il puntatore è una suggestione. Se questo pannello fosse un locale, la ASL lo chiudeva per XSS ambientale.
// NESSUN CONTROLLO LOCALSTORAGE QUI IN CIMA! La sicurezza ora è nel Backend.
let currentModerationPostId = null;
let currentModerationPage = 1;
function cleanTextFromMarkdown(text) {
return (text || "")
.replace(/!\[[^\]]*\]\([^)]*\)/g, " ")
.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
.replace(/`([^`]+)`/g, "$1")
.replace(/<[^>]+>/g, " ")
.replace(/[#+>*_~\-]+/g, " ")
.replace(/\s+/g, " ")
.trim();
}
function estimateReadingTime(text) {
const words = cleanTextFromMarkdown(text).split(" ").filter(Boolean).length;
if (words < 50) {
return { label: "less than 1 min", words };
}
const minutes = Math.max(1, Math.ceil(words / 200));
return { label: `${minutes} min`, words };
}
function updateEditorReadingTime() {
const metaText = document.getElementById("metaDescription")?.value || "";
const articleText = document.getElementById("contentMarkdown")?.value || "";
const metaTarget = document.getElementById("meta-char-count");
const articleTarget = document.getElementById("article-reading-time");
const articleReading = estimateReadingTime(articleText);
if (metaTarget) {
metaTarget.textContent = `Meta description length: ${metaText.length}/170 characters`;
}
if (articleTarget) {
articleTarget.textContent = `Article reading time: ${articleReading.label}`;
}
}
// 1. CARICA POST NELLA SIDEBAR
async function loadAdminPosts(page = 1) {
const listDiv = document.getElementById("admin-post-list");
try {
// fetch ora invia in automatico il cookie HttpOnly se siamo sullo stesso dominio
const res = await fetch(`/api/admin/posts?page=${page}`);
// ECCO IL VERO BUTTAFUORI: Se il server dice 401 (Cookie mancante o scaduto), ti butta fuori
if (res.status === 401) {
window.location.href = "/login";
return;
}
const data = await res.json();
listDiv.innerHTML = "<h3>Your Posts</h3>";
data.posts.forEach((post) => {
const normalizedStatus =
post.status === "Published" ? "Published" : "Draft";
const badgeClass =
normalizedStatus === "Published" ? "bg-published" : "bg-draft";
const articleUrl = post.slug
? `/articolo?slug=${encodeURIComponent(post.slug)}`
: `/articolo?id=${post._id}`;
listDiv.innerHTML += `
<div class="post-item-admin">
<h4>${post.title}</h4>
<span class="badge ${badgeClass}">${normalizedStatus}</span>
<div style="margin-top: 10px; display: flex; gap: 5px;">
<button type="button" class="btn-glossy js-edit-post" data-post-id="${post._id}">Edit</button>
<button type="button" class="btn-glossy btn-danger-glossy js-delete-post" data-post-id="${post._id}">Delete</button>
<a href="${articleUrl}" target="_blank" class="btn-glossy" style="text-decoration:none">View</a>
</div>
</div>
`;
});
// Controlli Paginazione Sidebar
const nav = document.createElement("div");
nav.style.textAlign = "center";
nav.style.marginTop = "15px";
if (page > 1)
nav.innerHTML += `<button type="button" class="btn-glossy js-admin-page" data-page="${page - 1}">«</button> `;
nav.innerHTML += `<span style="font-size:0.8em"> ${page} / ${data.totalPages} </span>`;
if (page < data.totalPages)
nav.innerHTML += ` <button type="button" class="btn-glossy js-admin-page" data-page="${page + 1}">»</button>`;
listDiv.appendChild(nav);
} catch (error) {
listDiv.innerHTML = "<p>Error loading posts.</p>";
}
}
// 2. CARICA DATI NELL'EDITOR E COMMENTI
async function editPost(id) {
try {
const res = await fetch(`/api/posts/${id}`);
// Se scade la sessione mentre modifichi
if (res.status === 401) return (window.location.href = "/login");
const post = await res.json();
document.getElementById("editor-title").textContent =
"Editing Article: " + post.title;
document.getElementById("edit-post-id").value = post._id;
document.getElementById("title").value = post.title;
document.getElementById("slug").value = post.slug;
document.getElementById("metaDescription").value =
post.metaDescription || "";
document.getElementById("category").value = post.category;
document.getElementById("status").value =
post.status === "Published" ? "Published" : "Draft";
document.getElementById("contentMarkdown").value = post.contentMarkdown;
updateEditorReadingTime();
loadCommentsForModeration(post._id);
window.scrollTo(0, 0);
} catch (error) {
alert("Error fetching post data");
}
}
// 3. MODERAZIONE COMMENTI (PAGINATA)
async function loadCommentsForModeration(postId, page = 1) {
currentModerationPostId = postId;
currentModerationPage = page;
let areaMod = document.getElementById("moderazione-comments");
if (!areaMod) {
areaMod = document.createElement("div");
areaMod.id = "moderazione-comments";
areaMod.style.marginTop = "40px";
document.querySelector(".editor-area").appendChild(areaMod);
}
try {
const res = await fetch(`/api/posts/${postId}/comments?page=${page}`);
if (res.status === 401) return (window.location.href = "/login");
const data = await res.json();
areaMod.innerHTML = `<hr><h3>Comment Moderation (Page ${data.currentPage})</h3>`;
if (!data.comments || data.comments.length === 0) {
areaMod.innerHTML += "<p>No comments for this post.</p>";
return;
}
data.comments.forEach((c) => {
// Usa una funzione per evitare XSS se non l'hai già dichiarata globale
const safeText = c.text.replace(
/[&<>'"]/g,
(tag) =>
({
"&": "&",
"<": "<",
">": ">",
"'": "'",
'"': """,
})[tag],
);
const safeAuthor = c.author.replace(
/[&<>'"]/g,
(tag) =>
({
"&": "&",
"<": "<",
">": ">",
"'": "'",
'"': """,
})[tag],
);
areaMod.innerHTML += `
<div class="comment-item-admin" data-comment-id="${c._id}">
<div style="display:flex; justify-content:space-between; align-items:center;">
<strong>${safeAuthor}</strong>
<button type="button" class="btn-glossy btn-danger-glossy js-delete-comment" style="font-size:0.7em" data-post-id="${postId}" data-comment-id="${c._id}">DELETE</button>
</div>
<p style="margin: 8px 0 0 0;">${safeText}</p>
</div>
`;
});
// Paginazione Commenti
const nav = document.createElement("div");
nav.style.textAlign = "center";
nav.style.marginTop = "10px";
if (page > 1)
nav.innerHTML += `<button type="button" class="btn-glossy js-moderation-page" data-post-id="${postId}" data-page="${page - 1}">« Previous</button> `;
if (page < data.totalPages)
nav.innerHTML += ` <button type="button" class="btn-glossy js-moderation-page" data-post-id="${postId}" data-page="${page + 1}">Next »</button>`;
areaMod.appendChild(nav);
} catch (err) {
areaMod.innerHTML = "<p>Error loading comments.</p>";
}
}
// 4. AZIONI (SALVA / ELIMINA COMMENTO / ELIMINA POST)
async function deleteComment(postId, commentId) {
if (!confirm("Delete this comment?")) return;
try {
const res = await fetch(`/api/posts/${postId}/comments/${commentId}`, {
method: "DELETE",
});
if (res.status === 401) return (window.location.href = "/login");
if (res.ok) {
const commentNode = document.querySelector(
`[data-comment-id="${commentId}"]`,
);
if (commentNode) {
commentNode.remove();
}
await loadCommentsForModeration(
currentModerationPostId || postId,
currentModerationPage,
);
return;
}
const errorData = await res.json().catch(() => ({}));
alert(errorData.error || "Error deleting comment.");
} catch (error) {
alert("Error deleting comment.");
}
}
document.getElementById("form-editor").addEventListener("submit", async (e) => {
e.preventDefault();
const postId = document.getElementById("edit-post-id").value;
const saveResultEl = document.getElementById("save-result");
const formData = {
title: document.getElementById("title").value,
slug: document.getElementById("slug").value,
metaDescription: document.getElementById("metaDescription").value,
category: document.getElementById("category").value,
status: document.getElementById("status").value,
contentMarkdown: document.getElementById("contentMarkdown").value,
};
try {
const response = await fetch(
postId ? `/api/posts/${postId}` : `/api/posts`,
{
method: postId ? "PUT" : "POST",
headers: { "Content-Type": "application/json" }, // NON c'è più l'Authorization Bearer qui!
body: JSON.stringify(formData),
},
);
if (response.status === 401) return (window.location.href = "/login");
if (response.ok) {
const savedPost = await response.json().catch(() => null);
const normalizedStatus =
savedPost?.status === "Published"
? "Published"
: savedPost?.status || formData.status;
if (normalizedStatus === "Draft") {
if (savedPost?._id) {
document.getElementById("edit-post-id").value =
savedPost._id;
}
document.getElementById("editor-title").textContent =
"Editing Article: " + formData.title;
if (saveResultEl) {
saveResultEl.textContent =
"Draft saved. You can continue writing.";
saveResultEl.style.color = "#5cb85c";
}
} else {
alert("Saved!");
resetForm();
if (saveResultEl) {
saveResultEl.textContent = "";
}
}
loadAdminPosts();
return;
}
if (saveResultEl) {
saveResultEl.textContent = "Save failed. Please try again.";
saveResultEl.style.color = "#d9534f";
}
} catch (error) {
alert("Server error.");
}
});
async function deletePost(id) {
if (!confirm("Delete this post permanently?")) return;
try {
const res = await fetch(`/api/posts/${id}`, { method: "DELETE" });
if (res.status === 401) return (window.location.href = "/login");
if (res.ok) {
loadAdminPosts();
resetForm();
}
} catch (error) {
alert("Error deleting post");
}
}
function resetForm() {
document.getElementById("form-editor").reset();
document.getElementById("edit-post-id").value = "";
document.getElementById("editor-title").textContent = "Create New Post";
const modSection = document.getElementById("moderazione-comments");
if (modSection) modSection.innerHTML = "";
updateEditorReadingTime();
}
function insertTextAtCursor(textarea, textToInsert) {
const start = textarea.selectionStart ?? textarea.value.length;
const end = textarea.selectionEnd ?? textarea.value.length;
const currentText = textarea.value;
textarea.value =
currentText.substring(0, start) +
textToInsert +
currentText.substring(end);
const newPosition = start + textToInsert.length;
textarea.selectionStart = newPosition;
textarea.selectionEnd = newPosition;
textarea.focus();
}
const MAX_IMAGE_UPLOAD_BYTES = 20 * 1024 * 1024;
async function uploadEditorImage(file) {
const statusEl = document.getElementById("image-upload-status");
const contentMarkdown = document.getElementById("contentMarkdown");
const sizeSelector = document.getElementById("image-size-selector");
if (!file) return;
if (file.size > MAX_IMAGE_UPLOAD_BYTES) {
statusEl.textContent = "Image too large. Max 20MB.";
statusEl.style.color = "#d9534f";
return;
}
statusEl.textContent = "Uploading image...";
statusEl.style.color = "#666";
const formData = new FormData();
formData.append("image", file);
try {
const response = await fetch("/api/upload-image", {
method: "POST",
body: formData,
});
if (response.status === 401) {
window.location.href = "/login";
return;
}
const data = await response.json();
if (!response.ok) {
statusEl.textContent = data.error || "Upload failed.";
statusEl.style.color = "#d9534f";
return;
}
const imageSizeClasses = {
small: "img-fixed-small",
medium: "img-fixed-medium",
large: "img-fixed-large",
};
const selectedSize = sizeSelector?.value || "medium";
const imageClass =
imageSizeClasses[selectedSize] || imageSizeClasses.medium;
const altText = (file.name || "image")
.replace(/\.[^/.]+$/, "")
.replace(/[<>"']/g, "")
.trim();
const imageTag = `<img src="${data.imageUrl}" alt="${altText || "image"}" class="${imageClass}">`;
insertTextAtCursor(contentMarkdown, `\n${imageTag}\n`);
statusEl.textContent = "Image inserted in article.";
statusEl.style.color = "#5cb85c";
} catch (error) {
statusEl.textContent = "Server error during upload.";
statusEl.style.color = "#d9534f";
}
}
// 5. LOGOUT TRAMITE ROTTA API
document.getElementById("btn-logout").addEventListener("click", async () => {
try {
// Chiamiamo il server per farci distruggere il cookie!
await fetch("/api/logout", { method: "POST" });
window.location.href = "/login";
} catch (err) {
window.location.href = "/login";
}
});
document.addEventListener("DOMContentLoaded", () => loadAdminPosts(1));
document.addEventListener("DOMContentLoaded", () => {
updateEditorReadingTime();
document
.getElementById("metaDescription")
?.addEventListener("input", updateEditorReadingTime);
document
.getElementById("contentMarkdown")
?.addEventListener("input", updateEditorReadingTime);
});
document
.getElementById("btn-upload-image")
.addEventListener("click", function () {
document.getElementById("image-upload-input").click();
});
document
.getElementById("image-upload-input")
.addEventListener("change", function (e) {
const [file] = e.target.files;
uploadEditorImage(file);
e.target.value = "";
});
document.getElementById("btn-new-post")?.addEventListener("click", () => {
resetForm();
});
document.addEventListener("click", (event) => {
const pageButton = event.target.closest(".js-admin-page");
if (pageButton) {
const page = Number(pageButton.dataset.page);
if (Number.isFinite(page)) {
loadAdminPosts(page);
}
return;
}
const editButton = event.target.closest(".js-edit-post");
if (editButton) {
editPost(editButton.dataset.postId);
return;
}
const deletePostButton = event.target.closest(".js-delete-post");
if (deletePostButton) {
deletePost(deletePostButton.dataset.postId);
return;
}
const deleteCommentButton = event.target.closest(".js-delete-comment");
if (deleteCommentButton) {
deleteComment(
deleteCommentButton.dataset.postId,
deleteCommentButton.dataset.commentId,
);
return;
}
const moderationPageButton = event.target.closest(".js-moderation-page");
if (moderationPageButton) {
const page = Number(moderationPageButton.dataset.page);
loadCommentsForModeration(
moderationPageButton.dataset.postId,
Number.isFinite(page) ? page : 1,
);
}
});