From 155f4303d510dbc3fe22d448ffd424adb7b6ed7f Mon Sep 17 00:00:00 2001 From: Fovway Date: Tue, 28 Oct 2025 01:03:16 +0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B0=D0=BA=D0=BA=D0=B0=D1=83=D0=BD=D1=82=D0=B0=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Реализован новый API-эндпоинт для удаления аккаунта с подтверждением пароля. - Добавлено модальное окно для подтверждения удаления аккаунта на странице профиля. - Обновлены стили и логика для предпросмотра заметок с учетом текущей темы. - Улучшены обработчики событий для кнопки удаления аккаунта и модального окна. --- public/app.js | 110 ++++++++++++++++- public/profile.html | 55 +++++++++ public/profile.js | 96 +++++++++++++++ public/style.css | 292 ++++++++++++++++++++++++++++++++++++++++++-- server.js | 133 ++++++++++++++++++++ 5 files changed, 677 insertions(+), 9 deletions(-) diff --git a/public/app.js b/public/app.js index 1df069d..a5c85e1 100644 --- a/public/app.js +++ b/public/app.js @@ -1170,13 +1170,16 @@ function togglePreview() { const contentWithTags = makeTagsClickable(htmlContent); notePreviewContent.innerHTML = contentWithTags; + // Применяем текущую тему к предпросмотру + applyThemeToPreview(); + // Инициализируем lazy loading для изображений в превью setTimeout(() => { initLazyLoading(); }, 0); } else { notePreviewContent.innerHTML = - '

Нет содержимого для предпросмотра

'; + '

Нет содержимого для предпросмотра

'; } // Меняем иконку кнопки @@ -1194,6 +1197,98 @@ function togglePreview() { } } +// Функция применения темы к предпросмотру +function applyThemeToPreview() { + if (!notePreviewContainer || notePreviewContainer.style.display === "none") { + return; + } + + const currentTheme = document.documentElement.getAttribute("data-theme"); + + // Применяем тему к контейнеру предпросмотра + if (currentTheme === "dark") { + notePreviewContainer.setAttribute("data-theme", "dark"); + } else { + notePreviewContainer.removeAttribute("data-theme"); + } + + // Обновляем стили для элементов внутри предпросмотра + const previewElements = notePreviewContent.querySelectorAll("*"); + previewElements.forEach((element) => { + // Применяем тему к элементам кода + if (element.tagName === "CODE" || element.tagName === "PRE") { + if (currentTheme === "dark") { + element.style.backgroundColor = "var(--bg-quaternary)"; + element.style.color = "#e6e6e6"; + element.style.border = "1px solid var(--border-primary)"; + } else { + element.style.backgroundColor = "var(--bg-quaternary)"; + element.style.color = "var(--text-primary)"; + element.style.border = "1px solid var(--border-primary)"; + } + } + + // Применяем тему к цитатам + if (element.tagName === "BLOCKQUOTE") { + if (currentTheme === "dark") { + element.style.backgroundColor = "var(--bg-tertiary)"; + element.style.borderLeftColor = "var(--accent-color, #4a9eff)"; + element.style.color = "var(--text-secondary)"; + } else { + element.style.backgroundColor = "var(--bg-tertiary)"; + element.style.borderLeftColor = "var(--accent-color, #007bff)"; + element.style.color = "var(--text-secondary)"; + } + } + }); +} + +// Функция применения темы к предпросмотру в режиме редактирования +function applyThemeToEditPreview(editPreviewContainer, editPreviewContent) { + if (!editPreviewContainer || editPreviewContainer.style.display === "none") { + return; + } + + const currentTheme = document.documentElement.getAttribute("data-theme"); + + // Применяем тему к контейнеру предпросмотра редактирования + if (currentTheme === "dark") { + editPreviewContainer.setAttribute("data-theme", "dark"); + } else { + editPreviewContainer.removeAttribute("data-theme"); + } + + // Обновляем стили для элементов внутри предпросмотра редактирования + const previewElements = editPreviewContent.querySelectorAll("*"); + previewElements.forEach((element) => { + // Применяем тему к элементам кода + if (element.tagName === "CODE" || element.tagName === "PRE") { + if (currentTheme === "dark") { + element.style.backgroundColor = "var(--bg-quaternary)"; + element.style.color = "#e6e6e6"; + element.style.border = "1px solid var(--border-primary)"; + } else { + element.style.backgroundColor = "var(--bg-quaternary)"; + element.style.color = "var(--text-primary)"; + element.style.border = "1px solid var(--border-primary)"; + } + } + + // Применяем тему к цитатам + if (element.tagName === "BLOCKQUOTE") { + if (currentTheme === "dark") { + element.style.backgroundColor = "var(--bg-tertiary)"; + element.style.borderLeftColor = "var(--accent-color, #4a9eff)"; + element.style.color = "var(--text-secondary)"; + } else { + element.style.backgroundColor = "var(--bg-tertiary)"; + element.style.borderLeftColor = "var(--accent-color, #007bff)"; + element.style.color = "var(--text-secondary)"; + } + } + }); +} + // Обработчик выбора файлов imageInput.addEventListener("change", function (event) { const files = Array.from(event.target.files); @@ -2566,13 +2661,19 @@ function addNoteEventListeners() { const contentWithTags = makeTagsClickable(htmlContent); editPreviewContent.innerHTML = contentWithTags; + // Применяем текущую тему к предпросмотру редактирования + applyThemeToEditPreview( + editPreviewContainer, + editPreviewContent + ); + // Инициализируем lazy loading для изображений в превью setTimeout(() => { initLazyLoading(); }, 0); } else { editPreviewContent.innerHTML = - '

Нет содержимого для предпросмотра

'; + '

Нет содержимого для предпросмотра

'; } // Меняем иконку кнопки @@ -4068,6 +4169,11 @@ function applyTheme(theme) { ); } } + + // Применяем тему к предпросмотру, если он открыт + if (isPreviewMode) { + applyThemeToPreview(); + } } // Инициализируем переключатель темы при загрузке страницы diff --git a/public/profile.html b/public/profile.html index 1b3e81e..672d57d 100644 --- a/public/profile.html +++ b/public/profile.html @@ -189,6 +189,61 @@ + +
+ + +

+ Удаление аккаунта - это необратимое действие. Все ваши заметки, + изображения и данные будут удалены навсегда. +

+ + + + + + diff --git a/public/profile.js b/public/profile.js index f778537..820da99 100644 --- a/public/profile.js +++ b/public/profile.js @@ -688,6 +688,99 @@ function setupLogoutHandler() { }); } +// Функция для инициализации обработчика удаления аккаунта +function initDeleteAccountHandler() { + const deleteAccountBtn = document.getElementById("deleteAccountBtn"); + const modal = document.getElementById("deleteAccountModal"); + const closeBtn = document.getElementById("deleteAccountModalClose"); + const cancelBtn = document.getElementById("cancelDeleteAccount"); + const confirmBtn = document.getElementById("confirmDeleteAccount"); + const passwordInput = document.getElementById("deleteAccountPassword"); + + // Открытие модального окна + deleteAccountBtn.addEventListener("click", () => { + // Очищаем поле пароля и открываем модальное окно + passwordInput.value = ""; + modal.style.display = "block"; + passwordInput.focus(); + }); + + // Закрытие модального окна + function closeModal() { + modal.style.display = "none"; + passwordInput.value = ""; + } + + closeBtn.addEventListener("click", closeModal); + cancelBtn.addEventListener("click", closeModal); + + // Закрытие при клике вне модального окна + window.addEventListener("click", (e) => { + if (e.target === modal) { + closeModal(); + } + }); + + // Подтверждение удаления + confirmBtn.addEventListener("click", async () => { + const password = passwordInput.value.trim(); + + if (!password) { + showNotification("Введите пароль", "warning"); + passwordInput.focus(); + return; + } + + // Блокируем кнопку во время выполнения + confirmBtn.disabled = true; + confirmBtn.innerHTML = + ' Удаление...'; + + try { + const response = await fetch("/api/user/delete-account", { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ password }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || "Ошибка удаления аккаунта"); + } + + // Успешное удаление + showNotification("Аккаунт успешно удален", "success"); + + // Очищаем localStorage + localStorage.removeItem("isAuthenticated"); + localStorage.removeItem("username"); + + // Перенаправляем на главную страницу + setTimeout(() => { + window.location.href = "/"; + }, 2000); + } catch (error) { + console.error("Ошибка:", error); + showNotification(error.message || "Ошибка удаления аккаунта", "error"); + } finally { + // Разблокируем кнопку + confirmBtn.disabled = false; + confirmBtn.innerHTML = + ' Удалить аккаунт'; + } + }); + + // Обработка Enter в поле пароля + passwordInput.addEventListener("keypress", (e) => { + if (e.key === "Enter") { + confirmBtn.click(); + } + }); +} + // Загружаем профиль при загрузке страницы document.addEventListener("DOMContentLoaded", function () { // Проверяем аутентификацию при загрузке страницы @@ -699,4 +792,7 @@ document.addEventListener("DOMContentLoaded", function () { // Добавляем обработчик для кнопки выхода setupLogoutHandler(); + + // Инициализируем обработчик удаления аккаунта + initDeleteAccountHandler(); }); diff --git a/public/style.css b/public/style.css index 5338167..2071a60 100644 --- a/public/style.css +++ b/public/style.css @@ -1957,12 +1957,13 @@ textarea:focus { .note-preview-container { margin: 15px 0; padding: 15px; - background: #f8f9fa; - border: 2px solid #007bff; + background: var(--bg-tertiary); + border: 2px solid var(--accent-color, #007bff); border-radius: 8px; min-height: 200px; max-height: 600px; overflow-y: auto; + transition: background-color 0.3s ease, border-color 0.3s ease; } .note-preview-header { @@ -1971,17 +1972,20 @@ textarea:focus { align-items: center; margin-bottom: 15px; padding-bottom: 10px; - border-bottom: 2px solid #dee2e6; + border-bottom: 2px solid var(--border-primary); font-weight: 600; - color: #007bff; + color: var(--accent-color, #007bff); font-size: 16px; + transition: color 0.3s ease, border-color 0.3s ease; } .note-preview-content { - background: white; + background: var(--bg-secondary); + color: var(--text-primary); padding: 15px; border-radius: 6px; min-height: 150px; + transition: background-color 0.3s ease, color 0.3s ease; } .note-preview-content h1, @@ -1992,32 +1996,63 @@ textarea:focus { .note-preview-content h6 { margin-top: 0.5em; margin-bottom: 0.5em; + color: var(--text-primary); + transition: color 0.3s ease; } .note-preview-content p { margin: 0.5em 0; + color: var(--text-primary); + transition: color 0.3s ease; } .note-preview-content ul, .note-preview-content ol { margin: 0.5em 0; padding-left: 2em; + color: var(--text-primary); + transition: color 0.3s ease; +} + +.note-preview-content li { + color: var(--text-primary); + transition: color 0.3s ease; +} + +.note-preview-content a { + color: var(--accent-color, #007bff); + text-decoration: none; + transition: color 0.3s ease; +} + +.note-preview-content a:hover { + text-decoration: underline; } .note-preview-content code { - background: var(--bg-tertiary); + background: var(--bg-quaternary); color: var(--text-primary); padding: 2px 6px; border-radius: 4px; font-family: "Courier New", monospace; + transition: background-color 0.3s ease, color 0.3s ease; } .note-preview-content pre { - background: var(--bg-tertiary); + background: var(--bg-quaternary); color: var(--text-primary); padding: 10px; border-radius: 6px; overflow-x: auto; + border: 1px solid var(--border-primary); + transition: background-color 0.3s ease, color 0.3s ease, + border-color 0.3s ease; +} + +.note-preview-content pre code { + background: transparent; + padding: 0; + border-radius: 0; } .note-preview-content blockquote { @@ -2028,6 +2063,13 @@ textarea:focus { background-color: var(--bg-tertiary); padding: 8px 15px; border-radius: 0 4px 4px 0; + transition: color 0.3s ease, background-color 0.3s ease, + border-color 0.3s ease; +} + +.note-preview-content blockquote p { + color: var(--text-secondary); + margin: 0; } .note-preview-content img { @@ -2035,6 +2077,192 @@ textarea:focus { height: auto; border-radius: 6px; margin: 10px 0; + box-shadow: 0 2px 4px var(--shadow-light); + transition: box-shadow 0.3s ease; +} + +.note-preview-content table { + width: 100%; + border-collapse: collapse; + margin: 10px 0; + background: var(--bg-secondary); + border-radius: 6px; + overflow: hidden; + box-shadow: 0 1px 3px var(--shadow-light); + transition: background-color 0.3s ease, box-shadow 0.3s ease; +} + +.note-preview-content th, +.note-preview-content td { + padding: 8px 12px; + text-align: left; + border-bottom: 1px solid var(--border-primary); + color: var(--text-primary); + transition: color 0.3s ease, border-color 0.3s ease; +} + +.note-preview-content th { + background: var(--bg-tertiary); + font-weight: bold; + color: var(--text-primary); + transition: background-color 0.3s ease, color 0.3s ease; +} + +.note-preview-content tr:hover { + background: var(--bg-quaternary); + transition: background-color 0.3s ease; +} + +.note-preview-content hr { + border: none; + height: 1px; + background: var(--border-primary); + margin: 20px 0; + transition: background-color 0.3s ease; +} + +/* Специальные стили для темной темы в предпросмотре */ +[data-theme="dark"] .note-preview-container { + background: var(--bg-tertiary); + border-color: var(--accent-color, #4a9eff); +} + +[data-theme="dark"] .note-preview-header { + color: var(--accent-color, #4a9eff); + border-bottom-color: var(--border-primary); +} + +[data-theme="dark"] .note-preview-content { + background: var(--bg-secondary); + color: var(--text-primary); +} + +[data-theme="dark"] .note-preview-content code { + background: var(--bg-quaternary); + color: #e6e6e6; + border: 1px solid var(--border-primary); +} + +[data-theme="dark"] .note-preview-content pre { + background: var(--bg-quaternary); + color: #e6e6e6; + border: 1px solid var(--border-primary); +} + +[data-theme="dark"] .note-preview-content pre code { + background: transparent; + border: none; + color: #e6e6e6; +} + +[data-theme="dark"] .note-preview-content blockquote { + background-color: var(--bg-tertiary); + border-left-color: var(--accent-color, #4a9eff); + color: var(--text-secondary); +} + +[data-theme="dark"] .note-preview-content table { + background: var(--bg-secondary); + box-shadow: 0 1px 3px var(--shadow-light); +} + +[data-theme="dark"] .note-preview-content th { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +[data-theme="dark"] .note-preview-content th, +[data-theme="dark"] .note-preview-content td { + border-bottom-color: var(--border-primary); + color: var(--text-primary); +} + +[data-theme="dark"] .note-preview-content tr:hover { + background: var(--bg-quaternary); +} + +[data-theme="dark"] .note-preview-content img { + box-shadow: 0 2px 4px var(--shadow-light); +} + +/* Стили для чекбоксов в предпросмотре */ +.note-preview-content input[type="checkbox"] { + cursor: pointer; + margin-right: 8px; + width: 18px; + height: 18px; + accent-color: var(--accent-color, #007bff); + vertical-align: middle; + position: relative; + top: -1px; +} + +/* Стили для элементов списка с чекбоксами в предпросмотре */ +.note-preview-content .task-list-item { + list-style-type: none; + margin-left: -20px; + padding: 0; + line-height: 1.5; + transition: all 0.3s ease; +} + +.note-preview-content .task-list-item:has(input[type="checkbox"]:checked) { + opacity: 0.65; +} + +.note-preview-content .task-list-item input[type="checkbox"]:checked ~ * { + text-decoration: line-through; + color: var(--text-muted); +} + +.note-preview-content .task-list-item:hover { + background-color: rgba(0, 123, 255, 0.05); + border-radius: 4px; + padding-left: 4px; + margin-left: -24px; +} + +/* Альтернативный вариант для перечеркивания всего текста в элементе */ +.note-preview-content input[type="checkbox"]:checked { + text-decoration: none; +} + +.note-preview-content input[type="checkbox"]:checked + * { + text-decoration: line-through; + color: var(--text-muted); +} + +/* Если marked.js не добавляет класс task-list-item, используем :has селектор */ +.note-preview-content li:has(input[type="checkbox"]) { + list-style-type: none; + margin-left: -20px; + padding: 0; + line-height: 1.5; + transition: all 0.3s ease; +} + +.note-preview-content li:has(input[type="checkbox"]:checked) { + opacity: 0.65; +} + +.note-preview-content + li:has(input[type="checkbox"]) + input[type="checkbox"]:checked { + text-decoration: none; +} + +.note-preview-content li:has(input[type="checkbox"]:checked) label, +.note-preview-content li:has(input[type="checkbox"]:checked) span, +.note-preview-content li:has(input[type="checkbox"]:checked) p { + text-decoration: line-through; + color: var(--text-muted); +} + +.note-preview-content li:has(input[type="checkbox"]):hover { + background-color: rgba(0, 123, 255, 0.05); + border-radius: 4px; + padding-left: 4px; + margin-left: -24px; } .clear-images-btn { @@ -2621,6 +2849,56 @@ textarea:focus { opacity: 0.6; } +/* Стили для опасной зоны */ +.danger-zone { + background-color: var(--bg-secondary); + border: 2px solid #dc3545; + border-radius: 8px; + padding: 20px; + margin-top: 20px; + position: relative; +} + +.danger-zone::before { + content: "⚠️"; + position: absolute; + top: -10px; + left: 20px; + background-color: var(--bg-primary); + padding: 0 10px; + font-size: 16px; +} + +.danger-zone h3 { + color: #dc3545; + margin-top: 0; + margin-bottom: 15px; + font-size: 18px; + font-weight: bold; +} + +.danger-zone p { + color: var(--text-secondary); + font-size: 14px; + line-height: 1.5; + margin-bottom: 15px; +} + +.danger-zone .btn-danger { + background-color: #dc3545; + border-color: #dc3545; + font-weight: 600; + padding: 12px 24px; + font-size: 15px; +} + +.danger-zone .btn-danger:hover { + background-color: #c82333; + border-color: #bd2130; + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(220, 53, 69, 0.3); +} + .btn-secondary { background-color: var(--bg-tertiary); color: var(--text-primary); diff --git a/server.js b/server.js index 4baf5ad..29b5ab6 100644 --- a/server.js +++ b/server.js @@ -2109,6 +2109,139 @@ app.post("/logout", (req, res) => { }); }); +// API для удаления аккаунта +app.delete("/api/user/delete-account", requireApiAuth, async (req, res) => { + const { password } = req.body; + const userId = req.session.userId; + + if (!password) { + return res.status(400).json({ error: "Пароль обязателен" }); + } + + try { + // Получаем пользователя и проверяем пароль + const getUserSql = "SELECT id, password FROM users WHERE id = ?"; + db.get(getUserSql, [userId], async (err, user) => { + if (err) { + console.error("Ошибка получения пользователя:", err.message); + return res.status(500).json({ error: "Ошибка сервера" }); + } + + if (!user) { + return res.status(404).json({ error: "Пользователь не найден" }); + } + + // Проверяем пароль + const isPasswordValid = await bcrypt.compare(password, user.password); + if (!isPasswordValid) { + return res.status(401).json({ error: "Неверный пароль" }); + } + + // Получаем аватарку пользователя перед удалением + const getAvatarSql = "SELECT avatar FROM users WHERE id = ?"; + db.get(getAvatarSql, [userId], (err, userData) => { + if (err) { + console.error("Ошибка получения аватарки:", err.message); + 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); + } + }); + } + + // Начинаем транзакцию для удаления всех данных пользователя + 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 = ?) + `; + db.run(deleteImagesSql, [userId], (err) => { + if (err) { + console.error("Ошибка удаления изображений:", err.message); + db.run("ROLLBACK"); + return res + .status(500) + .json({ error: "Ошибка удаления изображений" }); + } + }); + + // Удаляем все заметки пользователя (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: "Ошибка удаления логов" }); + } + } + ); + + // Удаляем пользователя из базы данных + db.run("DELETE FROM users WHERE id = ?", [userId], (err) => { + if (err) { + console.error("Ошибка удаления пользователя:", err.message); + db.run("ROLLBACK"); + return res + .status(500) + .json({ error: "Ошибка удаления пользователя" }); + } + + // Подтверждаем транзакцию + 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, + }); + }); + }); + }); + }); + }); + }); + } catch (error) { + console.error("Ошибка удаления аккаунта:", error); + res.status(500).json({ error: "Ошибка сервера" }); + } +}); + // Запуск сервера app.listen(PORT, () => { console.log(`🚀 Сервер запущен на порту ${PORT}`);