feat: добавлена поддержка сессий с использованием SQLite и улучшена аутентификация
- Реализовано хранение сессий в базе данных SQLite с помощью connect-sqlite3 - Добавлены API для проверки статуса аутентификации - Обновлены клиентские скрипты для управления состоянием аутентификации - Добавлены проверки аутентификации на страницах входа и профиля - Улучшено управление состоянием аутентификации в localStorage
This commit is contained in:
parent
346e6d0172
commit
e4b2be3052
66
SESSION_PERSISTENCE_UPDATE.md
Normal file
66
SESSION_PERSISTENCE_UPDATE.md
Normal 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
98
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
// Ошибка входа
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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 {
|
||||
// Ошибка регистрации
|
||||
|
||||
26
server.js
26
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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user