diff --git a/public/app.js b/public/app.js index 9c8aca8..f4ac5a3 100644 --- a/public/app.js +++ b/public/app.js @@ -1396,8 +1396,7 @@ async function renderNotes(notes) { noteImages.forEach((image) => { imagesHtml += `
- ${image.original_name} - + ${image.original_name}
`; }); @@ -1611,6 +1610,34 @@ function addNoteEventListeners() { const noteContainer = event.target.closest("#note"); const noteContent = noteContainer.querySelector(".textNote"); + // Получаем существующие изображения заметки + const existingImages = Array.isArray(noteContainer.dataset.images) + ? JSON.parse(noteContainer.dataset.images) + : []; + + // Получаем изображения из контейнера заметки + const imagesContainer = noteContainer.querySelector( + ".note-images-container" + ); + let noteImagesList = []; + if (imagesContainer) { + const imageElements = + imagesContainer.querySelectorAll(".note-image-item"); + imageElements.forEach((imgElement) => { + const img = imgElement.querySelector("img"); + if (img) { + // Получаем id из data-атрибута изображения или из URL + const imageId = + img.dataset.imageId || img.getAttribute("data-image-id") || null; + noteImagesList.push({ + id: imageId, + src: img.dataset.imageSrc || img.src, + name: img.alt, + }); + } + }); + } + // Разворачиваем заметку при редактировании noteContent.classList.remove("collapsed"); @@ -1620,6 +1647,11 @@ function addNoteEventListeners() { showMoreBtn.style.display = "none"; } + // Скрываем контейнер с изображениями заметки при редактировании + if (imagesContainer) { + imagesContainer.style.display = "none"; + } + // Создаем контейнер для markdown кнопок const markdownButtonsContainer = document.createElement("div"); markdownButtonsContainer.classList.add("markdown-buttons"); @@ -1858,6 +1890,66 @@ function addNoteEventListeners() { editImageInput.style.display = "none"; editImageInput.id = `editImageInput-${noteId}`; + // Контейнер для существующих изображений + const existingImagesContainer = document.createElement("div"); + existingImagesContainer.id = `existingImagesContainer-${noteId}`; + existingImagesContainer.classList.add("image-preview-container"); + existingImagesContainer.style.display = + noteImagesList.length > 0 ? "block" : "none"; + + const existingImagesHeader = document.createElement("div"); + existingImagesHeader.classList.add("image-preview-header"); + existingImagesHeader.innerHTML = `Прикрепленные изображения:`; + + const existingImagesList = document.createElement("div"); + existingImagesList.id = `existingImagesList-${noteId}`; + existingImagesList.classList.add("image-preview-list"); + + existingImagesContainer.appendChild(existingImagesHeader); + existingImagesContainer.appendChild(existingImagesList); + + // Массив для отслеживания удаленных изображений + const deletedImagesIds = []; + + // Отображаем существующие изображения + if (noteImagesList.length > 0) { + noteImagesList.forEach((image) => { + const previewItem = document.createElement("div"); + previewItem.className = "image-preview-item"; + previewItem.dataset.imageId = image.id; + + const img = document.createElement("img"); + img.src = image.src; + img.alt = image.name || "Изображение"; + img.loading = "lazy"; + + const removeBtn = document.createElement("button"); + removeBtn.className = "remove-image-btn"; + removeBtn.textContent = "×"; + removeBtn.title = "Удалить изображение"; + + // Обработчик удаления существующего изображения + removeBtn.addEventListener("click", function () { + deletedImagesIds.push(image.id); + previewItem.remove(); + // Если больше нет изображений, скрываем контейнер + if (existingImagesList.children.length === 0) { + existingImagesContainer.style.display = "none"; + } + }); + + const imageInfo = document.createElement("div"); + imageInfo.className = "image-info"; + imageInfo.textContent = image.name || "Изображение"; + + previewItem.appendChild(img); + previewItem.appendChild(removeBtn); + previewItem.appendChild(imageInfo); + + existingImagesList.appendChild(previewItem); + }); + } + const editImagePreviewContainer = document.createElement("div"); editImagePreviewContainer.id = `editImagePreviewContainer-${noteId}`; editImagePreviewContainer.classList.add("image-preview-container"); @@ -1970,7 +2062,11 @@ function addNoteEventListeners() { // Функция сохранения для редактирования const saveEditNote = async function () { - if (textarea.value.trim() !== "" || editSelectedImages.length > 0) { + if ( + textarea.value.trim() !== "" || + editSelectedImages.length > 0 || + deletedImagesIds.length > 0 + ) { try { // Сбрасываем режим предпросмотра перед сохранением if (isEditPreviewMode) { @@ -1993,6 +2089,11 @@ function addNoteEventListeners() { throw new Error("Ошибка сохранения заметки"); } + // Удаляем изображения, которые были помечены на удаление + for (const imageId of deletedImagesIds) { + await deleteNoteImage(noteId, imageId); + } + // Загружаем новые изображения, если они есть if (editSelectedImages.length > 0) { await uploadEditImages(noteId); @@ -2098,6 +2199,7 @@ function addNoteEventListeners() { noteContent.appendChild(textarea); noteContent.appendChild(editPreviewContainer); noteContent.appendChild(editImageInput); + noteContent.appendChild(existingImagesContainer); noteContent.appendChild(editImagePreviewContainer); noteContent.appendChild(saveButtonContainer); @@ -2224,39 +2326,7 @@ function addImageEventListeners() { imageElement.addEventListener("click", imageElement._clickHandler); }); - // Обработчики для кнопок удаления изображений - document - .querySelectorAll(".remove-note-image-btn") - .forEach((buttonElement) => { - // Удаляем старые обработчики, если они есть - if (buttonElement._clickHandler) { - buttonElement.removeEventListener("click", buttonElement._clickHandler); - } - - // Создаем новый обработчик - buttonElement._clickHandler = async function (event) { - event.preventDefault(); - event.stopPropagation(); - - const noteId = this.dataset.noteId; - const imageId = this.dataset.imageId; - - if ( - noteId && - imageId && - confirm("Вы уверены, что хотите удалить это изображение?") - ) { - const success = await deleteNoteImage(noteId, imageId); - if (success) { - await loadNotes(true); // Перезагружаем заметки - } else { - alert("Ошибка удаления изображения"); - } - } - }; - - buttonElement.addEventListener("click", buttonElement._clickHandler); - }); + // Обработчики для кнопок удаления изображений удалены - кнопки удаления только в режиме редактирования } // Функция для применения визуальных стилей к чекбоксу diff --git a/server.js b/server.js index 5366027..b4221de 100644 --- a/server.js +++ b/server.js @@ -15,7 +15,14 @@ const app = express(); const PORT = process.env.PORT || 3000; // Настройка trust proxy для правильного получения IP адресов через прокси -app.set('trust proxy', true); +// Доверяем localhost и стандартной Docker bridge сети +app.set("trust proxy", [ + "127.0.0.1", + "::1", + "172.17.0.0/16", + "10.0.0.0/8", + "192.168.0.0/16", +]); // Создаем директорию для аватарок, если её нет const uploadsDir = path.join(__dirname, "public", "uploads"); @@ -295,18 +302,42 @@ function logAction(userId, actionType, details, ipAddress) { // Функция для получения IP-адреса клиента function getClientIP(req) { - // Проверяем различные заголовки, которые могут содержать внешний IP-адрес - // Приоритет: x-forwarded-for, x-real-ip, x-client-ip, cf-connecting-ip (Cloudflare) - let ip = - req.headers["x-forwarded-for"]?.split(",")[0].trim() || - req.headers["x-real-ip"] || - req.headers["x-client-ip"] || - req.headers["cf-connecting-ip"] || // Для Cloudflare - req.headers["x-cluster-client-ip"] || // Для кластеров - req.connection?.remoteAddress || - req.socket?.remoteAddress || - req.connection?.socket?.remoteAddress || - "unknown"; + // Для отладки логируем все релевантные заголовки и IP адреса + const debugInfo = { + "req.ip": req.ip, + "req.connection.remoteAddress": req.connection?.remoteAddress, + "x-forwarded-for": req.headers["x-forwarded-for"], + "x-real-ip": req.headers["x-real-ip"], + "x-client-ip": req.headers["x-client-ip"], + "cf-connecting-ip": req.headers["cf-connecting-ip"], + }; + console.log("IP Debug info:", JSON.stringify(debugInfo, null, 2)); + + // Используем встроенный в Express метод req.ip, который учитывает trust proxy + let ip = req.ip; + + // Если req.ip вернул undefined или является локальным/Docker адресом, пробуем другие варианты + if ( + !ip || + ip === "127.0.0.1" || + ip === "::1" || + ip === "172.17.0.1" || + ip.startsWith("172.17.") || + ip.startsWith("10.") || + ip.startsWith("192.168.") + ) { + // Проверяем заголовки прокси по приоритету + ip = + req.headers["x-forwarded-for"]?.split(",")[0].trim() || + req.headers["x-real-ip"] || + req.headers["x-client-ip"] || + req.headers["cf-connecting-ip"] || // Для Cloudflare + req.headers["x-cluster-client-ip"] || // Для кластеров + req.connection?.remoteAddress || + req.socket?.remoteAddress || + req.connection?.socket?.remoteAddress || + "unknown"; + } // Очищаем IP от скобок IPv6 и портов if (ip && ip !== "unknown") { @@ -340,7 +371,11 @@ function getClientIP(req) { ip = "127.0.0.1"; } - return ip || "unknown"; + // Финальный IP для логов + const finalIP = ip || "unknown"; + console.log(`Final IP for logging: ${finalIP}`); + + return finalIP; } // Миграции базы данных @@ -536,11 +571,16 @@ app.post("/api/register", async (req, res) => { req.session.userId = this.lastID; req.session.username = username; req.session.authenticated = true; - + // Логируем регистрацию const clientIP = getClientIP(req); - logAction(this.lastID, "register", `Регистрация нового пользователя`, clientIP); - + logAction( + this.lastID, + "register", + `Регистрация нового пользователя`, + clientIP + ); + res.json({ success: true, message: "Регистрация успешна" }); }); } catch (err) { @@ -579,11 +619,11 @@ app.post("/api/login", async (req, res) => { req.session.userId = user.id; req.session.username = user.username; req.session.authenticated = true; - + // Логируем вход const clientIP = getClientIP(req); logAction(user.id, "login", `Вход в систему`, clientIP); - + res.json({ success: true, message: "Вход успешен" }); } catch (err) { console.error("Ошибка при сравнении паролей:", err); @@ -802,12 +842,17 @@ app.post("/api/notes", requireAuth, (req, res) => { console.error("Ошибка создания заметки:", err.message); return res.status(500).json({ error: "Ошибка сервера" }); } - + // Логируем создание заметки const clientIP = getClientIP(req); const noteId = this.lastID; - logAction(req.session.userId, "note_create", `Создана заметка #${noteId}`, clientIP); - + logAction( + req.session.userId, + "note_create", + `Создана заметка #${noteId}`, + clientIP + ); + res.json({ id: noteId, content, date, time }); }); }); @@ -849,11 +894,16 @@ app.put("/api/notes/:id", requireAuth, (req, res) => { if (this.changes === 0) { return res.status(404).json({ error: "Заметка не найдена" }); } - + // Логируем обновление заметки const clientIP = getClientIP(req); - logAction(req.session.userId, "note_update", `Обновлена заметка #${id}`, clientIP); - + logAction( + req.session.userId, + "note_update", + `Обновлена заметка #${id}`, + clientIP + ); + res.json({ id, content, date: row.date, time: row.time }); }); }); @@ -912,11 +962,16 @@ app.delete("/api/notes/:id", requireAuth, (req, res) => { if (this.changes === 0) { return res.status(404).json({ error: "Заметка не найдена" }); } - + // Логируем удаление заметки const clientIP = getClientIP(req); - logAction(req.session.userId, "note_delete", `Удалена заметка #${id}`, clientIP); - + logAction( + req.session.userId, + "note_delete", + `Удалена заметка #${id}`, + clientIP + ); + res.json({ message: "Заметка удалена" }); }); }); @@ -1369,7 +1424,12 @@ app.put("/api/notes/:id/pin", requireAuth, (req, res) => { // Логируем действие const clientIP = getClientIP(req); const action = newPinState ? "закреплена" : "откреплена"; - logAction(req.session.userId, "note_pin", `Заметка #${id} ${action}`, clientIP); + logAction( + req.session.userId, + "note_pin", + `Заметка #${id} ${action}`, + clientIP + ); res.json({ success: true, is_pinned: newPinState }); }); @@ -1396,7 +1456,8 @@ app.put("/api/notes/:id/archive", requireAuth, (req, res) => { return res.status(403).json({ error: "Нет доступа к этой заметке" }); } - const updateSql = "UPDATE notes SET is_archived = 1, is_pinned = 0 WHERE id = ?"; + const updateSql = + "UPDATE notes SET is_archived = 1, is_pinned = 0 WHERE id = ?"; db.run(updateSql, [id], function (err) { if (err) { @@ -1406,7 +1467,12 @@ app.put("/api/notes/:id/archive", requireAuth, (req, res) => { // Логируем действие const clientIP = getClientIP(req); - logAction(req.session.userId, "note_archive", `Заметка #${id} архивирована`, clientIP); + logAction( + req.session.userId, + "note_archive", + `Заметка #${id} архивирована`, + clientIP + ); res.json({ success: true, message: "Заметка архивирована" }); }); @@ -1443,7 +1509,12 @@ app.put("/api/notes/:id/unarchive", requireAuth, (req, res) => { // Логируем действие const clientIP = getClientIP(req); - logAction(req.session.userId, "note_unarchive", `Заметка #${id} восстановлена из архива`, clientIP); + logAction( + req.session.userId, + "note_unarchive", + `Заметка #${id} восстановлена из архива`, + clientIP + ); res.json({ success: true, message: "Заметка восстановлена" }); }); @@ -1548,7 +1619,12 @@ app.delete("/api/notes/archived/:id", requireAuth, (req, res) => { // Логируем действие const clientIP = getClientIP(req); - logAction(req.session.userId, "note_delete_permanent", `Заметка #${id} окончательно удалена из архива`, clientIP); + logAction( + req.session.userId, + "note_delete_permanent", + `Заметка #${id} окончательно удалена из архива`, + clientIP + ); res.json({ success: true, message: "Заметка удалена окончательно" }); }); @@ -1628,12 +1704,12 @@ app.get("/settings", requireAuth, (req, res) => { app.post("/logout", (req, res) => { const userId = req.session.userId; const clientIP = getClientIP(req); - + // Логируем выход if (userId) { logAction(userId, "logout", "Выход из системы", clientIP); } - + req.session.destroy((err) => { if (err) { return res.status(500).json({ error: "Ошибка выхода" });