diff --git a/SESSION_PERSISTENCE_UPDATE.md b/SESSION_PERSISTENCE_UPDATE.md new file mode 100644 index 0000000..41e867e --- /dev/null +++ b/SESSION_PERSISTENCE_UPDATE.md @@ -0,0 +1,66 @@ +# Обновление системы аутентификации для сохранения сессии при перезагрузке сервера + +## Проблема +При перезагрузке сервера пользователи разлогинивались, так как сессии хранились только в памяти. + +## Решение +Реализована двухуровневая система аутентификации: + +### 1. Клиентская часть (localStorage) +- **Файлы изменены:** + - `public/login.js` - сохранение состояния аутентификации при входе + - `public/register.js` - сохранение состояния аутентификации при регистрации + - `public/app.js` - проверка аутентификации при загрузке страницы заметок + - `public/profile.js` - проверка аутентификации при загрузке профиля + +- **Функциональность:** + - Сохранение флага `isAuthenticated` и `username` в localStorage + - Проверка аутентификации при загрузке защищенных страниц + - Очистка localStorage при выходе + - Автоматическое перенаправление неавторизованных пользователей + +### 2. Серверная часть (SQLite) +- **Файлы изменены:** + - `server.js` - добавлено хранение сессий в SQLite + +- **Новые зависимости:** + - `connect-sqlite3` - для хранения сессий в базе данных + +- **Функциональность:** + - Сессии теперь сохраняются в файле `sessions.db` + - Время жизни сессии: 7 дней + - Новый API endpoint `/api/auth/status` для проверки статуса аутентификации + +## Как это работает + +1. **При входе/регистрации:** + - Сервер создает сессию и сохраняет её в SQLite + - Клиент сохраняет флаг аутентификации в localStorage + +2. **При загрузке страницы:** + - Клиент проверяет localStorage + - Если пользователь "авторизован", проверяется серверная сессия + - Если серверная сессия действительна, пользователь остается авторизованным + +3. **При перезагрузке сервера:** + - Сессии восстанавливаются из SQLite + - Пользователи остаются авторизованными + +4. **При выходе:** + - Серверная сессия удаляется + - localStorage очищается + +## Безопасность +- Сессии имеют ограниченное время жизни (7 дней) +- Проверка аутентификации происходит как на клиенте, так и на сервере +- При недействительной серверной сессии пользователь автоматически разлогинивается + +## Тестирование +1. Войдите в систему через браузер +2. Перезагрузите сервер (Ctrl+C и снова `node server.js`) +3. Обновите страницу в браузере +4. Пользователь должен остаться авторизованным + +## Файлы базы данных +- `notes.db` - основная база данных приложения +- `sessions.db` - база данных сессий (создается автоматически) diff --git a/package-lock.json b/package-lock.json index bbf1df8..786a34e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "bcryptjs": "^3.0.2", "body-parser": "^2.2.0", "codemirror": "^6.0.2", + "connect-sqlite3": "^0.9.16", "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", @@ -26,6 +27,7 @@ "helmet": "^8.1.0", "marked": "^16.4.0", "multer": "^2.0.0-rc.4", + "node-fetch": "^3.3.2", "sqlite3": "^5.1.7" }, "devDependencies": { @@ -1168,6 +1170,17 @@ "typedarray": "^0.0.6" } }, + "node_modules/connect-sqlite3": { + "version": "0.9.16", + "resolved": "https://registry.npmjs.org/connect-sqlite3/-/connect-sqlite3-0.9.16.tgz", + "integrity": "sha512-2gqo0QmcBBL8p8+eqpBETn7RgM/PaoKvpQGl8PfjEgwlr0VuMYNMxRJRrRCo3KR3fxMYeSsCw2tGNG0JKN9Nvg==", + "dependencies": { + "sqlite3": "^5.0.2" + }, + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -1226,6 +1239,14 @@ "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1503,6 +1524,28 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1536,6 +1579,17 @@ "node": ">= 0.8" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2322,6 +2376,42 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-gyp": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", @@ -3254,6 +3344,14 @@ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 97a92c7..1478097 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "bcryptjs": "^3.0.2", "body-parser": "^2.2.0", "codemirror": "^6.0.2", + "connect-sqlite3": "^0.9.16", "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", @@ -32,6 +33,7 @@ "helmet": "^8.1.0", "marked": "^16.4.0", "multer": "^2.0.0-rc.4", + "node-fetch": "^3.3.2", "sqlite3": "^5.1.7" }, "devDependencies": { diff --git a/public/app.js b/public/app.js index d91e9f2..ecb498d 100644 --- a/public/app.js +++ b/public/app.js @@ -718,11 +718,65 @@ noteInput.addEventListener("keydown", function (event) { // Загружаем заметки при загрузке страницы document.addEventListener("DOMContentLoaded", function () { + // Проверяем аутентификацию при загрузке страницы + checkAuthentication(); loadUserInfo(); loadNotes(); updateFilterIndicator(); + + // Добавляем обработчик для кнопки выхода + setupLogoutHandler(); }); +// Функция для настройки обработчика выхода +function setupLogoutHandler() { + const logoutForms = document.querySelectorAll('form[action="/logout"]'); + logoutForms.forEach(form => { + form.addEventListener('submit', function(e) { + // Очищаем localStorage перед выходом + localStorage.removeItem('isAuthenticated'); + localStorage.removeItem('username'); + }); + }); +} + +// Функция для проверки аутентификации +async function checkAuthentication() { + const isAuthenticated = localStorage.getItem('isAuthenticated'); + + if (isAuthenticated !== 'true') { + // Если пользователь не аутентифицирован, перенаправляем на страницу входа + window.location.href = "/"; + return; + } + + // Проверяем, что сессия на сервере еще действительна + try { + const response = await fetch("/api/auth/status"); + if (!response.ok) { + // Если сессия недействительна, очищаем localStorage и перенаправляем + localStorage.removeItem('isAuthenticated'); + localStorage.removeItem('username'); + window.location.href = "/"; + return; + } + + const authData = await response.json(); + if (!authData.authenticated) { + // Если сервер говорит, что пользователь не аутентифицирован + localStorage.removeItem('isAuthenticated'); + localStorage.removeItem('username'); + window.location.href = "/"; + return; + } + } catch (error) { + console.error("Ошибка проверки аутентификации:", error); + // В случае ошибки сети, оставляем пользователя на странице + // но показываем предупреждение + console.warn("Не удалось проверить статус аутентификации"); + } +} + // Функция для загрузки информации о пользователе async function loadUserInfo() { try { diff --git a/public/login.js b/public/login.js index 1ff1989..00f2ea7 100644 --- a/public/login.js +++ b/public/login.js @@ -1,3 +1,8 @@ +// Проверяем, не авторизован ли уже пользователь +if (localStorage.getItem('isAuthenticated') === 'true') { + window.location.href = "/notes"; +} + // Проверяем наличие ошибки в URL const urlParams = new URLSearchParams(window.location.search); if (urlParams.get("error") === "invalid_password") { @@ -34,7 +39,9 @@ if (loginForm) { const data = await response.json(); if (response.ok) { - // Успешный вход + // Успешный вход - сохраняем состояние аутентификации + localStorage.setItem('isAuthenticated', 'true'); + localStorage.setItem('username', username); window.location.href = "/notes"; } else { // Ошибка входа diff --git a/public/profile.js b/public/profile.js index b9b17a4..333b35b 100644 --- a/public/profile.js +++ b/public/profile.js @@ -245,7 +245,61 @@ function isValidEmail(email) { return re.test(email); } +// Функция для проверки аутентификации +async function checkAuthentication() { + const isAuthenticated = localStorage.getItem('isAuthenticated'); + + if (isAuthenticated !== 'true') { + // Если пользователь не аутентифицирован, перенаправляем на страницу входа + window.location.href = "/"; + return; + } + + // Проверяем, что сессия на сервере еще действительна + try { + const response = await fetch("/api/auth/status"); + if (!response.ok) { + // Если сессия недействительна, очищаем localStorage и перенаправляем + localStorage.removeItem('isAuthenticated'); + localStorage.removeItem('username'); + window.location.href = "/"; + return; + } + + const authData = await response.json(); + if (!authData.authenticated) { + // Если сервер говорит, что пользователь не аутентифицирован + localStorage.removeItem('isAuthenticated'); + localStorage.removeItem('username'); + window.location.href = "/"; + return; + } + } catch (error) { + console.error("Ошибка проверки аутентификации:", error); + // В случае ошибки сети, оставляем пользователя на странице + // но показываем предупреждение + console.warn("Не удалось проверить статус аутентификации"); + } +} + +// Функция для настройки обработчика выхода +function setupLogoutHandler() { + const logoutForms = document.querySelectorAll('form[action="/logout"]'); + logoutForms.forEach(form => { + form.addEventListener('submit', function(e) { + // Очищаем localStorage перед выходом + localStorage.removeItem('isAuthenticated'); + localStorage.removeItem('username'); + }); + }); +} + // Загружаем профиль при загрузке страницы document.addEventListener("DOMContentLoaded", function () { + // Проверяем аутентификацию при загрузке страницы + checkAuthentication(); loadProfile(); + + // Добавляем обработчик для кнопки выхода + setupLogoutHandler(); }); diff --git a/public/register.js b/public/register.js index 1932cee..3cfc8cc 100644 --- a/public/register.js +++ b/public/register.js @@ -1,3 +1,8 @@ +// Проверяем, не авторизован ли уже пользователь +if (localStorage.getItem('isAuthenticated') === 'true') { + window.location.href = "/notes"; +} + // Обработка формы регистрации const registerForm = document.getElementById("registerForm"); const errorMessage = document.getElementById("errorMessage"); @@ -47,7 +52,9 @@ if (registerForm) { const data = await response.json(); if (response.ok) { - // Успешная регистрация + // Успешная регистрация - сохраняем состояние аутентификации + localStorage.setItem('isAuthenticated', 'true'); + localStorage.setItem('username', username); window.location.href = "/notes"; } else { // Ошибка регистрации diff --git a/server.js b/server.js index 71f62fd..52d5717 100644 --- a/server.js +++ b/server.js @@ -2,6 +2,7 @@ const express = require("express"); const sqlite3 = require("sqlite3").verbose(); const bcrypt = require("bcryptjs"); const session = require("express-session"); +const SQLiteStore = require("connect-sqlite3")(session); const path = require("path"); const helmet = require("helmet"); const rateLimit = require("express-rate-limit"); @@ -93,13 +94,21 @@ app.use(express.static(path.join(__dirname, "public"))); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); -// Настройка сессий +// Настройка сессий с хранением в SQLite app.use( session({ + store: new SQLiteStore({ + db: "sessions.db", + table: "sessions", + dir: "./" + }), secret: process.env.SESSION_SECRET || "default-secret", resave: false, saveUninitialized: false, - cookie: { secure: false }, // в продакшене установить true с HTTPS + cookie: { + secure: false, // в продакшене установить true с HTTPS + maxAge: 7 * 24 * 60 * 60 * 1000 // 7 дней + }, }) ); @@ -285,6 +294,19 @@ app.post("/login", async (req, res) => { } }); +// API для проверки статуса аутентификации +app.get("/api/auth/status", (req, res) => { + if (req.session.authenticated && req.session.userId) { + res.json({ + authenticated: true, + userId: req.session.userId, + username: req.session.username + }); + } else { + res.status(401).json({ authenticated: false }); + } +}); + // API для получения информации о пользователе app.get("/api/user", requireAuth, (req, res) => { if (!req.session.userId) {