roast.code
Antonio Fasanella
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) =>
                    ({
                        "&": "&amp;",
                        "<": "&lt;",
                        ">": "&gt;",
                        "'": "&#39;",
                        '"': "&quot;",
                    })[tag],
            );
            const safeAuthor = c.author.replace(
                /[&<>'"]/g,
                (tag) =>
                    ({
                        "&": "&amp;",
                        "<": "&lt;",
                        ">": "&gt;",
                        "'": "&#39;",
                        '"': "&quot;",
                    })[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,
        );
    }
});
Fatti roastare anche tu →