✨ Добавлена функция удаления аккаунта пользователя
- Реализован новый API-эндпоинт для удаления аккаунта с подтверждением пароля. - Добавлено модальное окно для подтверждения удаления аккаунта на странице профиля. - Обновлены стили и логика для предпросмотра заметок с учетом текущей темы. - Улучшены обработчики событий для кнопки удаления аккаунта и модального окна.
This commit is contained in:
parent
59992e54dd
commit
155f4303d5
110
public/app.js
110
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 =
|
||||
'<p style="color: #999; font-style: italic;">Нет содержимого для предпросмотра</p>';
|
||||
'<p style="color: var(--text-muted); font-style: italic;">Нет содержимого для предпросмотра</p>';
|
||||
}
|
||||
|
||||
// Меняем иконку кнопки
|
||||
@ -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 =
|
||||
'<p style="color: #999; font-style: italic;">Нет содержимого для предпросмотра</p>';
|
||||
'<p style="color: var(--text-muted); font-style: italic;">Нет содержимого для предпросмотра</p>';
|
||||
}
|
||||
|
||||
// Меняем иконку кнопки
|
||||
@ -4068,6 +4169,11 @@ function applyTheme(theme) {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Применяем тему к предпросмотру, если он открыт
|
||||
if (isPreviewMode) {
|
||||
applyThemeToPreview();
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализируем переключатель темы при загрузке страницы
|
||||
|
||||
@ -189,6 +189,61 @@
|
||||
<button id="changePasswordBtn" class="btnSave">
|
||||
Изменить пароль
|
||||
</button>
|
||||
|
||||
<hr class="separator" />
|
||||
|
||||
<button id="deleteAccountBtn" class="btn-danger">
|
||||
<span class="iconify" data-icon="mdi:account-remove"></span>
|
||||
Удалить аккаунт
|
||||
</button>
|
||||
<p style="color: #666; font-size: 14px; margin-bottom: 15px">
|
||||
Удаление аккаунта - это необратимое действие. Все ваши заметки,
|
||||
изображения и данные будут удалены навсегда.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно подтверждения удаления аккаунта -->
|
||||
<div id="deleteAccountModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 style="color: #dc3545">Удаление аккаунта</h3>
|
||||
<span class="modal-close" id="deleteAccountModalClose">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p style="color: #dc3545; font-weight: bold; margin-bottom: 15px">
|
||||
⚠️ ВНИМАНИЕ: Это действие нельзя отменить!
|
||||
</p>
|
||||
<p style="margin-bottom: 20px">
|
||||
Вы действительно хотите удалить свой аккаунт? Все ваши заметки,
|
||||
изображения, настройки и данные будут удалены навсегда.
|
||||
</p>
|
||||
<div style="margin-bottom: 15px">
|
||||
<label
|
||||
for="deleteAccountPassword"
|
||||
style="display: block; margin-bottom: 5px; font-weight: bold"
|
||||
>
|
||||
Введите пароль для подтверждения:
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="deleteAccountPassword"
|
||||
placeholder="Пароль от аккаунта"
|
||||
class="modal-password-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
id="confirmDeleteAccount"
|
||||
class="btn-danger"
|
||||
style="margin-right: 10px"
|
||||
>
|
||||
<span class="iconify" data-icon="mdi:account-remove"></span> Удалить
|
||||
аккаунт
|
||||
</button>
|
||||
<button id="cancelDeleteAccount" class="btn-secondary">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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 =
|
||||
'<span class="iconify" data-icon="mdi:loading"></span> Удаление...';
|
||||
|
||||
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 =
|
||||
'<span class="iconify" data-icon="mdi:account-remove"></span> Удалить аккаунт';
|
||||
}
|
||||
});
|
||||
|
||||
// Обработка 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();
|
||||
});
|
||||
|
||||
292
public/style.css
292
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);
|
||||
|
||||
133
server.js
133
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}`);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user