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 += `
-

-
+
`;
});
@@ -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: "Ошибка выхода" });