✨ Обновлены функции обработки IP-адресов и улучшен интерфейс редактирования заметок
- Настроена обработка IP-адресов с учетом доверенных прокси и логирование для отладки. - Добавлены функции для отображения и удаления существующих изображений при редактировании заметок. - Оптимизированы обработчики событий для кнопок удаления изображений, теперь они доступны только в режиме редактирования. - Обновлены стили и структура интерфейса для улучшения пользовательского опыта.
This commit is contained in:
parent
96caa85e3f
commit
df2c94dc0e
142
public/app.js
142
public/app.js
@ -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
146
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: "Ошибка выхода" });
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user