✨ Добавлена поддержка загрузки и управления файлами в заметках
- Реализована возможность загрузки файлов (PDF, DOC, XLS и др.) к заметкам с помощью нового API. - Добавлены функции для получения, удаления и отображения прикрепленных файлов в интерфейсе заметок. - Обновлены стили и обработчики событий для работы с файлами в режиме редактирования и создания заметок. - Улучшена логика обработки файлов, включая проверку на дубликаты и ограничения по размеру.
This commit is contained in:
parent
372cea2e92
commit
49834d4ef4
545
public/app.js
545
public/app.js
@ -318,6 +318,13 @@ const imagePreviewContainer = document.getElementById("imagePreviewContainer");
|
||||
const imagePreviewList = document.getElementById("imagePreviewList");
|
||||
const clearImagesBtn = document.getElementById("clearImagesBtn");
|
||||
|
||||
// Элементы для загрузки файлов
|
||||
const fileBtn = document.getElementById("fileBtn");
|
||||
const fileInput = document.getElementById("fileInput");
|
||||
const filePreviewContainer = document.getElementById("filePreviewContainer");
|
||||
const filePreviewList = document.getElementById("filePreviewList");
|
||||
const clearFilesBtn = document.getElementById("clearFilesBtn");
|
||||
|
||||
// Элементы для предпросмотра заметки
|
||||
const notePreviewContainer = document.getElementById("notePreviewContainer");
|
||||
const notePreviewContent = document.getElementById("notePreviewContent");
|
||||
@ -329,6 +336,7 @@ const modalClose = document.querySelector(".image-modal-close");
|
||||
|
||||
// Массив для хранения выбранных изображений
|
||||
let selectedImages = [];
|
||||
let selectedFiles = [];
|
||||
|
||||
// Флаг режима предпросмотра
|
||||
let isPreviewMode = false;
|
||||
@ -1156,6 +1164,19 @@ imageBtn.addEventListener("touchend", function (event) {
|
||||
imageInput.click();
|
||||
});
|
||||
|
||||
// Обработчик для кнопки прикрепления файлов
|
||||
fileBtn.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
fileInput.click();
|
||||
});
|
||||
|
||||
fileBtn.addEventListener("touchend", function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
fileInput.click();
|
||||
});
|
||||
|
||||
// Обработчик для кнопки предпросмотра
|
||||
previewBtn.addEventListener("click", function () {
|
||||
togglePreview();
|
||||
@ -1399,6 +1420,48 @@ clearImagesBtn.addEventListener("click", function () {
|
||||
imageInput.value = "";
|
||||
});
|
||||
|
||||
// Обработчик для загрузки файлов
|
||||
fileInput.addEventListener("change", function (event) {
|
||||
const files = Array.from(event.target.files);
|
||||
let addedCount = 0;
|
||||
|
||||
files.forEach((file) => {
|
||||
// Проверяем размер файла (максимум 50MB)
|
||||
if (file.size > 50 * 1024 * 1024) {
|
||||
showNotification(
|
||||
`Файл "${file.name}" слишком большой. Максимальный размер: 50MB`,
|
||||
"error"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, не добавлен ли уже файл с таким именем и размером
|
||||
const isDuplicate = selectedFiles.some(
|
||||
(existingFile) =>
|
||||
existingFile.name === file.name && existingFile.size === file.size
|
||||
);
|
||||
|
||||
if (!isDuplicate) {
|
||||
selectedFiles.push(file);
|
||||
addedCount++;
|
||||
}
|
||||
});
|
||||
|
||||
if (addedCount > 0) {
|
||||
updateFilePreview();
|
||||
showNotification(`Добавлено файлов: ${addedCount}`, "success");
|
||||
}
|
||||
|
||||
fileInput.value = "";
|
||||
});
|
||||
|
||||
// Обработчик очистки всех файлов
|
||||
clearFilesBtn.addEventListener("click", function () {
|
||||
selectedFiles = [];
|
||||
updateFilePreview();
|
||||
fileInput.value = "";
|
||||
});
|
||||
|
||||
// Обработчики модального окна
|
||||
modalClose.addEventListener("click", function () {
|
||||
imageModal.style.display = "none";
|
||||
@ -1473,6 +1536,65 @@ function updateImagePreview() {
|
||||
});
|
||||
}
|
||||
|
||||
// Функция для обновления превью файлов
|
||||
function updateFilePreview() {
|
||||
if (selectedFiles.length === 0) {
|
||||
filePreviewContainer.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
filePreviewContainer.style.display = "block";
|
||||
filePreviewList.innerHTML = "";
|
||||
|
||||
selectedFiles.forEach((file, index) => {
|
||||
const previewItem = document.createElement("div");
|
||||
previewItem.className = "file-preview-item";
|
||||
|
||||
// Форматируем размер файла
|
||||
const fileSize = (file.size / 1024 / 1024).toFixed(2);
|
||||
const fileName =
|
||||
file.name.length > 30 ? file.name.substring(0, 30) + "..." : file.name;
|
||||
|
||||
// Определяем иконку по расширению файла
|
||||
const ext = file.name.split(".").pop().toLowerCase();
|
||||
let icon = "mdi:file";
|
||||
if (ext === "pdf") icon = "mdi:file-pdf";
|
||||
else if (["doc", "docx"].includes(ext)) icon = "mdi:file-word";
|
||||
else if (["xls", "xlsx"].includes(ext)) icon = "mdi:file-excel";
|
||||
else if (ext === "txt") icon = "mdi:file-document";
|
||||
else if (["zip", "rar", "7z"].includes(ext)) icon = "mdi:folder-zip";
|
||||
|
||||
previewItem.innerHTML = `
|
||||
<div class="file-icon">
|
||||
<span class="iconify" data-icon="${icon}"></span>
|
||||
</div>
|
||||
<div class="file-info">
|
||||
<div class="file-name">${fileName}</div>
|
||||
<div class="file-size">${fileSize} MB</div>
|
||||
</div>
|
||||
<button class="remove-file-btn" data-index="${index}" title="Удалить файл">×</button>
|
||||
`;
|
||||
|
||||
filePreviewList.appendChild(previewItem);
|
||||
|
||||
// Обработчик удаления файла
|
||||
const removeBtn = previewItem.querySelector(".remove-file-btn");
|
||||
removeBtn.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
selectedFiles.splice(index, 1);
|
||||
updateFilePreview();
|
||||
});
|
||||
|
||||
removeBtn.addEventListener("touchend", function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
selectedFiles.splice(index, 1);
|
||||
updateFilePreview();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Функция для отображения изображения в модальном окне
|
||||
function showImageModal(imageSrc) {
|
||||
console.log("showImageModal called with:", imageSrc);
|
||||
@ -1576,6 +1698,70 @@ async function getNoteImages(noteId) {
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для загрузки файлов на сервер
|
||||
async function uploadFiles(noteId) {
|
||||
if (selectedFiles.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
selectedFiles.forEach((file) => {
|
||||
formData.append("files", file);
|
||||
});
|
||||
|
||||
try {
|
||||
const isMobile =
|
||||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||
navigator.userAgent
|
||||
) ||
|
||||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2) ||
|
||||
window.matchMedia("(max-width: 768px)").matches;
|
||||
|
||||
if (isMobile) {
|
||||
const loadingDiv = document.createElement("div");
|
||||
loadingDiv.id = "mobile-file-upload-loading";
|
||||
loadingDiv.style.cssText = `
|
||||
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.8); color: white; padding: 20px;
|
||||
border-radius: 10px; z-index: 10000; font-size: 16px; text-align: center;
|
||||
`;
|
||||
loadingDiv.innerHTML = `
|
||||
<div>📎 Загрузка файлов...</div>
|
||||
<div style="font-size: 12px; margin-top: 10px;">${selectedFiles.length} файл(ов)</div>
|
||||
`;
|
||||
document.body.appendChild(loadingDiv);
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/notes/${noteId}/files`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Ошибка загрузки файлов");
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
const loadingDiv = document.getElementById("mobile-file-upload-loading");
|
||||
if (loadingDiv) {
|
||||
loadingDiv.remove();
|
||||
}
|
||||
|
||||
return result.files || [];
|
||||
} catch (error) {
|
||||
console.error("Ошибка загрузки файлов:", error);
|
||||
|
||||
const loadingDiv = document.getElementById("mobile-file-upload-loading");
|
||||
if (loadingDiv) {
|
||||
loadingDiv.remove();
|
||||
}
|
||||
|
||||
showNotification(`Ошибка загрузки файлов: ${error.message}`, "error");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для удаления изображения заметки
|
||||
async function deleteNoteImage(noteId, imageId) {
|
||||
try {
|
||||
@ -1594,6 +1780,24 @@ async function deleteNoteImage(noteId, imageId) {
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для удаления файла заметки
|
||||
async function deleteNoteFile(noteId, fileId) {
|
||||
try {
|
||||
const response = await fetch(`/api/notes/${noteId}/files/${fileId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Ошибка удаления файла");
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Ошибка удаления файла:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для загрузки заметок с сервера
|
||||
async function loadNotes(forceReload = false) {
|
||||
const now = Date.now();
|
||||
@ -1881,6 +2085,37 @@ async function renderNotes(notes) {
|
||||
imagesHtml += "</div>";
|
||||
}
|
||||
|
||||
// Используем файлы, которые уже пришли с заметкой
|
||||
const noteFiles = Array.isArray(note.files) ? note.files : [];
|
||||
let filesHtml = "";
|
||||
|
||||
if (noteFiles.length > 0) {
|
||||
filesHtml = '<div class="note-files-container">';
|
||||
noteFiles.forEach((file) => {
|
||||
const ext = file.original_name.split(".").pop().toLowerCase();
|
||||
let icon = "mdi:file";
|
||||
if (ext === "pdf") icon = "mdi:file-pdf";
|
||||
else if (["doc", "docx"].includes(ext)) icon = "mdi:file-word";
|
||||
else if (["xls", "xlsx"].includes(ext)) icon = "mdi:file-excel";
|
||||
else if (ext === "txt") icon = "mdi:file-document";
|
||||
else if (["zip", "rar", "7z"].includes(ext)) icon = "mdi:folder-zip";
|
||||
|
||||
const fileSize = (file.file_size / 1024 / 1024).toFixed(2);
|
||||
filesHtml += `
|
||||
<div class="note-file-item">
|
||||
<a href="${file.file_path}" download="${file.original_name}" class="note-file-link" data-file-id="${file.id}">
|
||||
<span class="iconify file-icon" data-icon="${icon}"></span>
|
||||
<div class="file-info">
|
||||
<div class="file-name">${file.original_name}</div>
|
||||
<div class="file-size">${fileSize} MB</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
filesHtml += "</div>";
|
||||
}
|
||||
|
||||
// Форматируем дату создания и изменения по локали устройства
|
||||
let dateDisplay;
|
||||
if (note.created_at) {
|
||||
@ -1935,6 +2170,7 @@ async function renderNotes(notes) {
|
||||
"""
|
||||
)}">${parsedContent}</div>
|
||||
${imagesHtml}
|
||||
${filesHtml}
|
||||
</div>
|
||||
`;
|
||||
notesList.insertAdjacentHTML("beforeend", noteHtml);
|
||||
@ -2128,6 +2364,32 @@ function addNoteEventListeners() {
|
||||
});
|
||||
}
|
||||
|
||||
// Получаем файлы из контейнера заметки
|
||||
const filesContainer = noteContainer.querySelector(
|
||||
".note-files-container"
|
||||
);
|
||||
let noteFilesList = [];
|
||||
if (filesContainer) {
|
||||
const fileElements = filesContainer.querySelectorAll(".note-file-item");
|
||||
fileElements.forEach((fileElement) => {
|
||||
const link = fileElement.querySelector(".note-file-link");
|
||||
if (link) {
|
||||
const fileId = link.dataset.fileId || null;
|
||||
const fileName =
|
||||
link.querySelector(".file-name")?.textContent || "";
|
||||
const fileSize =
|
||||
link.querySelector(".file-size")?.textContent || "";
|
||||
const filePath = link.getAttribute("href") || "";
|
||||
noteFilesList.push({
|
||||
id: fileId,
|
||||
name: fileName,
|
||||
size: fileSize,
|
||||
path: filePath,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Разворачиваем заметку при редактировании
|
||||
noteContent.classList.remove("collapsed");
|
||||
|
||||
@ -2142,6 +2404,11 @@ function addNoteEventListeners() {
|
||||
imagesContainer.style.display = "none";
|
||||
}
|
||||
|
||||
// Скрываем контейнер с файлами заметки при редактировании
|
||||
if (filesContainer) {
|
||||
filesContainer.style.display = "none";
|
||||
}
|
||||
|
||||
// Создаем контейнер для markdown кнопок
|
||||
const markdownButtonsContainer = document.createElement("div");
|
||||
markdownButtonsContainer.classList.add(
|
||||
@ -2176,6 +2443,7 @@ function addNoteEventListeners() {
|
||||
tag: "- [ ] ",
|
||||
},
|
||||
{ id: "editImageBtn", icon: "mdi:image-plus", tag: "image" },
|
||||
{ id: "editFileBtn", icon: "mdi:file-plus", tag: "file" },
|
||||
{ id: "editPreviewBtn", icon: "mdi:eye", tag: "preview" },
|
||||
];
|
||||
|
||||
@ -2384,6 +2652,14 @@ function addNoteEventListeners() {
|
||||
editImageInput.style.display = "none";
|
||||
editImageInput.id = `editImageInput-${noteId}`;
|
||||
|
||||
// Создаем элементы для загрузки файлов в режиме редактирования
|
||||
const editFileInput = document.createElement("input");
|
||||
editFileInput.type = "file";
|
||||
editFileInput.accept = ".pdf,.doc,.docx,.xls,.xlsx,.txt,.zip,.rar,.7z";
|
||||
editFileInput.multiple = true;
|
||||
editFileInput.style.display = "none";
|
||||
editFileInput.id = `editFileInput-${noteId}`;
|
||||
|
||||
// Контейнер для существующих изображений
|
||||
const existingImagesContainer = document.createElement("div");
|
||||
existingImagesContainer.id = `existingImagesContainer-${noteId}`;
|
||||
@ -2466,6 +2742,98 @@ function addNoteEventListeners() {
|
||||
// Массив для хранения новых изображений в режиме редактирования
|
||||
const editSelectedImages = [];
|
||||
|
||||
// Контейнер для существующих файлов
|
||||
const existingFilesContainer = document.createElement("div");
|
||||
existingFilesContainer.id = `existingFilesContainer-${noteId}`;
|
||||
existingFilesContainer.classList.add("file-preview-container");
|
||||
existingFilesContainer.style.display =
|
||||
noteFilesList.length > 0 ? "block" : "none";
|
||||
|
||||
const existingFilesHeader = document.createElement("div");
|
||||
existingFilesHeader.classList.add("file-preview-header");
|
||||
existingFilesHeader.innerHTML = `<span>Прикрепленные файлы:</span>`;
|
||||
|
||||
const existingFilesList = document.createElement("div");
|
||||
existingFilesList.id = `existingFilesList-${noteId}`;
|
||||
existingFilesList.classList.add("file-preview-list");
|
||||
|
||||
existingFilesContainer.appendChild(existingFilesHeader);
|
||||
existingFilesContainer.appendChild(existingFilesList);
|
||||
|
||||
// Массив для отслеживания удаленных файлов
|
||||
const deletedFilesIds = [];
|
||||
|
||||
// Отображаем существующие файлы
|
||||
if (noteFilesList.length > 0) {
|
||||
noteFilesList.forEach((file) => {
|
||||
const previewItem = document.createElement("div");
|
||||
previewItem.className = "file-preview-item";
|
||||
previewItem.dataset.fileId = file.id;
|
||||
|
||||
const ext = file.name.split(".").pop().toLowerCase();
|
||||
let icon = "mdi:file";
|
||||
if (ext === "pdf") icon = "mdi:file-pdf-box";
|
||||
else if (["doc", "docx"].includes(ext)) icon = "mdi:file-word-box";
|
||||
else if (["xls", "xlsx"].includes(ext)) icon = "mdi:file-excel-box";
|
||||
else if (ext === "txt") icon = "mdi:file-document-outline";
|
||||
else if (["zip", "rar", "7z"].includes(ext)) icon = "mdi:folder-zip";
|
||||
|
||||
const fileIcon = document.createElement("span");
|
||||
fileIcon.className = "iconify file-icon";
|
||||
fileIcon.setAttribute("data-icon", icon);
|
||||
|
||||
const fileInfo = document.createElement("div");
|
||||
fileInfo.className = "file-info-edit";
|
||||
fileInfo.innerHTML = `
|
||||
<div class="file-name">${file.name}</div>
|
||||
<div class="file-size">${file.size}</div>
|
||||
`;
|
||||
|
||||
const removeBtn = document.createElement("button");
|
||||
removeBtn.className = "remove-file-btn";
|
||||
removeBtn.textContent = "×";
|
||||
removeBtn.title = "Удалить файл";
|
||||
|
||||
// Обработчик удаления существующего файла
|
||||
removeBtn.addEventListener("click", function () {
|
||||
deletedFilesIds.push(file.id);
|
||||
previewItem.remove();
|
||||
// Если больше нет файлов, скрываем контейнер
|
||||
if (existingFilesList.children.length === 0) {
|
||||
existingFilesContainer.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
previewItem.appendChild(fileIcon);
|
||||
previewItem.appendChild(fileInfo);
|
||||
previewItem.appendChild(removeBtn);
|
||||
|
||||
existingFilesList.appendChild(previewItem);
|
||||
});
|
||||
}
|
||||
|
||||
const editFilePreviewContainer = document.createElement("div");
|
||||
editFilePreviewContainer.id = `editFilePreviewContainer-${noteId}`;
|
||||
editFilePreviewContainer.classList.add("file-preview-container");
|
||||
editFilePreviewContainer.style.display = "none";
|
||||
|
||||
const editFilePreviewHeader = document.createElement("div");
|
||||
editFilePreviewHeader.classList.add("file-preview-header");
|
||||
editFilePreviewHeader.innerHTML = `
|
||||
<span>Новые файлы:</span>
|
||||
<button type="button" class="clear-files-btn" id="editClearFilesBtn-${noteId}">Очистить все</button>
|
||||
`;
|
||||
|
||||
const editFilePreviewList = document.createElement("div");
|
||||
editFilePreviewList.id = `editFilePreviewList-${noteId}`;
|
||||
editFilePreviewList.classList.add("file-preview-list");
|
||||
|
||||
editFilePreviewContainer.appendChild(editFilePreviewHeader);
|
||||
editFilePreviewContainer.appendChild(editFilePreviewList);
|
||||
|
||||
// Массив для хранения новых файлов в режиме редактирования
|
||||
const editSelectedFiles = [];
|
||||
|
||||
// Контейнер для кнопки сохранения и подсказки
|
||||
const saveButtonContainer = document.createElement("div");
|
||||
saveButtonContainer.classList.add("save-button-container");
|
||||
@ -2545,6 +2913,50 @@ function addNoteEventListeners() {
|
||||
});
|
||||
};
|
||||
|
||||
// Функция обновления превью файлов для режима редактирования
|
||||
const updateEditFilePreview = function () {
|
||||
if (editSelectedFiles.length === 0) {
|
||||
editFilePreviewContainer.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
editFilePreviewContainer.style.display = "block";
|
||||
editFilePreviewList.innerHTML = "";
|
||||
|
||||
editSelectedFiles.forEach((file, index) => {
|
||||
const previewItem = document.createElement("div");
|
||||
previewItem.className = "file-preview-item";
|
||||
|
||||
const ext = file.name.split(".").pop().toLowerCase();
|
||||
let icon = "mdi:file";
|
||||
if (ext === "pdf") icon = "mdi:file-pdf-box";
|
||||
else if (["doc", "docx"].includes(ext)) icon = "mdi:file-word-box";
|
||||
else if (["xls", "xlsx"].includes(ext)) icon = "mdi:file-excel-box";
|
||||
else if (ext === "txt") icon = "mdi:file-document-outline";
|
||||
else if (["zip", "rar", "7z"].includes(ext)) icon = "mdi:folder-zip";
|
||||
|
||||
const fileSize = (file.size / 1024 / 1024).toFixed(2);
|
||||
|
||||
previewItem.innerHTML = `
|
||||
<span class="iconify file-icon" data-icon="${icon}"></span>
|
||||
<div class="file-info-edit">
|
||||
<div class="file-name">${file.name}</div>
|
||||
<div class="file-size">${fileSize} MB</div>
|
||||
</div>
|
||||
<button class="remove-file-btn" data-index="${index}">×</button>
|
||||
`;
|
||||
|
||||
editFilePreviewList.appendChild(previewItem);
|
||||
|
||||
// Обработчик удаления файла
|
||||
const removeBtn = previewItem.querySelector(".remove-file-btn");
|
||||
removeBtn.addEventListener("click", function () {
|
||||
editSelectedFiles.splice(index, 1);
|
||||
updateEditFilePreview();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Функция загрузки изображений для режима редактирования
|
||||
const uploadEditImages = async function (noteId) {
|
||||
if (editSelectedImages.length === 0) {
|
||||
@ -2574,12 +2986,43 @@ function addNoteEventListeners() {
|
||||
}
|
||||
};
|
||||
|
||||
// Функция загрузки файлов для режима редактирования
|
||||
const uploadEditFiles = async function (noteId) {
|
||||
if (editSelectedFiles.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
editSelectedFiles.forEach((file) => {
|
||||
formData.append("files", file);
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/notes/${noteId}/files`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Ошибка загрузки файлов");
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result.files || [];
|
||||
} catch (error) {
|
||||
console.error("Ошибка загрузки файлов:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// Функция сохранения для редактирования
|
||||
const saveEditNote = async function () {
|
||||
if (
|
||||
textarea.value.trim() !== "" ||
|
||||
editSelectedImages.length > 0 ||
|
||||
deletedImagesIds.length > 0
|
||||
deletedImagesIds.length > 0 ||
|
||||
editSelectedFiles.length > 0 ||
|
||||
deletedFilesIds.length > 0
|
||||
) {
|
||||
try {
|
||||
// Сбрасываем режим предпросмотра перед сохранением
|
||||
@ -2595,7 +3038,7 @@ function addNoteEventListeners() {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: textarea.value || " ", // Минимальный контент, если только изображения
|
||||
content: textarea.value || " ", // Минимальный контент, если только изображения/файлы
|
||||
}),
|
||||
});
|
||||
|
||||
@ -2608,11 +3051,21 @@ function addNoteEventListeners() {
|
||||
await deleteNoteImage(noteId, imageId);
|
||||
}
|
||||
|
||||
// Удаляем файлы, которые были помечены на удаление
|
||||
for (const fileId of deletedFilesIds) {
|
||||
await deleteNoteFile(noteId, fileId);
|
||||
}
|
||||
|
||||
// Загружаем новые изображения, если они есть
|
||||
if (editSelectedImages.length > 0) {
|
||||
await uploadEditImages(noteId);
|
||||
}
|
||||
|
||||
// Загружаем новые файлы, если они есть
|
||||
if (editSelectedFiles.length > 0) {
|
||||
await uploadEditFiles(noteId);
|
||||
}
|
||||
|
||||
// Перезагружаем заметки
|
||||
await loadNotes(true);
|
||||
} catch (error) {
|
||||
@ -2630,8 +3083,9 @@ function addNoteEventListeners() {
|
||||
const originalMarkdown = noteContent.dataset.originalContent || "";
|
||||
const hasTextChanges = textarea.value !== originalMarkdown;
|
||||
const hasNewImages = editSelectedImages.length > 0;
|
||||
const hasNewFiles = editSelectedFiles.length > 0;
|
||||
|
||||
if (hasTextChanges || hasNewImages) {
|
||||
if (hasTextChanges || hasNewImages || hasNewFiles) {
|
||||
const ok = await showConfirmModal(
|
||||
"Подтверждение отмены",
|
||||
"Отменить изменения?",
|
||||
@ -2659,6 +3113,18 @@ function addNoteEventListeners() {
|
||||
editImageInput.value = "";
|
||||
}
|
||||
|
||||
// Очистить выбранные новые файлы и превью
|
||||
editSelectedFiles.length = 0;
|
||||
if (editFilePreviewContainer) {
|
||||
editFilePreviewContainer.style.display = "none";
|
||||
}
|
||||
if (editFilePreviewList) {
|
||||
editFilePreviewList.innerHTML = "";
|
||||
}
|
||||
if (editFileInput) {
|
||||
editFileInput.value = "";
|
||||
}
|
||||
|
||||
// Перерисовать заметки, вернув исходное состояние
|
||||
await loadNotes(true);
|
||||
};
|
||||
@ -2696,6 +3162,41 @@ function addNoteEventListeners() {
|
||||
editImageInput.value = "";
|
||||
});
|
||||
|
||||
// Обработчики для загрузки файлов в режиме редактирования
|
||||
editFileInput.addEventListener("change", function (event) {
|
||||
const files = Array.from(event.target.files);
|
||||
const allowedExtensions = /\.(pdf|doc|docx|xls|xlsx|txt|zip|rar|7z)$/i;
|
||||
|
||||
files.forEach((file) => {
|
||||
if (allowedExtensions.test(file.name)) {
|
||||
if (file.size <= 50 * 1024 * 1024) {
|
||||
editSelectedFiles.push(file);
|
||||
} else {
|
||||
showNotification(
|
||||
`Файл ${file.name} слишком большой (максимум 50 МБ)`,
|
||||
"error"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
showNotification(
|
||||
`Файл ${file.name} имеет недопустимый формат`,
|
||||
"error"
|
||||
);
|
||||
}
|
||||
});
|
||||
updateEditFilePreview();
|
||||
});
|
||||
|
||||
// Обработчик очистки всех файлов в режиме редактирования
|
||||
const editClearFilesBtn = editFilePreviewHeader.querySelector(
|
||||
`#editClearFilesBtn-${noteId}`
|
||||
);
|
||||
editClearFilesBtn.addEventListener("click", function () {
|
||||
editSelectedFiles.length = 0;
|
||||
updateEditFilePreview();
|
||||
editFileInput.value = "";
|
||||
});
|
||||
|
||||
// Создаем контейнер для предпросмотра в режиме редактирования
|
||||
const editPreviewContainer = document.createElement("div");
|
||||
editPreviewContainer.classList.add("note-preview-container");
|
||||
@ -2714,14 +3215,17 @@ function addNoteEventListeners() {
|
||||
// Флаг для режима предпросмотра редактирования
|
||||
let isEditPreviewMode = false;
|
||||
|
||||
// Очищаем текущий контент и вставляем markdown кнопки, textarea, элементы для изображений и контейнер с кнопкой сохранить
|
||||
// Очищаем текущий контент и вставляем markdown кнопки, textarea, элементы для изображений и файлов и контейнер с кнопкой сохранить
|
||||
noteContent.innerHTML = "";
|
||||
noteContent.appendChild(markdownButtonsContainer);
|
||||
noteContent.appendChild(textarea);
|
||||
noteContent.appendChild(editPreviewContainer);
|
||||
noteContent.appendChild(editImageInput);
|
||||
noteContent.appendChild(editFileInput);
|
||||
noteContent.appendChild(existingImagesContainer);
|
||||
noteContent.appendChild(editImagePreviewContainer);
|
||||
noteContent.appendChild(existingFilesContainer);
|
||||
noteContent.appendChild(editFilePreviewContainer);
|
||||
noteContent.appendChild(saveButtonContainer);
|
||||
|
||||
// Применяем авторасширение после добавления в DOM
|
||||
@ -2741,6 +3245,9 @@ function addNoteEventListeners() {
|
||||
if (button.tag === "image") {
|
||||
// Для кнопки изображения открываем диалог выбора файлов
|
||||
editImageInput.click();
|
||||
} else if (button.tag === "file") {
|
||||
// Для кнопки файлов открываем диалог выбора файлов
|
||||
editFileInput.click();
|
||||
} else if (button.tag === "color") {
|
||||
// Для кнопки цвета открываем диалог выбора цвета
|
||||
insertColorTagForEdit(textarea);
|
||||
@ -3099,7 +3606,11 @@ function addSpoilerEventListeners() {
|
||||
|
||||
// Функция сохранения заметки (вынесена отдельно для повторного использования)
|
||||
async function saveNote() {
|
||||
if (noteInput.value.trim() !== "" || selectedImages.length > 0) {
|
||||
if (
|
||||
noteInput.value.trim() !== "" ||
|
||||
selectedImages.length > 0 ||
|
||||
selectedFiles.length > 0
|
||||
) {
|
||||
try {
|
||||
const { date, time } = getFormattedDateTime();
|
||||
|
||||
@ -3128,11 +3639,21 @@ async function saveNote() {
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
`;
|
||||
const attachmentsInfo = [];
|
||||
if (selectedImages.length > 0) {
|
||||
attachmentsInfo.push(`${selectedImages.length} изображений`);
|
||||
}
|
||||
if (selectedFiles.length > 0) {
|
||||
attachmentsInfo.push(`${selectedFiles.length} файлов`);
|
||||
}
|
||||
|
||||
savingIndicator.innerHTML = `
|
||||
<div>💾 Сохранение заметки...</div>
|
||||
${
|
||||
selectedImages.length > 0
|
||||
? `<div style="font-size: 12px; margin-top: 10px;">+ ${selectedImages.length} изображений</div>`
|
||||
attachmentsInfo.length > 0
|
||||
? `<div style="font-size: 12px; margin-top: 10px;">+ ${attachmentsInfo.join(
|
||||
", "
|
||||
)}</div>`
|
||||
: ""
|
||||
}
|
||||
`;
|
||||
@ -3163,17 +3684,25 @@ async function saveNote() {
|
||||
await uploadImages(noteId);
|
||||
}
|
||||
|
||||
// Загружаем файлы, если они есть
|
||||
if (selectedFiles.length > 0) {
|
||||
await uploadFiles(noteId);
|
||||
}
|
||||
|
||||
// Удаляем индикатор сохранения
|
||||
if (savingIndicator) {
|
||||
savingIndicator.remove();
|
||||
}
|
||||
|
||||
// Очищаем поле ввода и изображения, перезагружаем заметки
|
||||
// Очищаем поле ввода, изображения и файлы, перезагружаем заметки
|
||||
noteInput.value = "";
|
||||
noteInput.style.height = "auto";
|
||||
selectedImages = [];
|
||||
selectedFiles = [];
|
||||
updateImagePreview();
|
||||
updateFilePreview();
|
||||
imageInput.value = "";
|
||||
fileInput.value = "";
|
||||
|
||||
// Сбрасываем режим предпросмотра, если он был активен
|
||||
if (isPreviewMode) {
|
||||
|
||||
@ -351,6 +351,9 @@
|
||||
>
|
||||
<span class="iconify" data-icon="mdi:image-plus"></span>
|
||||
</button>
|
||||
<button class="btnMarkdown" id="fileBtn" title="Прикрепить файлы">
|
||||
<span class="iconify" data-icon="mdi:file-plus"></span>
|
||||
</button>
|
||||
<button class="btnMarkdown" id="previewBtn" title="Предпросмотр">
|
||||
<span class="iconify" data-icon="mdi:eye"></span>
|
||||
</button>
|
||||
@ -383,6 +386,15 @@
|
||||
style="display: none"
|
||||
/>
|
||||
|
||||
<!-- Скрытый input для загрузки файлов -->
|
||||
<input
|
||||
type="file"
|
||||
id="fileInput"
|
||||
accept=".pdf,.doc,.docx,.xls,.xlsx,.txt,.zip,.rar,.7z"
|
||||
multiple
|
||||
style="display: none"
|
||||
/>
|
||||
|
||||
<!-- Контейнер для отображения загруженных изображений -->
|
||||
<div
|
||||
id="imagePreviewContainer"
|
||||
@ -402,6 +414,21 @@
|
||||
<div id="imagePreviewList" class="image-preview-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Контейнер для отображения прикрепленных файлов -->
|
||||
<div
|
||||
id="filePreviewContainer"
|
||||
class="file-preview-container"
|
||||
style="display: none"
|
||||
>
|
||||
<div class="file-preview-header">
|
||||
<span>Прикрепленные файлы:</span>
|
||||
<button type="button" id="clearFilesBtn" class="clear-files-btn">
|
||||
Очистить все
|
||||
</button>
|
||||
</div>
|
||||
<div id="filePreviewList" class="file-preview-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="save-button-container">
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
|
||||
322
public/style.css
322
public/style.css
@ -2175,6 +2175,44 @@ textarea:focus {
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Выравниваем размер контейнеров в режиме редактирования под размер контейнеров создания */
|
||||
.textNote .markdown-buttons--edit,
|
||||
.textNote .note-preview-container,
|
||||
.textNote .image-preview-container,
|
||||
.textNote .file-preview-container,
|
||||
.textNote .textInput,
|
||||
.textNote textarea.textInput {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Стили для файлов в режиме редактирования - делаем такими же сжатыми как в создании */
|
||||
.textNote .file-preview-container .file-preview-item {
|
||||
padding: 6px 10px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.textNote .file-preview-container .file-preview-item .file-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.textNote .file-preview-container .file-preview-item .file-name {
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.textNote .file-preview-container .file-preview-item .file-size {
|
||||
font-size: 11px;
|
||||
margin-top: 1px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.textNote .file-preview-container .file-preview-item .remove-file-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.note-preview-content h1,
|
||||
.note-preview-content h2,
|
||||
.note-preview-content h3,
|
||||
@ -2576,6 +2614,64 @@ textarea:focus {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.note-files-container {
|
||||
margin: 10px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.note-file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.note-file-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.note-file-link:hover {
|
||||
background: #e9ecef;
|
||||
border-color: var(--accent-color, #007bff);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.note-file-link .file-icon {
|
||||
font-size: 32px;
|
||||
color: var(--accent-color, #007bff);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.note-file-link .file-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.note-file-link .file-name {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.note-file-link .file-size {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.note-image-item .remove-note-image-btn {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
@ -3372,3 +3468,229 @@ textarea:focus {
|
||||
background: linear-gradient(45deg, #d4edda, #c3e6cb);
|
||||
box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.35);
|
||||
}
|
||||
|
||||
/* Стили для файлов */
|
||||
.file-preview-container {
|
||||
margin: 15px 0;
|
||||
padding: 15px;
|
||||
background: #f0f8ff;
|
||||
border: 2px dashed #b0d4f1;
|
||||
border-radius: 8px;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.file-preview-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
.file-preview-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.file-preview-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 10px;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #dee2e6;
|
||||
gap: 12px;
|
||||
min-height: 40px;
|
||||
}
|
||||
.file-preview-item .file-icon {
|
||||
font-size: 24px;
|
||||
color: var(--accent-color, #007bff);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.file-preview-item .file-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.file-preview-item .file-info-edit {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.file-preview-item .file-name {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.file-preview-item .file-size {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
margin-top: 1px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.file-info-edit .file-name {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.file-info-edit .file-size {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
margin-top: 1px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.file-preview-item .remove-file-btn {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.file-preview-item .remove-file-btn:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
.clear-files-btn {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
.clear-files-btn:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
/* Темная тема для файлов */
|
||||
[data-theme="dark"] .file-preview-container {
|
||||
background: #2a3a4a;
|
||||
border-color: #4a5a6a;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .file-preview-header {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .file-preview-item {
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--border-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .file-preview-item .file-name {
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .file-preview-item .file-size {
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
margin-top: 1px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .file-info-edit .file-name {
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .file-info-edit .file-size {
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
margin-top: 1px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* Темная тема для файлов в режиме редактирования */
|
||||
[data-theme="dark"]
|
||||
.textNote
|
||||
.file-preview-container
|
||||
.file-preview-item
|
||||
.file-name {
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
[data-theme="dark"]
|
||||
.textNote
|
||||
.file-preview-container
|
||||
.file-preview-item
|
||||
.file-size {
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
margin-top: 1px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .note-file-link {
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--border-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .note-file-link:hover {
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .note-file-link .file-name {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .note-file-link .file-size {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Темная тема для изображений */
|
||||
[data-theme="dark"] .image-preview-container {
|
||||
background: #2a3a4a;
|
||||
border-color: #4a5a6a;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .image-preview-header {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .image-preview-item {
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--border-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .image-preview-item .image-info {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .note-images-container {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .note-image-item {
|
||||
border-color: var(--border-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .note-image {
|
||||
border-color: var(--border-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .note-image:hover::after {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
797
server.js
797
server.js
@ -102,6 +102,58 @@ const uploadNoteImages = multer({
|
||||
},
|
||||
});
|
||||
|
||||
// Настройка multer для загрузки файлов заметок
|
||||
const noteFileStorage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
cb(null, uploadsDir);
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
|
||||
cb(
|
||||
null,
|
||||
"note-file-" +
|
||||
req.session.userId +
|
||||
"-" +
|
||||
uniqueSuffix +
|
||||
path.extname(file.originalname)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const uploadNoteFiles = multer({
|
||||
storage: noteFileStorage,
|
||||
limits: { fileSize: 50 * 1024 * 1024 }, // 50MB максимум для файлов заметок
|
||||
fileFilter: function (req, file, cb) {
|
||||
// Разрешенные типы файлов
|
||||
const allowedMimes = [
|
||||
"application/pdf",
|
||||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/vnd.ms-excel",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"text/plain",
|
||||
"application/zip",
|
||||
"application/x-zip-compressed",
|
||||
"application/x-rar-compressed",
|
||||
"application/x-7z-compressed",
|
||||
];
|
||||
|
||||
const allowedExtensions = /pdf|doc|docx|xls|xlsx|txt|zip|rar|7z/;
|
||||
const extname = allowedExtensions.test(
|
||||
path.extname(file.originalname).toLowerCase()
|
||||
);
|
||||
|
||||
if (allowedMimes.includes(file.mimetype) || extname) {
|
||||
return cb(null, true);
|
||||
}
|
||||
cb(
|
||||
new Error(
|
||||
"Только файлы форматов pdf, doc, docx, xls, xlsx, txt, zip, rar, 7z разрешены!"
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// Middleware для безопасности
|
||||
app.use(
|
||||
helmet({
|
||||
@ -217,6 +269,20 @@ function createTables() {
|
||||
)
|
||||
`;
|
||||
|
||||
const createNoteFilesTable = `
|
||||
CREATE TABLE IF NOT EXISTS note_files (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
note_id INTEGER NOT NULL,
|
||||
filename TEXT NOT NULL,
|
||||
original_name TEXT NOT NULL,
|
||||
file_path TEXT NOT NULL,
|
||||
file_size INTEGER NOT NULL,
|
||||
mime_type TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE
|
||||
)
|
||||
`;
|
||||
|
||||
const createUsersTable = `
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@ -247,6 +313,14 @@ function createTables() {
|
||||
}
|
||||
});
|
||||
|
||||
db.run(createNoteFilesTable, (err) => {
|
||||
if (err) {
|
||||
console.error("Ошибка создания таблицы файлов заметок:", err.message);
|
||||
} else {
|
||||
console.log("Таблица файлов заметок готова");
|
||||
}
|
||||
});
|
||||
|
||||
db.run(createUsersTable, (err) => {
|
||||
if (err) {
|
||||
console.error("Ошибка создания таблицы пользователей:", err.message);
|
||||
@ -268,6 +342,7 @@ function createIndexes() {
|
||||
"CREATE INDEX IF NOT EXISTS idx_notes_is_archived ON notes(is_archived)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_notes_pinned_at ON notes(pinned_at)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_note_images_note_id ON note_images(note_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_note_files_note_id ON note_files(note_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_action_logs_user_id ON action_logs(user_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_action_logs_created_at ON action_logs(created_at)",
|
||||
@ -866,9 +941,9 @@ app.get("/api/notes/search", requireApiAuth, (req, res) => {
|
||||
SELECT
|
||||
n.*,
|
||||
CASE
|
||||
WHEN COUNT(ni.id) = 0 THEN '[]'
|
||||
WHEN COUNT(DISTINCT ni.id) = 0 THEN '[]'
|
||||
ELSE json_group_array(
|
||||
json_object(
|
||||
DISTINCT json_object(
|
||||
'id', ni.id,
|
||||
'filename', ni.filename,
|
||||
'original_name', ni.original_name,
|
||||
@ -877,10 +952,25 @@ app.get("/api/notes/search", requireApiAuth, (req, res) => {
|
||||
'mime_type', ni.mime_type,
|
||||
'created_at', ni.created_at
|
||||
)
|
||||
)
|
||||
END as images
|
||||
) FILTER (WHERE ni.id IS NOT NULL)
|
||||
END as images,
|
||||
CASE
|
||||
WHEN COUNT(DISTINCT nf.id) = 0 THEN '[]'
|
||||
ELSE json_group_array(
|
||||
DISTINCT json_object(
|
||||
'id', nf.id,
|
||||
'filename', nf.filename,
|
||||
'original_name', nf.original_name,
|
||||
'file_path', nf.file_path,
|
||||
'file_size', nf.file_size,
|
||||
'mime_type', nf.mime_type,
|
||||
'created_at', nf.created_at
|
||||
)
|
||||
) FILTER (WHERE nf.id IS NOT NULL)
|
||||
END as files
|
||||
FROM notes n
|
||||
LEFT JOIN note_images ni ON n.id = ni.note_id
|
||||
LEFT JOIN note_files nf ON n.id = nf.note_id
|
||||
${whereClause}
|
||||
GROUP BY n.id
|
||||
ORDER BY n.is_pinned DESC, n.pinned_at DESC, n.created_at DESC
|
||||
@ -892,25 +982,26 @@ app.get("/api/notes/search", requireApiAuth, (req, res) => {
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
// Парсим JSON строки изображений
|
||||
const notesWithImages = rows.map((row) => ({
|
||||
// Парсим JSON строки изображений и файлов
|
||||
const notesWithImagesAndFiles = rows.map((row) => ({
|
||||
...row,
|
||||
images: row.images === "[]" ? [] : JSON.parse(row.images),
|
||||
files: row.files === "[]" ? [] : JSON.parse(row.files),
|
||||
}));
|
||||
|
||||
res.json(notesWithImages);
|
||||
res.json(notesWithImagesAndFiles);
|
||||
});
|
||||
});
|
||||
|
||||
// API для получения всех заметок с изображениями (исключая архивные)
|
||||
// API для получения всех заметок с изображениями и файлами (исключая архивные)
|
||||
app.get("/api/notes", requireApiAuth, (req, res) => {
|
||||
const sql = `
|
||||
SELECT
|
||||
n.*,
|
||||
CASE
|
||||
WHEN COUNT(ni.id) = 0 THEN '[]'
|
||||
WHEN COUNT(DISTINCT ni.id) = 0 THEN '[]'
|
||||
ELSE json_group_array(
|
||||
json_object(
|
||||
DISTINCT json_object(
|
||||
'id', ni.id,
|
||||
'filename', ni.filename,
|
||||
'original_name', ni.original_name,
|
||||
@ -919,10 +1010,25 @@ app.get("/api/notes", requireApiAuth, (req, res) => {
|
||||
'mime_type', ni.mime_type,
|
||||
'created_at', ni.created_at
|
||||
)
|
||||
)
|
||||
END as images
|
||||
) FILTER (WHERE ni.id IS NOT NULL)
|
||||
END as images,
|
||||
CASE
|
||||
WHEN COUNT(DISTINCT nf.id) = 0 THEN '[]'
|
||||
ELSE json_group_array(
|
||||
DISTINCT json_object(
|
||||
'id', nf.id,
|
||||
'filename', nf.filename,
|
||||
'original_name', nf.original_name,
|
||||
'file_path', nf.file_path,
|
||||
'file_size', nf.file_size,
|
||||
'mime_type', nf.mime_type,
|
||||
'created_at', nf.created_at
|
||||
)
|
||||
) FILTER (WHERE nf.id IS NOT NULL)
|
||||
END as files
|
||||
FROM notes n
|
||||
LEFT JOIN note_images ni ON n.id = ni.note_id
|
||||
LEFT JOIN note_files nf ON n.id = nf.note_id
|
||||
WHERE n.user_id = ? AND n.is_archived = 0
|
||||
GROUP BY n.id
|
||||
ORDER BY n.is_pinned DESC, n.pinned_at DESC, n.created_at DESC
|
||||
@ -934,13 +1040,14 @@ app.get("/api/notes", requireApiAuth, (req, res) => {
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
// Парсим JSON строки изображений
|
||||
const notesWithImages = rows.map((row) => ({
|
||||
// Парсим JSON строки изображений и файлов
|
||||
const notesWithImagesAndFiles = rows.map((row) => ({
|
||||
...row,
|
||||
images: row.images === "[]" ? [] : JSON.parse(row.images),
|
||||
files: row.files === "[]" ? [] : JSON.parse(row.files),
|
||||
}));
|
||||
|
||||
res.json(notesWithImages);
|
||||
res.json(notesWithImagesAndFiles);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1063,35 +1170,59 @@ app.delete("/api/notes/:id", requireApiAuth, (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Удаляем записи об изображениях из БД
|
||||
const deleteImagesSql = "DELETE FROM note_images WHERE note_id = ?";
|
||||
db.run(deleteImagesSql, [id], (err) => {
|
||||
// Удаляем файлы заметки
|
||||
const getFilesSql = "SELECT file_path FROM note_files WHERE note_id = ?";
|
||||
db.all(getFilesSql, [id], (err, files) => {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления изображений:", err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Удаляем саму заметку
|
||||
const deleteSql = "DELETE FROM notes WHERE id = ?";
|
||||
db.run(deleteSql, id, function (err) {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления заметки:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
if (this.changes === 0) {
|
||||
return res.status(404).json({ error: "Заметка не найдена" });
|
||||
console.error("Ошибка получения файлов:", err.message);
|
||||
} else {
|
||||
// Удаляем файлы
|
||||
files.forEach((file) => {
|
||||
const filePath = path.join(__dirname, "public", file.file_path);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Логируем удаление заметки
|
||||
const clientIP = getClientIP(req);
|
||||
logAction(
|
||||
req.session.userId,
|
||||
"note_delete",
|
||||
`Удалена заметка #${id}`,
|
||||
clientIP
|
||||
);
|
||||
// Удаляем записи об изображениях из БД
|
||||
const deleteImagesSql = "DELETE FROM note_images WHERE note_id = ?";
|
||||
db.run(deleteImagesSql, [id], (err) => {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления изображений:", err.message);
|
||||
}
|
||||
});
|
||||
|
||||
res.json({ message: "Заметка удалена" });
|
||||
// Удаляем записи о файлах из БД
|
||||
const deleteFilesSql = "DELETE FROM note_files WHERE note_id = ?";
|
||||
db.run(deleteFilesSql, [id], (err) => {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления файлов:", err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Удаляем саму заметку
|
||||
const deleteSql = "DELETE FROM notes WHERE id = ?";
|
||||
db.run(deleteSql, id, function (err) {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления заметки:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
if (this.changes === 0) {
|
||||
return res.status(404).json({ error: "Заметка не найдена" });
|
||||
}
|
||||
|
||||
// Логируем удаление заметки
|
||||
const clientIP = getClientIP(req);
|
||||
logAction(
|
||||
req.session.userId,
|
||||
"note_delete",
|
||||
`Удалена заметка #${id}`,
|
||||
clientIP
|
||||
);
|
||||
|
||||
res.json({ message: "Заметка удалена" });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1280,6 +1411,189 @@ app.delete("/api/notes/:noteId/images/:imageId", requireApiAuth, (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// API для загрузки файлов к заметке
|
||||
app.post(
|
||||
"/api/notes/:id/files",
|
||||
requireAuth,
|
||||
uploadNoteFiles.array("files", 10), // Максимум 10 файлов
|
||||
(req, res) => {
|
||||
const { id } = req.params;
|
||||
const files = req.files;
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
return res.status(400).json({ error: "Файлы не загружены" });
|
||||
}
|
||||
|
||||
// Проверяем, что заметка принадлежит текущему пользователю
|
||||
const checkSql = "SELECT user_id FROM notes WHERE id = ?";
|
||||
db.get(checkSql, [id], (err, row) => {
|
||||
if (err) {
|
||||
console.error("Ошибка проверки доступа:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
if (!row) {
|
||||
return res.status(404).json({ error: "Заметка не найдена" });
|
||||
}
|
||||
|
||||
if (row.user_id !== req.session.userId) {
|
||||
return res.status(403).json({ error: "Нет доступа к этой заметке" });
|
||||
}
|
||||
|
||||
// Сохраняем информацию о файлах в БД
|
||||
const insertSql = `
|
||||
INSERT INTO note_files (note_id, filename, original_name, file_path, file_size, mime_type)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const uploadedFiles = [];
|
||||
let completed = 0;
|
||||
|
||||
files.forEach((file) => {
|
||||
const filePath = "/uploads/" + file.filename;
|
||||
const params = [
|
||||
id,
|
||||
file.filename,
|
||||
file.originalname,
|
||||
filePath,
|
||||
file.size,
|
||||
file.mimetype,
|
||||
];
|
||||
|
||||
db.run(insertSql, params, function (err) {
|
||||
if (err) {
|
||||
console.error("Ошибка сохранения файла:", err.message);
|
||||
// Удаляем файл, если не удалось сохранить в БД
|
||||
const fileFullPath = path.join(__dirname, "public", filePath);
|
||||
if (fs.existsSync(fileFullPath)) {
|
||||
fs.unlinkSync(fileFullPath);
|
||||
}
|
||||
} else {
|
||||
uploadedFiles.push({
|
||||
id: this.lastID,
|
||||
filename: file.filename,
|
||||
original_name: file.originalname,
|
||||
file_path: filePath,
|
||||
file_size: file.size,
|
||||
mime_type: file.mimetype,
|
||||
});
|
||||
}
|
||||
|
||||
completed++;
|
||||
if (completed === files.length) {
|
||||
if (uploadedFiles.length === 0) {
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: "Не удалось загрузить файлы" });
|
||||
}
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Загружено ${uploadedFiles.length} файлов`,
|
||||
files: uploadedFiles,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// API для получения файлов заметки
|
||||
app.get("/api/notes/:id/files", requireApiAuth, (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
// Проверяем, что заметка принадлежит текущему пользователю
|
||||
const checkSql = "SELECT user_id FROM notes WHERE id = ?";
|
||||
db.get(checkSql, [id], (err, row) => {
|
||||
if (err) {
|
||||
console.error("Ошибка проверки доступа:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
if (!row) {
|
||||
return res.status(404).json({ error: "Заметка не найдена" });
|
||||
}
|
||||
|
||||
if (row.user_id !== req.session.userId) {
|
||||
return res.status(403).json({ error: "Нет доступа к этой заметке" });
|
||||
}
|
||||
|
||||
// Получаем файлы заметки
|
||||
const getFilesSql = `
|
||||
SELECT id, filename, original_name, file_path, file_size, mime_type, created_at
|
||||
FROM note_files
|
||||
WHERE note_id = ?
|
||||
ORDER BY created_at ASC
|
||||
`;
|
||||
|
||||
db.all(getFilesSql, [id], (err, files) => {
|
||||
if (err) {
|
||||
console.error("Ошибка получения файлов:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
res.json(files);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// API для удаления файла заметки
|
||||
app.delete("/api/notes/:noteId/files/:fileId", requireApiAuth, (req, res) => {
|
||||
const { noteId, fileId } = req.params;
|
||||
|
||||
// Проверяем, что заметка принадлежит текущему пользователю
|
||||
const checkSql = "SELECT user_id FROM notes WHERE id = ?";
|
||||
db.get(checkSql, [noteId], (err, row) => {
|
||||
if (err) {
|
||||
console.error("Ошибка проверки доступа:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
if (!row) {
|
||||
return res.status(404).json({ error: "Заметка не найдена" });
|
||||
}
|
||||
|
||||
if (row.user_id !== req.session.userId) {
|
||||
return res.status(403).json({ error: "Нет доступа к этой заметке" });
|
||||
}
|
||||
|
||||
// Получаем информацию о файле
|
||||
const getFileSql =
|
||||
"SELECT file_path FROM note_files WHERE id = ? AND note_id = ?";
|
||||
db.get(getFileSql, [fileId, noteId], (err, file) => {
|
||||
if (err) {
|
||||
console.error("Ошибка получения файла:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
if (!file) {
|
||||
return res.status(404).json({ error: "Файл не найден" });
|
||||
}
|
||||
|
||||
// Удаляем файл
|
||||
const filePath = path.join(__dirname, "public", file.file_path);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
|
||||
// Удаляем запись из БД
|
||||
const deleteSql = "DELETE FROM note_files WHERE id = ? AND note_id = ?";
|
||||
db.run(deleteSql, [fileId, noteId], function (err) {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления файла:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
if (this.changes === 0) {
|
||||
return res.status(404).json({ error: "Файл не найден" });
|
||||
}
|
||||
|
||||
res.json({ success: true, message: "Файл удален" });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Страница личного кабинета
|
||||
app.get("/profile", requireAuth, (req, res) => {
|
||||
// Получаем цвет пользователя для предотвращения FOUC
|
||||
@ -1648,9 +1962,9 @@ app.get("/api/notes/archived", requireApiAuth, (req, res) => {
|
||||
SELECT
|
||||
n.*,
|
||||
CASE
|
||||
WHEN COUNT(ni.id) = 0 THEN '[]'
|
||||
WHEN COUNT(DISTINCT ni.id) = 0 THEN '[]'
|
||||
ELSE json_group_array(
|
||||
json_object(
|
||||
DISTINCT json_object(
|
||||
'id', ni.id,
|
||||
'filename', ni.filename,
|
||||
'original_name', ni.original_name,
|
||||
@ -1659,10 +1973,25 @@ app.get("/api/notes/archived", requireApiAuth, (req, res) => {
|
||||
'mime_type', ni.mime_type,
|
||||
'created_at', ni.created_at
|
||||
)
|
||||
)
|
||||
END as images
|
||||
) FILTER (WHERE ni.id IS NOT NULL)
|
||||
END as images,
|
||||
CASE
|
||||
WHEN COUNT(DISTINCT nf.id) = 0 THEN '[]'
|
||||
ELSE json_group_array(
|
||||
DISTINCT json_object(
|
||||
'id', nf.id,
|
||||
'filename', nf.filename,
|
||||
'original_name', nf.original_name,
|
||||
'file_path', nf.file_path,
|
||||
'file_size', nf.file_size,
|
||||
'mime_type', nf.mime_type,
|
||||
'created_at', nf.created_at
|
||||
)
|
||||
) FILTER (WHERE nf.id IS NOT NULL)
|
||||
END as files
|
||||
FROM notes n
|
||||
LEFT JOIN note_images ni ON n.id = ni.note_id
|
||||
LEFT JOIN note_files nf ON n.id = nf.note_id
|
||||
WHERE n.user_id = ? AND n.is_archived = 1
|
||||
GROUP BY n.id
|
||||
ORDER BY n.is_pinned DESC, n.pinned_at DESC, n.created_at DESC
|
||||
@ -1674,13 +2003,14 @@ app.get("/api/notes/archived", requireApiAuth, (req, res) => {
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
// Парсим JSON строки изображений
|
||||
const notesWithImages = rows.map((row) => ({
|
||||
// Парсим JSON строки изображений и файлов
|
||||
const notesWithImagesAndFiles = rows.map((row) => ({
|
||||
...row,
|
||||
images: row.images === "[]" ? [] : JSON.parse(row.images),
|
||||
files: row.files === "[]" ? [] : JSON.parse(row.files),
|
||||
}));
|
||||
|
||||
res.json(notesWithImages);
|
||||
res.json(notesWithImagesAndFiles);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1750,37 +2080,65 @@ app.delete("/api/notes/archived/all", requireApiAuth, async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Удаляем записи об изображениях
|
||||
const deleteImagesSql = `DELETE FROM note_images WHERE note_id IN (${placeholders})`;
|
||||
db.run(deleteImagesSql, noteIds, (err) => {
|
||||
// Получаем и удаляем файлы заметок
|
||||
const getFilesSql = `SELECT file_path FROM note_files WHERE note_id IN (${placeholders})`;
|
||||
db.all(getFilesSql, noteIds, (err, files) => {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления изображений:", err.message);
|
||||
console.error("Ошибка получения файлов:", err.message);
|
||||
} else {
|
||||
// Удаляем файлы
|
||||
files.forEach((file) => {
|
||||
const filePath = path.join(__dirname, "public", file.file_path);
|
||||
if (fs.existsSync(filePath)) {
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
} catch (err) {
|
||||
console.error("Ошибка удаления файла:", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Удаляем сами заметки
|
||||
const deleteNotesSql =
|
||||
"DELETE FROM notes WHERE user_id = ? AND is_archived = 1";
|
||||
db.run(deleteNotesSql, [req.session.userId], function (err) {
|
||||
// Удаляем записи об изображениях
|
||||
const deleteImagesSql = `DELETE FROM note_images WHERE note_id IN (${placeholders})`;
|
||||
db.run(deleteImagesSql, noteIds, (err) => {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления заметок:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
console.error("Ошибка удаления изображений:", err.message);
|
||||
}
|
||||
|
||||
const deletedCount = this.changes;
|
||||
// Удаляем записи о файлах
|
||||
const deleteFilesSql = `DELETE FROM note_files WHERE note_id IN (${placeholders})`;
|
||||
db.run(deleteFilesSql, noteIds, (err) => {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления файлов:", err.message);
|
||||
}
|
||||
|
||||
// Логируем действие
|
||||
const clientIP = getClientIP(req);
|
||||
logAction(
|
||||
req.session.userId,
|
||||
"note_delete_permanent",
|
||||
`Удалены все архивные заметки (${deletedCount} шт.)`,
|
||||
clientIP
|
||||
);
|
||||
// Удаляем сами заметки
|
||||
const deleteNotesSql =
|
||||
"DELETE FROM notes WHERE user_id = ? AND is_archived = 1";
|
||||
db.run(deleteNotesSql, [req.session.userId], function (err) {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления заметок:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Удалено ${deletedCount} архивных заметок`,
|
||||
deletedCount,
|
||||
const deletedCount = this.changes;
|
||||
|
||||
// Логируем действие
|
||||
const clientIP = getClientIP(req);
|
||||
logAction(
|
||||
req.session.userId,
|
||||
"note_delete_permanent",
|
||||
`Удалены все архивные заметки (${deletedCount} шт.)`,
|
||||
clientIP
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Удалено ${deletedCount} архивных заметок`,
|
||||
deletedCount,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1831,32 +2189,55 @@ app.delete("/api/notes/archived/:id", requireApiAuth, (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Удаляем записи об изображениях
|
||||
const deleteImagesSql = "DELETE FROM note_images WHERE note_id = ?";
|
||||
db.run(deleteImagesSql, [id], (err) => {
|
||||
// Удаляем файлы заметки
|
||||
const getFilesSql = "SELECT file_path FROM note_files WHERE note_id = ?";
|
||||
db.all(getFilesSql, [id], (err, files) => {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления изображений:", err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Удаляем саму заметку
|
||||
const deleteSql = "DELETE FROM notes WHERE id = ?";
|
||||
db.run(deleteSql, id, function (err) {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления заметки:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
console.error("Ошибка получения файлов:", err.message);
|
||||
} else {
|
||||
files.forEach((file) => {
|
||||
const filePath = path.join(__dirname, "public", file.file_path);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Логируем действие
|
||||
const clientIP = getClientIP(req);
|
||||
logAction(
|
||||
req.session.userId,
|
||||
"note_delete_permanent",
|
||||
`Заметка #${id} окончательно удалена из архива`,
|
||||
clientIP
|
||||
);
|
||||
// Удаляем записи об изображениях
|
||||
const deleteImagesSql = "DELETE FROM note_images WHERE note_id = ?";
|
||||
db.run(deleteImagesSql, [id], (err) => {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления изображений:", err.message);
|
||||
}
|
||||
});
|
||||
|
||||
res.json({ success: true, message: "Заметка удалена окончательно" });
|
||||
// Удаляем записи о файлах
|
||||
const deleteFilesSql = "DELETE FROM note_files WHERE note_id = ?";
|
||||
db.run(deleteFilesSql, [id], (err) => {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления файлов:", err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Удаляем саму заметку
|
||||
const deleteSql = "DELETE FROM notes WHERE id = ?";
|
||||
db.run(deleteSql, id, function (err) {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления заметки:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
// Логируем действие
|
||||
const clientIP = getClientIP(req);
|
||||
logAction(
|
||||
req.session.userId,
|
||||
"note_delete_permanent",
|
||||
`Заметка #${id} окончательно удалена из архива`,
|
||||
clientIP
|
||||
);
|
||||
|
||||
res.json({ success: true, message: "Заметка удалена окончательно" });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -2214,7 +2595,7 @@ app.delete("/api/user/delete-account", requireApiAuth, async (req, res) => {
|
||||
return res.status(401).json({ error: "Неверный пароль" });
|
||||
}
|
||||
|
||||
// Получаем аватарку пользователя перед удалением
|
||||
// Получаем аватарку и все файлы пользователя перед удалением
|
||||
const getAvatarSql = "SELECT avatar FROM users WHERE id = ?";
|
||||
db.get(getAvatarSql, [userId], (err, userData) => {
|
||||
if (err) {
|
||||
@ -2222,90 +2603,180 @@ app.delete("/api/user/delete-account", requireApiAuth, async (req, res) => {
|
||||
return res.status(500).json({ error: "Ошибка получения аватарки" });
|
||||
}
|
||||
|
||||
// Удаляем файл аватарки, если он существует
|
||||
if (userData && userData.avatar) {
|
||||
const avatarPath = path.join(
|
||||
__dirname,
|
||||
"public",
|
||||
"uploads",
|
||||
userData.avatar
|
||||
);
|
||||
fs.unlink(avatarPath, (err) => {
|
||||
if (err && err.code !== "ENOENT") {
|
||||
console.error("Ошибка удаления файла аватарки:", err.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Получаем все изображения заметок пользователя
|
||||
const getImagesSql = `
|
||||
SELECT ni.file_path
|
||||
FROM note_images ni
|
||||
JOIN notes n ON ni.note_id = n.id
|
||||
WHERE n.user_id = ?
|
||||
`;
|
||||
db.all(getImagesSql, [userId], (err, images) => {
|
||||
if (err) {
|
||||
console.error(
|
||||
"Ошибка получения изображений пользователя:",
|
||||
err.message
|
||||
);
|
||||
}
|
||||
|
||||
// Начинаем транзакцию для удаления всех данных пользователя
|
||||
db.serialize(() => {
|
||||
db.run("BEGIN TRANSACTION");
|
||||
|
||||
// Удаляем все изображения заметок пользователя (через JOIN с notes)
|
||||
const deleteImagesSql = `
|
||||
DELETE FROM note_images
|
||||
WHERE note_id IN (SELECT id FROM notes WHERE user_id = ?)
|
||||
// Получаем все файлы заметок пользователя
|
||||
const getFilesSql = `
|
||||
SELECT nf.file_path
|
||||
FROM note_files nf
|
||||
JOIN notes n ON nf.note_id = n.id
|
||||
WHERE n.user_id = ?
|
||||
`;
|
||||
db.run(deleteImagesSql, [userId], (err) => {
|
||||
db.all(getFilesSql, [userId], (err, files) => {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления изображений:", err.message);
|
||||
db.run("ROLLBACK");
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: "Ошибка удаления изображений" });
|
||||
console.error(
|
||||
"Ошибка получения файлов пользователя:",
|
||||
err.message
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Удаляем все заметки пользователя (CASCADE удалит связанные изображения)
|
||||
db.run("DELETE FROM notes WHERE user_id = ?", [userId], (err) => {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления заметок:", err.message);
|
||||
db.run("ROLLBACK");
|
||||
return res.status(500).json({ error: "Ошибка удаления заметок" });
|
||||
}
|
||||
});
|
||||
|
||||
// Удаляем логи пользователя
|
||||
db.run(
|
||||
"DELETE FROM action_logs WHERE user_id = ?",
|
||||
[userId],
|
||||
(err) => {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления логов:", err.message);
|
||||
db.run("ROLLBACK");
|
||||
return res.status(500).json({ error: "Ошибка удаления логов" });
|
||||
// Удаляем файл аватарки
|
||||
if (userData && userData.avatar) {
|
||||
const avatarPath = path.join(
|
||||
__dirname,
|
||||
"public",
|
||||
userData.avatar
|
||||
);
|
||||
if (fs.existsSync(avatarPath)) {
|
||||
try {
|
||||
fs.unlinkSync(avatarPath);
|
||||
} catch (err) {
|
||||
console.error("Ошибка удаления файла аватарки:", err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Удаляем пользователя из базы данных
|
||||
db.run("DELETE FROM users WHERE id = ?", [userId], (err) => {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления пользователя:", err.message);
|
||||
db.run("ROLLBACK");
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: "Ошибка удаления пользователя" });
|
||||
// Удаляем все изображения
|
||||
if (images) {
|
||||
images.forEach((image) => {
|
||||
const imagePath = path.join(
|
||||
__dirname,
|
||||
"public",
|
||||
image.file_path
|
||||
);
|
||||
if (fs.existsSync(imagePath)) {
|
||||
try {
|
||||
fs.unlinkSync(imagePath);
|
||||
} catch (err) {
|
||||
console.error("Ошибка удаления изображения:", err.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Подтверждаем транзакцию
|
||||
db.run("COMMIT", (err) => {
|
||||
if (err) {
|
||||
console.error("Ошибка подтверждения транзакции:", err.message);
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: "Ошибка подтверждения транзакции" });
|
||||
}
|
||||
// Удаляем все файлы
|
||||
if (files) {
|
||||
files.forEach((file) => {
|
||||
const filePath = path.join(__dirname, "public", file.file_path);
|
||||
if (fs.existsSync(filePath)) {
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
} catch (err) {
|
||||
console.error("Ошибка удаления файла:", err.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Уничтожаем сессию
|
||||
req.session.destroy((err) => {
|
||||
// Начинаем транзакцию для удаления всех данных из БД
|
||||
db.serialize(() => {
|
||||
db.run("BEGIN TRANSACTION");
|
||||
|
||||
// Удаляем все изображения заметок пользователя
|
||||
const deleteImagesSql = `
|
||||
DELETE FROM note_images
|
||||
WHERE note_id IN (SELECT id FROM notes WHERE user_id = ?)
|
||||
`;
|
||||
db.run(deleteImagesSql, [userId], (err) => {
|
||||
if (err) {
|
||||
console.error("Ошибка уничтожения сессии:", err.message);
|
||||
console.error(
|
||||
"Ошибка удаления изображений из БД:",
|
||||
err.message
|
||||
);
|
||||
db.run("ROLLBACK");
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: "Ошибка удаления изображений" });
|
||||
}
|
||||
});
|
||||
|
||||
// Удаляем все файлы заметок пользователя
|
||||
const deleteFilesSql = `
|
||||
DELETE FROM note_files
|
||||
WHERE note_id IN (SELECT id FROM notes WHERE user_id = ?)
|
||||
`;
|
||||
db.run(deleteFilesSql, [userId], (err) => {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления файлов из БД:", err.message);
|
||||
db.run("ROLLBACK");
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: "Ошибка удаления файлов" });
|
||||
}
|
||||
});
|
||||
|
||||
// Удаляем все заметки пользователя
|
||||
db.run("DELETE FROM notes WHERE user_id = ?", [userId], (err) => {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления заметок:", err.message);
|
||||
db.run("ROLLBACK");
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: "Ошибка удаления заметок" });
|
||||
}
|
||||
});
|
||||
|
||||
// Удаляем логи пользователя
|
||||
db.run(
|
||||
"DELETE FROM action_logs WHERE user_id = ?",
|
||||
[userId],
|
||||
(err) => {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления логов:", err.message);
|
||||
db.run("ROLLBACK");
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: "Ошибка удаления логов" });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Удаляем пользователя из базы данных
|
||||
db.run("DELETE FROM users WHERE id = ?", [userId], (err) => {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления пользователя:", err.message);
|
||||
db.run("ROLLBACK");
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: "Ошибка удаления пользователя" });
|
||||
}
|
||||
|
||||
res.json({
|
||||
message: "Аккаунт успешно удален",
|
||||
success: true,
|
||||
// Подтверждаем транзакцию
|
||||
db.run("COMMIT", (err) => {
|
||||
if (err) {
|
||||
console.error(
|
||||
"Ошибка подтверждения транзакции:",
|
||||
err.message
|
||||
);
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: "Ошибка подтверждения транзакции" });
|
||||
}
|
||||
|
||||
// Уничтожаем сессию
|
||||
req.session.destroy((err) => {
|
||||
if (err) {
|
||||
console.error("Ошибка уничтожения сессии:", err.message);
|
||||
}
|
||||
|
||||
res.json({
|
||||
message: "Аккаунт успешно удален",
|
||||
success: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user