@@ -258,12 +266,26 @@
+
-
+ Настройки - NoteJS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Загрузка... ';
+ }
+
+ try {
+ let url = `/api/logs?limit=${logsLimit}&offset=${logsOffset}`;
+ if (filterValue) {
+ url += `&action_type=${filterValue}`;
+ }
+
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error("Ошибка загрузки логов");
+ }
+
+ const logs = await response.json();
+
+ if (reset) {
+ tbody.innerHTML = "";
+ }
+
+ if (logs.length === 0 && logsOffset === 0) {
+ tbody.innerHTML =
+ 'Логов пока нет ';
+ loadMoreContainer.style.display = "none";
+ return;
+ }
+
+ if (logs.length < logsLimit) {
+ hasMoreLogs = false;
+ }
+
+ logs.forEach((log) => {
+ const row = document.createElement("tr");
+
+ // Форматируем дату
+ const created = new Date(log.created_at.replace(" ", "T") + "Z");
+ const dateStr = new Intl.DateTimeFormat("ru-RU", {
+ day: "2-digit",
+ month: "2-digit",
+ year: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ }).format(created);
+
+ // Форматируем тип действия
+ const actionTypes = {
+ login: "Вход",
+ logout: "Выход",
+ register: "Регистрация",
+ note_create: "Создание заметки",
+ note_update: "Редактирование",
+ note_delete: "Удаление",
+ note_pin: "Закрепление",
+ note_archive: "Архивирование",
+ note_unarchive: "Восстановление",
+ note_delete_permanent: "Окончательное удаление",
+ profile_update: "Обновление профиля",
+ };
+
+ const actionText = actionTypes[log.action_type] || log.action_type;
+
+ row.innerHTML = `
+ ${dateStr}
+ ${actionText}
+ ${log.details || "-"}
+ ${log.ip_address || "-"}
+ `;
+
+ tbody.appendChild(row);
+ });
+
+ logsOffset += logs.length;
+
+ if (hasMoreLogs && logs.length > 0) {
+ loadMoreContainer.style.display = "block";
+ } else {
+ loadMoreContainer.style.display = "none";
+ }
+ } catch (error) {
+ console.error("Ошибка загрузки логов:", error);
+ if (reset) {
+ tbody.innerHTML =
+ 'Ошибка загрузки логов ';
+ }
+ }
+}
+
+// Инициализация при загрузке страницы
+document.addEventListener("DOMContentLoaded", async function () {
+ // Проверяем аутентификацию
+ const isAuth = await checkAuthentication();
+ if (!isAuth) return;
+
+ // Загружаем информацию о пользователе
+ await loadUserInfo();
+
+ // Инициализируем табы
+ initTabs();
+
+ // Загружаем архивные заметки по умолчанию
+ loadArchivedNotes();
+
+ // Обработчик фильтра логов
+ document.getElementById("logTypeFilter").addEventListener("change", () => {
+ loadLogs(true);
+ });
+
+ // Обработчик кнопки обновления логов
+ document.getElementById("refreshLogs").addEventListener("click", () => {
+ loadLogs(true);
+ });
+
+ // Обработчик кнопки "Загрузить еще"
+ document.getElementById("loadMoreLogsBtn").addEventListener("click", () => {
+ loadLogs(false);
+ });
+});
+
+
diff --git a/public/style.css b/public/style.css
index cc52199..63f2ff7 100644
--- a/public/style.css
+++ b/public/style.css
@@ -247,6 +247,70 @@ header {
font-weight: 500;
}
+/* Мини-аватарка в хедере */
+.user-avatar-mini {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ overflow: hidden;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ border: 2px solid var(--accent-color, #007bff);
+ flex-shrink: 0;
+}
+
+.user-avatar-mini:hover {
+ transform: scale(1.05);
+ box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.2);
+}
+
+.user-avatar-mini img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.user-avatar-placeholder-mini {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #e0e0e0;
+ color: #999;
+ font-size: 24px;
+}
+
+/* Иконка настроек */
+.settings-icon-btn {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ border: 2px solid #ddd;
+ background: white;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.3s ease;
+ flex-shrink: 0;
+}
+
+.settings-icon-btn:hover {
+ background: var(--accent-color, #007bff);
+ border-color: var(--accent-color, #007bff);
+ color: white;
+ transform: rotate(45deg);
+}
+
+.settings-icon-btn .iconify {
+ font-size: 20px;
+ color: #666;
+ transition: color 0.3s ease;
+}
+
+.settings-icon-btn:hover .iconify {
+ color: white;
+}
+
.logout-btn {
padding: 8px 16px;
cursor: pointer;
@@ -376,25 +440,67 @@ textarea:focus {
.date {
font-size: 11px;
color: grey;
- white-space: nowrap;
display: flex;
align-items: center;
+ justify-content: space-between;
gap: 10px;
- flex-wrap: nowrap;
+ flex-wrap: wrap;
}
.date .date-text {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.note-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
}
.notesHeaderBtn {
- display: inline-block;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
cursor: pointer;
color: black;
font-weight: bold;
white-space: nowrap;
+ padding: 4px 8px;
+ border-radius: 4px;
+ transition: all 0.2s ease;
+ font-size: 11px;
+}
+
+.notesHeaderBtn:hover {
+ background: rgba(0, 0, 0, 0.05);
+}
+
+#pinBtn:hover {
+ background: #fff3cd;
+ color: #856404;
+}
+
+#archiveBtn:hover {
+ background: #e2e3e5;
+ color: #383d41;
+}
+
+#editBtn:hover {
+ background: #d1ecf1;
+ color: #0c5460;
+}
+
+#deleteBtn:hover {
+ background: #f8d7da;
+ color: #721c24;
}
.textNote {
@@ -662,6 +768,56 @@ textarea:focus {
background-color: #e0e0e0;
}
+/* Выпадающее меню заголовков */
+.header-dropdown {
+ position: relative;
+ display: inline-block;
+}
+
+.header-dropdown-menu {
+ display: none;
+ position: absolute;
+ top: 100%;
+ left: 0;
+ background: white;
+ border: 1px solid #ddd;
+ border-radius: 5px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+ z-index: 100;
+ margin-top: 2px;
+ min-width: 80px;
+}
+
+.header-dropdown-menu.show {
+ display: block;
+}
+
+.header-dropdown-menu button {
+ display: block;
+ width: 100%;
+ padding: 8px 16px;
+ border: none;
+ background: none;
+ text-align: left;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 500;
+ color: #333;
+ transition: background 0.2s ease;
+}
+
+.header-dropdown-menu button:hover {
+ background: #f0f0f0;
+}
+
+.header-dropdown-menu button:first-child {
+ border-radius: 5px 5px 0 0;
+}
+
+.header-dropdown-menu button:last-child {
+ border-radius: 0 0 5px 5px;
+}
+
.footer {
text-align: center;
font-size: 12px;
@@ -1710,3 +1866,315 @@ textarea:focus {
border-radius: 5px;
cursor: pointer;
}
+
+/* Стили для страницы настроек */
+.settings-tabs {
+ display: flex;
+ gap: 10px;
+ margin: 20px 0;
+ border-bottom: 2px solid #e0e0e0;
+ flex-wrap: wrap;
+}
+
+.settings-tab {
+ padding: 10px 20px;
+ background: none;
+ border: none;
+ border-bottom: 3px solid transparent;
+ cursor: pointer;
+ font-size: 16px;
+ color: #666;
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.settings-tab:hover {
+ color: var(--accent-color, #007bff);
+ background: rgba(0, 123, 255, 0.05);
+}
+
+.settings-tab.active {
+ color: var(--accent-color, #007bff);
+ border-bottom-color: var(--accent-color, #007bff);
+ font-weight: bold;
+}
+
+.settings-content {
+ margin-top: 20px;
+}
+
+.tab-content {
+ display: none;
+}
+
+.tab-content.active {
+ display: block;
+}
+
+/* Архивные заметки */
+.archived-notes-list {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+}
+
+.archived-note-item {
+ background: #f8f9fa;
+ border: 1px solid #dee2e6;
+ border-radius: 8px;
+ padding: 15px;
+ transition: all 0.3s ease;
+}
+
+.archived-note-item:hover {
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.archived-note-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 10px;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+.archived-note-date {
+ font-size: 12px;
+ color: #999;
+}
+
+.archived-note-actions {
+ display: flex;
+ gap: 8px;
+}
+
+.btn-restore,
+.btn-delete-permanent {
+ padding: 6px 12px;
+ border: 1px solid #ddd;
+ border-radius: 5px;
+ background: white;
+ cursor: pointer;
+ font-size: 13px;
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.btn-restore {
+ color: var(--accent-color, #007bff);
+ border-color: var(--accent-color, #007bff);
+}
+
+.btn-restore:hover {
+ background: var(--accent-color, #007bff);
+ color: white;
+}
+
+.btn-delete-permanent {
+ color: #dc3545;
+ border-color: #dc3545;
+}
+
+.btn-delete-permanent:hover {
+ background: #dc3545;
+ color: white;
+}
+
+.archived-note-content {
+ font-size: 14px;
+ color: #333;
+ line-height: 1.5;
+ max-height: 100px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+}
+
+.archived-note-images {
+ margin-top: 10px;
+ font-size: 12px;
+ color: #666;
+ font-style: italic;
+}
+
+/* История действий */
+.logs-filters {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 20px;
+ flex-wrap: wrap;
+}
+
+.log-filter-select {
+ flex: 1;
+ min-width: 200px;
+ padding: 8px 12px;
+ border: 1px solid #ddd;
+ border-radius: 5px;
+ font-size: 14px;
+}
+
+.log-filter-select:focus {
+ outline: none;
+ border-color: var(--accent-color, #007bff);
+}
+
+.logs-table-container {
+ overflow-x: auto;
+ margin-bottom: 20px;
+}
+
+.logs-table {
+ width: 100%;
+ border-collapse: collapse;
+ background: white;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.logs-table thead {
+ background: #f8f9fa;
+}
+
+.logs-table th {
+ padding: 12px;
+ text-align: left;
+ font-weight: bold;
+ color: #333;
+ border-bottom: 2px solid #dee2e6;
+ font-size: 14px;
+}
+
+.logs-table td {
+ padding: 12px;
+ border-bottom: 1px solid #dee2e6;
+ font-size: 13px;
+ color: #666;
+}
+
+.logs-table tbody tr:hover {
+ background: #f8f9fa;
+}
+
+.log-action-badge {
+ display: inline-block;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 500;
+ white-space: nowrap;
+}
+
+.log-action-login,
+.log-action-register {
+ background: #d4edda;
+ color: #155724;
+}
+
+.log-action-logout {
+ background: #f8d7da;
+ color: #721c24;
+}
+
+.log-action-note_create {
+ background: #d1ecf1;
+ color: #0c5460;
+}
+
+.log-action-note_update {
+ background: #fff3cd;
+ color: #856404;
+}
+
+.log-action-note_delete,
+.log-action-note_delete_permanent {
+ background: #f8d7da;
+ color: #721c24;
+}
+
+.log-action-note_pin {
+ background: #e7e7ff;
+ color: #4a4aff;
+}
+
+.log-action-note_archive {
+ background: #e2e3e5;
+ color: #383d41;
+}
+
+.log-action-note_unarchive {
+ background: #d4edda;
+ color: #155724;
+}
+
+.log-action-profile_update {
+ background: #fce4ec;
+ color: #c2185b;
+}
+
+.load-more-container {
+ text-align: center;
+ margin: 20px 0;
+}
+
+/* Стили для закрепленных заметок */
+.note-pinned {
+ border-left: 4px solid #ffc107;
+ background: #fffbf0;
+}
+
+.pin-indicator {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ color: #ffc107;
+ font-size: 12px;
+ font-weight: bold;
+ margin-left: 10px;
+}
+
+/* Адаптивность для настроек */
+@media (max-width: 768px) {
+ .settings-tabs {
+ flex-direction: column;
+ }
+
+ .settings-tab {
+ width: 100%;
+ justify-content: center;
+ }
+
+ .archived-note-header {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .archived-note-actions {
+ width: 100%;
+ justify-content: flex-start;
+ }
+
+ .logs-table {
+ font-size: 12px;
+ }
+
+ .logs-table th,
+ .logs-table td {
+ padding: 8px;
+ }
+
+ .logs-filters {
+ flex-direction: column;
+ }
+
+ .log-filter-select {
+ width: 100%;
+ }
+}
diff --git a/server.js b/server.js
index 1f61a3e..1914f1c 100644
--- a/server.js
+++ b/server.js
@@ -258,8 +258,12 @@ function createIndexes() {
"CREATE INDEX IF NOT EXISTS idx_notes_created_at ON notes(created_at)",
"CREATE INDEX IF NOT EXISTS idx_notes_updated_at ON notes(updated_at)",
"CREATE INDEX IF NOT EXISTS idx_notes_date ON notes(date)",
+ "CREATE INDEX IF NOT EXISTS idx_notes_is_pinned ON notes(is_pinned)",
+ "CREATE INDEX IF NOT EXISTS idx_notes_is_archived ON notes(is_archived)",
"CREATE INDEX IF NOT EXISTS idx_note_images_note_id ON note_images(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)",
];
indexes.forEach((indexSql, i) => {
@@ -273,8 +277,54 @@ function createIndexes() {
});
}
+// Функция для логирования действий пользователя
+function logAction(userId, actionType, details, ipAddress) {
+ const sql = `
+ INSERT INTO action_logs (user_id, action_type, details, ip_address)
+ VALUES (?, ?, ?, ?)
+ `;
+ db.run(sql, [userId, actionType, details, ipAddress], (err) => {
+ if (err) {
+ console.error("Ошибка логирования действия:", err.message);
+ }
+ });
+}
+
+// Функция для получения IP-адреса клиента
+function getClientIP(req) {
+ return (
+ req.headers["x-forwarded-for"]?.split(",")[0].trim() ||
+ req.headers["x-real-ip"] ||
+ req.connection?.remoteAddress ||
+ req.socket?.remoteAddress ||
+ req.connection?.socket?.remoteAddress ||
+ "unknown"
+ );
+}
+
// Миграции базы данных
function runMigrations() {
+ // Создаем таблицу логов действий, если её нет
+ const createLogsTable = `
+ CREATE TABLE IF NOT EXISTS action_logs (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ action_type TEXT NOT NULL,
+ details TEXT,
+ ip_address TEXT,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+ )
+ `;
+
+ db.run(createLogsTable, (err) => {
+ if (err) {
+ console.error("Ошибка создания таблицы логов:", err.message);
+ } else {
+ console.log("Таблица action_logs готова");
+ }
+ });
+
// Проверяем существование колонки accent_color и добавляем её если нужно
db.all("PRAGMA table_info(users)", (err, columns) => {
if (err) {
@@ -300,7 +350,7 @@ function runMigrations() {
}
});
- // Проверяем существование колонки updated_at в таблице notes и добавляем её если нужно
+ // Проверяем существование колонок в таблице notes и добавляем их если нужно
db.all("PRAGMA table_info(notes)", (err, columns) => {
if (err) {
console.error("Ошибка проверки структуры таблицы notes:", err.message);
@@ -308,13 +358,16 @@ function runMigrations() {
}
const hasUpdatedAt = columns.some((col) => col.name === "updated_at");
+ const hasPinned = columns.some((col) => col.name === "is_pinned");
+ const hasArchived = columns.some((col) => col.name === "is_archived");
+
+ // Добавляем updated_at если нужно
if (!hasUpdatedAt) {
db.run("ALTER TABLE notes ADD COLUMN updated_at DATETIME", (err) => {
if (err) {
console.error("Ошибка добавления колонки updated_at:", err.message);
} else {
console.log("Колонка updated_at добавлена в таблицу notes");
- // Устанавливаем updated_at = created_at для существующих записей
db.run(
"UPDATE notes SET updated_at = created_at WHERE updated_at IS NULL",
(err) => {
@@ -323,20 +376,50 @@ function runMigrations() {
"Ошибка обновления updated_at для существующих записей:",
err.message
);
- } else {
- console.log(
- "Колонка updated_at обновлена для существующих записей"
- );
- // Создаем индексы после добавления колонки
- createIndexes();
}
}
);
}
});
- } else {
- // Если колонка уже существует, просто создаем индексы
+ }
+
+ // Добавляем is_pinned если нужно
+ if (!hasPinned) {
+ db.run(
+ "ALTER TABLE notes ADD COLUMN is_pinned INTEGER DEFAULT 0",
+ (err) => {
+ if (err) {
+ console.error("Ошибка добавления колонки is_pinned:", err.message);
+ } else {
+ console.log("Колонка is_pinned добавлена в таблицу notes");
+ }
+ }
+ );
+ }
+
+ // Добавляем is_archived если нужно
+ if (!hasArchived) {
+ db.run(
+ "ALTER TABLE notes ADD COLUMN is_archived INTEGER DEFAULT 0",
+ (err) => {
+ if (err) {
+ console.error(
+ "Ошибка добавления колонки is_archived:",
+ err.message
+ );
+ } else {
+ console.log("Колонка is_archived добавлена в таблицу notes");
+ }
+ }
+ );
+ }
+
+ // Создаем индексы после всех изменений
+ if (hasUpdatedAt && hasPinned && hasArchived) {
createIndexes();
+ } else {
+ // Задержка для завершения миграций
+ setTimeout(createIndexes, 1000);
}
});
}
@@ -412,6 +495,11 @@ app.post("/api/register", async (req, res) => {
req.session.userId = this.lastID;
req.session.username = username;
req.session.authenticated = true;
+
+ // Логируем регистрацию
+ const clientIP = getClientIP(req);
+ logAction(this.lastID, "register", `Регистрация нового пользователя`, clientIP);
+
res.json({ success: true, message: "Регистрация успешна" });
});
} catch (err) {
@@ -450,6 +538,11 @@ app.post("/api/login", async (req, res) => {
req.session.userId = user.id;
req.session.username = user.username;
req.session.authenticated = true;
+
+ // Логируем вход
+ const clientIP = getClientIP(req);
+ logAction(user.id, "login", `Вход в систему`, clientIP);
+
res.json({ success: true, message: "Вход успешен" });
} catch (err) {
console.error("Ошибка при сравнении паролей:", err);
@@ -609,7 +702,7 @@ app.get("/api/notes/search", requireAuth, (req, res) => {
});
});
-// API для получения всех заметок с изображениями
+// API для получения всех заметок с изображениями (исключая архивные)
app.get("/api/notes", requireAuth, (req, res) => {
const sql = `
SELECT
@@ -630,9 +723,9 @@ app.get("/api/notes", requireAuth, (req, res) => {
END as images
FROM notes n
LEFT JOIN note_images ni ON n.id = ni.note_id
- WHERE n.user_id = ?
+ WHERE n.user_id = ? AND n.is_archived = 0
GROUP BY n.id
- ORDER BY n.created_at DESC
+ ORDER BY n.is_pinned DESC, n.created_at DESC
`;
db.all(sql, [req.session.userId], (err, rows) => {
@@ -668,7 +761,13 @@ app.post("/api/notes", requireAuth, (req, res) => {
console.error("Ошибка создания заметки:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
- res.json({ id: this.lastID, content, date, time });
+
+ // Логируем создание заметки
+ const clientIP = getClientIP(req);
+ const noteId = this.lastID;
+ logAction(req.session.userId, "note_create", `Создана заметка #${noteId}`, clientIP);
+
+ res.json({ id: noteId, content, date, time });
});
});
@@ -709,6 +808,11 @@ app.put("/api/notes/:id", requireAuth, (req, res) => {
if (this.changes === 0) {
return res.status(404).json({ error: "Заметка не найдена" });
}
+
+ // Логируем обновление заметки
+ const clientIP = getClientIP(req);
+ logAction(req.session.userId, "note_update", `Обновлена заметка #${id}`, clientIP);
+
res.json({ id, content, date: row.date, time: row.time });
});
});
@@ -767,6 +871,11 @@ app.delete("/api/notes/:id", requireAuth, (req, res) => {
if (this.changes === 0) {
return res.status(404).json({ error: "Заметка не найдена" });
}
+
+ // Логируем удаление заметки
+ const clientIP = getClientIP(req);
+ logAction(req.session.userId, "note_delete", `Удалена заметка #${id}`, clientIP);
+
res.json({ message: "Заметка удалена" });
});
});
@@ -1091,6 +1200,16 @@ app.put("/api/user/profile", requireAuth, async (req, res) => {
req.session.username = username;
}
+ // Логируем обновление профиля
+ const clientIP = getClientIP(req);
+ const changes = [];
+ if (username && username !== user.username) changes.push("логин");
+ if (email !== undefined) changes.push("email");
+ if (accent_color !== undefined) changes.push("цвет темы");
+ if (newPassword) changes.push("пароль");
+ const details = `Обновлен профиль: ${changes.join(", ")}`;
+ logAction(userId, "profile_update", details, clientIP);
+
res.json({ success: true, message: "Профиль успешно обновлен" });
});
});
@@ -1177,8 +1296,303 @@ app.delete("/api/user/avatar", requireAuth, (req, res) => {
});
});
+// API для закрепления заметки
+app.put("/api/notes/:id/pin", requireAuth, (req, res) => {
+ const { id } = req.params;
+
+ // Проверяем, что заметка принадлежит текущему пользователю
+ const checkSql = "SELECT user_id, is_pinned 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 newPinState = row.is_pinned ? 0 : 1;
+ const updateSql = "UPDATE notes SET is_pinned = ? WHERE id = ?";
+
+ db.run(updateSql, [newPinState, id], function (err) {
+ if (err) {
+ console.error("Ошибка изменения закрепления:", err.message);
+ return res.status(500).json({ error: "Ошибка сервера" });
+ }
+
+ // Логируем действие
+ const clientIP = getClientIP(req);
+ const action = newPinState ? "закреплена" : "откреплена";
+ logAction(req.session.userId, "note_pin", `Заметка #${id} ${action}`, clientIP);
+
+ res.json({ success: true, is_pinned: newPinState });
+ });
+ });
+});
+
+// API для архивирования заметки
+app.put("/api/notes/:id/archive", requireAuth, (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 updateSql = "UPDATE notes SET is_archived = 1, is_pinned = 0 WHERE id = ?";
+
+ db.run(updateSql, [id], function (err) {
+ if (err) {
+ console.error("Ошибка архивирования:", err.message);
+ return res.status(500).json({ error: "Ошибка сервера" });
+ }
+
+ // Логируем действие
+ const clientIP = getClientIP(req);
+ logAction(req.session.userId, "note_archive", `Заметка #${id} архивирована`, clientIP);
+
+ res.json({ success: true, message: "Заметка архивирована" });
+ });
+ });
+});
+
+// API для восстановления заметки из архива
+app.put("/api/notes/:id/unarchive", requireAuth, (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 updateSql = "UPDATE notes SET is_archived = 0 WHERE id = ?";
+
+ db.run(updateSql, [id], function (err) {
+ if (err) {
+ console.error("Ошибка восстановления:", err.message);
+ return res.status(500).json({ error: "Ошибка сервера" });
+ }
+
+ // Логируем действие
+ const clientIP = getClientIP(req);
+ logAction(req.session.userId, "note_unarchive", `Заметка #${id} восстановлена из архива`, clientIP);
+
+ res.json({ success: true, message: "Заметка восстановлена" });
+ });
+ });
+});
+
+// API для получения архивных заметок
+app.get("/api/notes/archived", requireAuth, (req, res) => {
+ const sql = `
+ SELECT
+ n.*,
+ CASE
+ WHEN COUNT(ni.id) = 0 THEN '[]'
+ ELSE json_group_array(
+ json_object(
+ 'id', ni.id,
+ 'filename', ni.filename,
+ 'original_name', ni.original_name,
+ 'file_path', ni.file_path,
+ 'file_size', ni.file_size,
+ 'mime_type', ni.mime_type,
+ 'created_at', ni.created_at
+ )
+ )
+ END as images
+ FROM notes n
+ LEFT JOIN note_images ni ON n.id = ni.note_id
+ WHERE n.user_id = ? AND n.is_archived = 1
+ GROUP BY n.id
+ ORDER BY n.created_at DESC
+ `;
+
+ db.all(sql, [req.session.userId], (err, rows) => {
+ if (err) {
+ console.error("Ошибка получения архивных заметок:", err.message);
+ return res.status(500).json({ error: "Ошибка сервера" });
+ }
+
+ // Парсим JSON строки изображений
+ const notesWithImages = rows.map((row) => ({
+ ...row,
+ images: row.images === "[]" ? [] : JSON.parse(row.images),
+ }));
+
+ res.json(notesWithImages);
+ });
+});
+
+// API для окончательного удаления архивной заметки
+app.delete("/api/notes/archived/:id", requireAuth, (req, res) => {
+ const { id } = req.params;
+
+ // Проверяем, что заметка принадлежит текущему пользователю и архивирована
+ const checkSql = "SELECT user_id, is_archived 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: "Нет доступа к этой заметке" });
+ }
+
+ if (!row.is_archived) {
+ return res.status(400).json({ error: "Заметка не архивирована" });
+ }
+
+ // Удаляем изображения заметки
+ const getImagesSql = "SELECT file_path FROM note_images WHERE note_id = ?";
+ db.all(getImagesSql, [id], (err, images) => {
+ if (err) {
+ console.error("Ошибка получения изображений:", err.message);
+ } else {
+ images.forEach((image) => {
+ const imagePath = path.join(__dirname, "public", image.file_path);
+ if (fs.existsSync(imagePath)) {
+ fs.unlinkSync(imagePath);
+ }
+ });
+ }
+
+ // Удаляем записи об изображениях
+ const deleteImagesSql = "DELETE FROM note_images WHERE note_id = ?";
+ db.run(deleteImagesSql, [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: "Заметка удалена окончательно" });
+ });
+ });
+ });
+});
+
+// API для получения логов пользователя
+app.get("/api/logs", requireAuth, (req, res) => {
+ const { action_type, limit = 100, offset = 0 } = req.query;
+
+ let sql = `
+ SELECT id, action_type, details, ip_address, created_at
+ FROM action_logs
+ WHERE user_id = ?
+ `;
+ const params = [req.session.userId];
+
+ // Фильтр по типу действия
+ if (action_type) {
+ sql += " AND action_type = ?";
+ params.push(action_type);
+ }
+
+ sql += " ORDER BY created_at DESC LIMIT ? OFFSET ?";
+ params.push(parseInt(limit), parseInt(offset));
+
+ db.all(sql, params, (err, rows) => {
+ if (err) {
+ console.error("Ошибка получения логов:", err.message);
+ return res.status(500).json({ error: "Ошибка сервера" });
+ }
+
+ res.json(rows);
+ });
+});
+
+// Страница настроек
+app.get("/settings", requireAuth, (req, res) => {
+ // Получаем цвет пользователя для предотвращения FOUC
+ const sql = "SELECT accent_color FROM users WHERE id = ?";
+ db.get(sql, [req.session.userId], (err, user) => {
+ if (err) {
+ console.error("Ошибка получения цвета пользователя:", err.message);
+ return res.sendFile(path.join(__dirname, "public", "settings.html"));
+ }
+
+ const accentColor = user?.accent_color || "#007bff";
+
+ // Читаем HTML файл
+ fs.readFile(
+ path.join(__dirname, "public", "settings.html"),
+ "utf8",
+ (err, html) => {
+ if (err) {
+ console.error("Ошибка чтения файла settings.html:", err.message);
+ return res.sendFile(path.join(__dirname, "public", "settings.html"));
+ }
+
+ // Вставляем inline CSS с правильным цветом
+ const inlineCSS = ``;
+ const modifiedHtml = html.replace(
+ //i,
+ `\n ${inlineCSS}`
+ );
+
+ res.send(modifiedHtml);
+ }
+ );
+ });
+});
+
// Выход
app.post("/logout", (req, res) => {
+ const userId = req.session.userId;
+ const clientIP = getClientIP(req);
+
+ // Логируем выход
+ if (userId) {
+ logAction(userId, "logout", "Выход из системы", clientIP);
+ }
+
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: "Ошибка выхода" });
+
+
diff --git a/public/settings.html b/public/settings.html
new file mode 100644
index 0000000..b7e0db2
--- /dev/null
+++ b/public/settings.html
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Настройки
+
+
+
+
+
+
+
+ Профиль
+
+
+ К заметкам
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Архивные заметки
++ Архивированные заметки можно восстановить или удалить окончательно +
+
+
+
+
+
+ История действий
+ + +
+
+
+
+
+
+
+
+
+
+
+
+ | Дата и время | +Действие | +Детали | +IP-адрес | +
|---|
+
+
+
+
+
+
+ Создатель: Fovway
+
+ ×
+
+
+
+
+
+
+
+
+
diff --git a/public/settings.js b/public/settings.js
new file mode 100644
index 0000000..f5e40e0
--- /dev/null
+++ b/public/settings.js
@@ -0,0 +1,359 @@
+// Переменные для пагинации логов
+let logsOffset = 0;
+const logsLimit = 50;
+let hasMoreLogs = true;
+
+// Проверка аутентификации
+async function checkAuthentication() {
+ try {
+ const response = await fetch("/api/auth/status");
+ if (!response.ok) {
+ localStorage.removeItem("isAuthenticated");
+ localStorage.removeItem("username");
+ window.location.href = "/";
+ return false;
+ }
+ const authData = await response.json();
+ if (!authData.authenticated) {
+ localStorage.removeItem("isAuthenticated");
+ localStorage.removeItem("username");
+ window.location.href = "/";
+ return false;
+ }
+ return true;
+ } catch (error) {
+ console.error("Ошибка проверки аутентификации:", error);
+ return false;
+ }
+}
+
+// Загрузка информации о пользователе для применения accent color
+async function loadUserInfo() {
+ try {
+ const response = await fetch("/api/user");
+ if (response.ok) {
+ const user = await response.json();
+ const accentColor = user.accent_color || "#007bff";
+ if (
+ getComputedStyle(document.documentElement)
+ .getPropertyValue("--accent-color")
+ .trim() !== accentColor
+ ) {
+ document.documentElement.style.setProperty("--accent-color", accentColor);
+ }
+ }
+ } catch (error) {
+ console.error("Ошибка загрузки информации о пользователе:", error);
+ }
+}
+
+// Переключение табов
+function initTabs() {
+ const tabs = document.querySelectorAll(".settings-tab");
+ const contents = document.querySelectorAll(".tab-content");
+
+ tabs.forEach((tab) => {
+ tab.addEventListener("click", () => {
+ const tabName = tab.dataset.tab;
+
+ // Убираем активный класс со всех табов и контентов
+ tabs.forEach((t) => t.classList.remove("active"));
+ contents.forEach((c) => c.classList.remove("active"));
+
+ // Добавляем активный класс к выбранному табу и контенту
+ tab.classList.add("active");
+ document.getElementById(`${tabName}-tab`).classList.add("active");
+
+ // Загружаем данные для таба
+ if (tabName === "archive") {
+ loadArchivedNotes();
+ } else if (tabName === "logs") {
+ loadLogs(true);
+ }
+ });
+ });
+}
+
+// Загрузка архивных заметок
+async function loadArchivedNotes() {
+ const container = document.getElementById("archived-notes-container");
+ container.innerHTML = 'Загрузка...
'; + + try { + const response = await fetch("/api/notes/archived"); + if (!response.ok) { + throw new Error("Ошибка загрузки архивных заметок"); + } + + const notes = await response.json(); + + if (notes.length === 0) { + container.innerHTML = + 'Архив пуст
'; + return; + } + + container.innerHTML = ""; + + notes.forEach((note) => { + const noteDiv = document.createElement("div"); + noteDiv.className = "archived-note-item"; + noteDiv.dataset.noteId = note.id; + + // Форматируем дату + const created = new Date(note.created_at.replace(" ", "T") + "Z"); + const dateStr = new Intl.DateTimeFormat("ru-RU", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }).format(created); + + // Преобразуем markdown в HTML для предпросмотра + const htmlContent = marked.parse(note.content); + const preview = htmlContent.substring(0, 200) + (htmlContent.length > 200 ? "..." : ""); + + // Изображения + let imagesHtml = ""; + if (note.images && note.images.length > 0) { + imagesHtml = `${note.images.length} изображений
`;
+ }
+
+ noteDiv.innerHTML = `
+
+ ${dateStr}
+
+
+
+
+
+ ${preview}
+ ${imagesHtml}
+ `;
+
+ container.appendChild(noteDiv);
+ });
+
+ // Добавляем обработчики событий
+ addArchivedNotesEventListeners();
+ } catch (error) {
+ console.error("Ошибка загрузки архивных заметок:", error);
+ container.innerHTML =
+ 'Ошибка загрузки архивных заметок
'; + } +} + +// Добавление обработчиков для архивных заметок +function addArchivedNotesEventListeners() { + // Восстановление + document.querySelectorAll(".btn-restore").forEach((btn) => { + btn.addEventListener("click", async (e) => { + const noteId = e.target.closest("button").dataset.id; + if (confirm("Восстановить эту заметку из архива?")) { + try { + const response = await fetch(`/api/notes/${noteId}/unarchive`, { + method: "PUT", + }); + + if (!response.ok) { + throw new Error("Ошибка восстановления заметки"); + } + + // Удаляем элемент из списка + document + .querySelector(`[data-note-id="${noteId}"]`) + ?.remove(); + + // Проверяем, остались ли заметки + const container = document.getElementById("archived-notes-container"); + if (container.children.length === 0) { + container.innerHTML = + 'Архив пуст
'; + } + + alert("Заметка восстановлена!"); + } catch (error) { + console.error("Ошибка:", error); + alert("Ошибка восстановления заметки"); + } + } + }); + }); + + // Окончательное удаление + document.querySelectorAll(".btn-delete-permanent").forEach((btn) => { + btn.addEventListener("click", async (e) => { + const noteId = e.target.closest("button").dataset.id; + if ( + confirm( + "Удалить эту заметку НАВСЕГДА? Это действие нельзя отменить!" + ) + ) { + try { + const response = await fetch(`/api/notes/archived/${noteId}`, { + method: "DELETE", + }); + + if (!response.ok) { + throw new Error("Ошибка удаления заметки"); + } + + // Удаляем элемент из списка + document + .querySelector(`[data-note-id="${noteId}"]`) + ?.remove(); + + // Проверяем, остались ли заметки + const container = document.getElementById("archived-notes-container"); + if (container.children.length === 0) { + container.innerHTML = + 'Архив пуст
'; + } + + alert("Заметка удалена окончательно"); + } catch (error) { + console.error("Ошибка:", error); + alert("Ошибка удаления заметки"); + } + } + }); + }); +} + +// Загрузка логов +async function loadLogs(reset = false) { + if (reset) { + logsOffset = 0; + hasMoreLogs = true; + } + + const tbody = document.getElementById("logsTableBody"); + const loadMoreContainer = document.getElementById("logsLoadMore"); + const filterValue = document.getElementById("logTypeFilter").value; + + if (reset) { + tbody.innerHTML = '