From 7367364d93f2a5b51a0d327de38a6a6c33898999 Mon Sep 17 00:00:00 2001 From: Fovway Date: Wed, 5 Nov 2025 21:28:27 +0700 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B8=D1=81=D0=BA=D0=B0=20=D0=B7=D0=B0=D0=BC=D0=B5?= =?UTF-8?q?=D1=82=D0=BE=D0=BA=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B5=D0=B3?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D1=80=D0=BE=D0=BD=D0=B5=D0=B7=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=81=D0=B8=D0=BC=D0=BE=D0=B3=D0=BE=20=D0=BF=D0=BE=D0=B8?= =?UTF-8?q?=D1=81=D0=BA=D0=B0=20=D0=BF=D0=BE=20=D1=81=D0=BE=D0=B4=D0=B5?= =?UTF-8?q?=D1=80=D0=B6=D0=B8=D0=BC=D0=BE=D0=BC=D1=83.=20=D0=94=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=BD=D0=BE=D0=B2=D1=8B?= =?UTF-8?q?=D0=B9=20API=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D1=8B=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=D0=B0,=20=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D1=8B=D0=B9=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B8=D1=80=D1=83=D0=B5=D1=82=20=D0=B4=D0=B5?= =?UTF-8?q?=D0=B9=D1=81=D1=82=D0=B2=D0=B8=D0=B5=20=D0=B8=20=D0=BE=D1=87?= =?UTF-8?q?=D0=B8=D1=89=D0=B0=D0=B5=D1=82=20=D1=81=D0=B5=D1=81=D1=81=D0=B8?= =?UTF-8?q?=D1=8E.=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B0=20=D0=BE=D1=87=D0=B8=D1=81=D1=82=D0=BA=D0=B0?= =?UTF-8?q?=20IndexedDB=20=D0=BF=D1=80=D0=B8=20=D1=80=D0=B0=D0=B7=D0=BB?= =?UTF-8?q?=D0=BE=D0=B3=D0=B8=D0=BD=D0=B8=D0=B2=D0=B0=D0=BD=D0=B8=D0=B8=20?= =?UTF-8?q?=D0=B8=20=D1=81=D0=BC=D0=B5=D0=BD=D0=B5=20=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F.=20?= =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=D1=8B=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=81=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=D0=BC=20=D0=B0=D1=83=D1=82=D0=B5=D0=BD=D1=82=D0=B8=D1=84?= =?UTF-8?q?=D0=B8=D0=BA=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B8=20=D0=BE=D1=87?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D0=BA=D0=B8=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D1=85=20=D0=BF=D1=80=D0=B8=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B8=20=D1=83=D0=B4?= =?UTF-8?q?=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D0=B8=20=D0=B0=D0=BA=D0=BA=D0=B0?= =?UTF-8?q?=D1=83=D0=BD=D1=82=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/server.js | 23 ++++++++++++++++++++--- dev-dist/sw.js | 2 +- src/api/axiosClient.ts | 5 +++++ src/components/ProtectedRoute.tsx | 14 ++++++++++++-- src/pages/LoginPage.tsx | 12 +++++++++++- src/pages/ProfilePage.tsx | 10 ++++++++-- src/pages/RegisterPage.tsx | 9 ++++++++- src/pages/SettingsPage.tsx | 7 +++++-- src/utils/indexedDB.ts | 10 ++++++++++ 9 files changed, 80 insertions(+), 12 deletions(-) diff --git a/backend/server.js b/backend/server.js index e174b1e..1c3d4b8 100644 --- a/backend/server.js +++ b/backend/server.js @@ -857,10 +857,10 @@ app.get("/api/notes/search", requireApiAuth, (req, res) => { let whereClause = "WHERE n.user_id = ? AND n.is_archived = 0"; let params = [req.session.userId]; - // Поиск по тексту + // Поиск по тексту (регистронезависимый) if (q && q.trim()) { - whereClause += " AND n.content LIKE ?"; - params.push(`%${q.trim()}%`); + whereClause += " AND LOWER(n.content) LIKE ?"; + params.push(`%${q.trim().toLowerCase()}%`); } // Поиск по тегу (регистронезависимый) @@ -2549,6 +2549,23 @@ app.post("/logout", (req, res) => { }); }); +// API для выхода (для фронтенда) +app.post("/api/logout", (req, res) => { + const userId = req.session.userId; + + // Логируем выход + if (userId) { + logAction(userId, "logout", "Выход из системы"); + } + + req.session.destroy((err) => { + if (err) { + return res.status(500).json({ error: "Ошибка выхода" }); + } + res.json({ success: true, message: "Выход выполнен успешно" }); + }); +}); + // API для удаления аккаунта app.delete("/api/user/delete-account", requireApiAuth, async (req, res) => { const { password } = req.body; diff --git a/dev-dist/sw.js b/dev-dist/sw.js index 79f7624..02d7e81 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-9dc17825'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.2vg2p27g3bg" + "revision": "0.mib567t8mr8" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/src/api/axiosClient.ts b/src/api/axiosClient.ts index bd76eaa..0bbe5bd 100644 --- a/src/api/axiosClient.ts +++ b/src/api/axiosClient.ts @@ -1,4 +1,5 @@ import axios from "axios"; +import { dbManager } from "../utils/indexedDB"; const axiosClient = axios.create({ baseURL: "/api", @@ -48,6 +49,10 @@ axiosClient.interceptors.response.use( // Разлогиниваем только если это НЕ запрос с проверкой пароля if (!isPasswordProtected) { + // Очищаем IndexedDB при автоматическом разлогинивании + dbManager.clearAll().catch((err) => { + console.error("Ошибка очистки IndexedDB при 401:", err); + }); localStorage.removeItem("isAuthenticated"); window.location.href = "/"; } diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx index 0310ba3..f57eafb 100644 --- a/src/components/ProtectedRoute.tsx +++ b/src/components/ProtectedRoute.tsx @@ -4,11 +4,13 @@ import { useAppSelector, useAppDispatch } from "../store/hooks"; import { setAuth, clearAuth } from "../store/slices/authSlice"; import { authApi } from "../api/authApi"; import { LoadingOverlay } from "./common/LoadingOverlay"; +import { dbManager } from "../utils/indexedDB"; export const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children, }) => { const isAuthenticated = useAppSelector((state) => state.auth.isAuthenticated); + const currentUserId = useAppSelector((state) => state.auth.userId); const dispatch = useAppDispatch(); const [isChecking, setIsChecking] = useState(true); @@ -17,9 +19,17 @@ export const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ try { const authStatus = await authApi.checkStatus(); if (authStatus.authenticated) { + const newUserId = authStatus.userId!; + + // Если пользователь изменился, очищаем IndexedDB + if (currentUserId && currentUserId !== newUserId) { + console.log(`[ProtectedRoute] User changed from ${currentUserId} to ${newUserId}, clearing IndexedDB`); + await dbManager.clearAll(); + } + dispatch( setAuth({ - userId: authStatus.userId!, + userId: newUserId, username: authStatus.username!, }) ); @@ -36,7 +46,7 @@ export const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ // Всегда проверяем статус аутентификации при монтировании, // независимо от начального состояния Redux (localStorage может быть устаревшим) checkAuth(); - }, [dispatch]); + }, [dispatch, currentUserId]); if (isChecking) { return ; diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 2cbffd1..546d124 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -6,6 +6,7 @@ import { authApi } from "../api/authApi"; import { useNotification } from "../hooks/useNotification"; import { ThemeToggle } from "../components/common/ThemeToggle"; import { Icon } from "@iconify/react"; +import { dbManager } from "../utils/indexedDB"; const LoginPage: React.FC = () => { const [username, setUsername] = useState(""); @@ -15,6 +16,7 @@ const LoginPage: React.FC = () => { const dispatch = useAppDispatch(); const { showNotification } = useNotification(); const isAuthenticated = useAppSelector((state) => state.auth.isAuthenticated); + const currentUserId = useAppSelector((state) => state.auth.userId); const [searchParams] = useSearchParams(); useEffect(() => { @@ -48,9 +50,17 @@ const LoginPage: React.FC = () => { if (data.success) { // Получаем информацию о пользователе const authStatus = await authApi.checkStatus(); + const newUserId = authStatus.userId!; + + // Если пользователь изменился, очищаем IndexedDB + if (currentUserId && currentUserId !== newUserId) { + console.log(`[Login] User changed from ${currentUserId} to ${newUserId}, clearing IndexedDB`); + await dbManager.clearAll(); + } + dispatch( setAuth({ - userId: authStatus.userId!, + userId: newUserId, username: authStatus.username!, }) ); diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index 8a87862..53b2563 100644 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -11,6 +11,7 @@ import { setAccentColor } from "../utils/colorUtils"; import { useNotification } from "../hooks/useNotification"; import { Modal } from "../components/common/Modal"; import { ThemeToggle } from "../components/common/ThemeToggle"; +import { dbManager } from "../utils/indexedDB"; const ProfilePage: React.FC = () => { const navigate = useNavigate(); @@ -211,6 +212,8 @@ const ProfilePage: React.FC = () => { setIsDeleting(true); try { await userApi.deleteAccount(deletePassword); + // Очищаем IndexedDB при удалении аккаунта + await dbManager.clearAll(); showNotification("Аккаунт успешно удален", "success"); dispatch(clearAuth()); setTimeout(() => { @@ -259,10 +262,13 @@ const ProfilePage: React.FC = () => { onClick={async () => { try { await authApi.logout(); - dispatch(clearAuth()); - navigate("/"); } catch (error) { console.error("Ошибка выхода:", error); + } finally { + // Очищаем IndexedDB в фоне (не блокируем выход) + dbManager.clearAll().catch((err) => { + console.error("Ошибка очистки IndexedDB при выходе:", err); + }); dispatch(clearAuth()); navigate("/"); } diff --git a/src/pages/RegisterPage.tsx b/src/pages/RegisterPage.tsx index aed3c19..62eb4a7 100644 --- a/src/pages/RegisterPage.tsx +++ b/src/pages/RegisterPage.tsx @@ -6,6 +6,7 @@ import { authApi } from "../api/authApi"; import { useNotification } from "../hooks/useNotification"; import { ThemeToggle } from "../components/common/ThemeToggle"; import { Icon } from "@iconify/react"; +import { dbManager } from "../utils/indexedDB"; const RegisterPage: React.FC = () => { const [username, setUsername] = useState(""); @@ -57,9 +58,15 @@ const RegisterPage: React.FC = () => { if (data.success) { // Получаем информацию о пользователе const authStatus = await authApi.checkStatus(); + const newUserId = authStatus.userId!; + + // При регистрации нового пользователя всегда очищаем IndexedDB + // (на случай, если в базе остались данные предыдущего пользователя) + await dbManager.clearAll(); + dispatch( setAuth({ - userId: authStatus.userId!, + userId: newUserId, username: authStatus.username!, }) ); diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 48a0cda..e839034 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -553,10 +553,13 @@ const SettingsPage: React.FC = () => { onClick={async () => { try { await authApi.logout(); - dispatch(clearAuth()); - navigate("/"); } catch (error) { console.error("Ошибка выхода:", error); + } finally { + // Очищаем IndexedDB в фоне (не блокируем выход) + dbManager.clearAll().catch((err) => { + console.error("Ошибка очистки IndexedDB при выходе:", err); + }); dispatch(clearAuth()); navigate("/"); } diff --git a/src/utils/indexedDB.ts b/src/utils/indexedDB.ts index 916c471..766c28f 100644 --- a/src/utils/indexedDB.ts +++ b/src/utils/indexedDB.ts @@ -225,6 +225,16 @@ class IndexedDBManager { }); } + /** + * Полная очистка всех данных из IndexedDB (заметки и очередь синхронизации) + */ + async clearAll(): Promise { + await Promise.all([ + this.clearAllNotes(), + this.clearSyncQueue(), + ]); + } + // ===== Утилиты ===== async getPendingSyncCount(): Promise {