Обновлены функции обработки 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);
});
// Обработчики для кнопок удаления изображений удалены - кнопки удаления только в режиме редактирования
}
// Функция для применения визуальных стилей к чекбоксу

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