Обновлены функции обработки IP-адресов и улучшен интерфейс редактирования заметок

- Настроена обработка IP-адресов с учетом доверенных прокси и логирование для отладки.
- Добавлены функции для отображения и удаления существующих изображений при редактировании заметок.
- Оптимизированы обработчики событий для кнопок удаления изображений, теперь они доступны только в режиме редактирования.
- Обновлены стили и структура интерфейса для улучшения пользовательского опыта.
This commit is contained in:
Fovway 2025-10-26 06:47:27 +07:00
parent 96caa85e3f
commit df2c94dc0e
2 changed files with 217 additions and 71 deletions

View File

@ -1396,8 +1396,7 @@ async function renderNotes(notes) {
noteImages.forEach((image) => {
imagesHtml += `
<div class="note-image-item">
<img src="${image.file_path}" alt="${image.original_name}" class="note-image" data-image-src="${image.file_path}" loading="lazy">
<button class="remove-note-image-btn" data-note-id="${note.id}" data-image-id="${image.id}" title="Удалить изображение">×</button>
<img src="${image.file_path}" alt="${image.original_name}" class="note-image" data-image-src="${image.file_path}" data-image-id="${image.id}" loading="lazy">
</div>
`;
});
@ -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 = `<span>Прикрепленные изображения:</span>`;
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);
});
// Обработчики для кнопок удаления изображений удалены - кнопки удаления только в режиме редактирования
}
// Функция для применения визуальных стилей к чекбоксу

104
server.js
View File

@ -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,9 +302,32 @@ 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 =
// Для отладки логируем все релевантные заголовки и 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"] ||
@ -307,6 +337,7 @@ function getClientIP(req) {
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;
}
// Миграции базы данных
@ -539,7 +574,12 @@ app.post("/api/register", async (req, res) => {
// Логируем регистрацию
const clientIP = getClientIP(req);
logAction(this.lastID, "register", `Регистрация нового пользователя`, clientIP);
logAction(
this.lastID,
"register",
`Регистрация нового пользователя`,
clientIP
);
res.json({ success: true, message: "Регистрация успешна" });
});
@ -806,7 +846,12 @@ app.post("/api/notes", requireAuth, (req, res) => {
// Логируем создание заметки
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 });
});
@ -852,7 +897,12 @@ app.put("/api/notes/:id", requireAuth, (req, res) => {
// Логируем обновление заметки
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 });
});
@ -915,7 +965,12 @@ app.delete("/api/notes/:id", requireAuth, (req, res) => {
// Логируем удаление заметки
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: "Заметка удалена окончательно" });
});