feat: добавлена поддержка сессий с использованием SQLite и улучшена аутентификация

- Реализовано хранение сессий в базе данных SQLite с помощью connect-sqlite3
- Добавлены API для проверки статуса аутентификации
- Обновлены клиентские скрипты для управления состоянием аутентификации
- Добавлены проверки аутентификации на страницах входа и профиля
- Улучшено управление состоянием аутентификации в localStorage
This commit is contained in:
Fovway 2025-10-19 15:15:05 +07:00
parent 346e6d0172
commit e4b2be3052
8 changed files with 314 additions and 4 deletions

View File

@ -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` - база данных сессий (создается автоматически)

98
package-lock.json generated
View File

@ -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",

View File

@ -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": {

View File

@ -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 {

View File

@ -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 {
// Ошибка входа

View File

@ -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();
});

View File

@ -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 {
// Ошибка регистрации

View File

@ -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) {