NoteJS/server.js
Fovway e4b2be3052 feat: добавлена поддержка сессий с использованием SQLite и улучшена аутентификация
- Реализовано хранение сессий в базе данных SQLite с помощью connect-sqlite3
- Добавлены API для проверки статуса аутентификации
- Обновлены клиентские скрипты для управления состоянием аутентификации
- Добавлены проверки аутентификации на страницах входа и профиля
- Улучшено управление состоянием аутентификации в localStorage
2025-10-19 15:15:05 +07:00

680 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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");
const bodyParser = require("body-parser");
const multer = require("multer");
const fs = require("fs");
require("dotenv").config();
const app = express();
const PORT = process.env.PORT || 3000;
// Создаем директорию для аватарок, если её нет
const uploadsDir = path.join(__dirname, "public", "uploads");
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
}
// Настройка multer для загрузки аватарок
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, uploadsDir);
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
cb(
null,
"avatar-" +
req.session.userId +
"-" +
uniqueSuffix +
path.extname(file.originalname)
);
},
});
const upload = multer({
storage: storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB максимум
fileFilter: function (req, file, cb) {
const filetypes = /jpeg|jpg|png|gif/;
const mimetype = filetypes.test(file.mimetype);
const extname = filetypes.test(
path.extname(file.originalname).toLowerCase()
);
if (mimetype && extname) {
return cb(null, true);
}
cb(new Error("Только изображения (jpeg, jpg, png, gif) разрешены!"));
},
});
// Middleware для безопасности
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: [
"'self'",
"'unsafe-inline'",
"https://cdnjs.cloudflare.com",
],
styleSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com"],
fontSrc: ["'self'", "https://cdnjs.cloudflare.com", "data:"],
imgSrc: ["'self'", "data:", "blob:"],
connectSrc: [
"'self'",
"https://api.iconify.design",
"https://api.simplesvg.com",
"https://api.unisvg.com",
],
},
},
})
);
// Ограничение запросов (отключено для разработки)
// const limiter = rateLimit({
// windowMs: 15 * 60 * 1000, // 15 минут
// max: 100, // максимум 100 запросов с одного IP
// });
// app.use(limiter);
// Статические файлы
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
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 дней
},
})
);
// Подключение к базе данных
const db = new sqlite3.Database("./notes.db", (err) => {
if (err) {
console.error("Ошибка подключения к базе данных:", err.message);
} else {
console.log("Подключено к SQLite базе данных");
createTables();
}
});
// Создание таблиц
function createTables() {
const createNotesTable = `
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
content TEXT NOT NULL,
date TEXT NOT NULL,
time TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`;
const createUsersTable = `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
email TEXT,
avatar TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`;
db.run(createNotesTable, (err) => {
if (err) {
console.error("Ошибка создания таблицы заметок:", err.message);
} else {
console.log("Таблица заметок готова");
}
});
db.run(createUsersTable, (err) => {
if (err) {
console.error("Ошибка создания таблицы пользователей:", err.message);
} else {
console.log("Таблица пользователей готова");
}
});
}
// Middleware для аутентификации
function requireAuth(req, res, next) {
if (req.session.authenticated) {
return next();
} else {
return res.redirect("/");
}
}
// Маршруты
// Главная страница с формой входа
app.get("/", (req, res) => {
if (req.session.authenticated) {
return res.redirect("/notes");
}
res.sendFile(path.join(__dirname, "public", "index.html"));
});
// Страница регистрации
app.get("/register", (req, res) => {
if (req.session.authenticated) {
return res.redirect("/notes");
}
res.sendFile(path.join(__dirname, "public", "register.html"));
});
// API регистрации
app.post("/api/register", async (req, res) => {
const { username, password, confirmPassword } = req.body;
// Валидация
if (!username || !password || !confirmPassword) {
return res.status(400).json({ error: "Все поля обязательны" });
}
if (username.length < 3) {
return res
.status(400)
.json({ error: "Логин должен быть не менее 3 символов" });
}
if (password.length < 6) {
return res
.status(400)
.json({ error: "Пароль должен быть не менее 6 символов" });
}
if (password !== confirmPassword) {
return res.status(400).json({ error: "Пароли не совпадают" });
}
try {
// Хешируем пароль
const hashedPassword = await bcrypt.hash(password, 10);
// Вставляем пользователя в БД
const sql = "INSERT INTO users (username, password) VALUES (?, ?)";
db.run(sql, [username, hashedPassword], function (err) {
if (err) {
if (err.message.includes("UNIQUE constraint failed")) {
return res.status(400).json({ error: "Этот логин уже занят" });
}
console.error("Ошибка регистрации:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
// Автоматически логиним пользователя после регистрации
req.session.userId = this.lastID;
req.session.username = username;
req.session.authenticated = true;
res.json({ success: true, message: "Регистрация успешна" });
});
} catch (err) {
console.error("Ошибка при хешировании:", err);
res.status(500).json({ error: "Ошибка сервера" });
}
});
// API входа
app.post("/api/login", async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: "Логин и пароль обязательны" });
}
const sql = "SELECT * FROM users WHERE username = ?";
db.get(sql, [username], async (err, user) => {
if (err) {
console.error("Ошибка входа:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
if (!user) {
return res.status(401).json({ error: "Неверный логин или пароль" });
}
try {
const validPassword = await bcrypt.compare(password, user.password);
if (!validPassword) {
return res.status(401).json({ error: "Неверный логин или пароль" });
}
// Успешный вход
req.session.userId = user.id;
req.session.username = user.username;
req.session.authenticated = true;
res.json({ success: true, message: "Вход успешен" });
} catch (err) {
console.error("Ошибка при сравнении паролей:", err);
res.status(500).json({ error: "Ошибка сервера" });
}
});
});
// Обработка входа (старый маршрут для совместимости)
app.post("/login", async (req, res) => {
const { password } = req.body;
const correctPassword = process.env.APP_PASSWORD;
if (password === correctPassword) {
req.session.authenticated = true;
res.redirect("/notes");
} else {
res.redirect("/?error=invalid_password");
}
});
// 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) {
return res.status(401).json({ error: "Не аутентифицирован" });
}
const sql = "SELECT username, email, avatar FROM users WHERE id = ?";
db.get(sql, [req.session.userId], (err, user) => {
if (err) {
console.error("Ошибка получения данных пользователя:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
if (!user) {
return res.status(404).json({ error: "Пользователь не найден" });
}
res.json(user);
});
});
// Страница с заметками (требует аутентификации)
app.get("/notes", requireAuth, (req, res) => {
res.sendFile(path.join(__dirname, "public", "notes.html"));
});
// API для поиска заметок (должен быть ПЕРЕД /api/notes/:id)
app.get("/api/notes/search", requireAuth, (req, res) => {
const { q, tag, date } = req.query;
let sql = "SELECT * FROM notes WHERE user_id = ?";
let params = [req.session.userId];
// Поиск по тексту
if (q && q.trim()) {
sql += " AND content LIKE ?";
params.push(`%${q.trim()}%`);
}
// Поиск по тегу
if (tag && tag.trim()) {
sql += " AND content LIKE ?";
params.push(`%#${tag.trim()}%`);
}
// Поиск по дате
if (date && date.trim()) {
sql += " AND date = ?";
params.push(date.trim());
}
sql += " ORDER BY created_at DESC";
db.all(sql, params, (err, rows) => {
if (err) {
console.error("Ошибка поиска заметок:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
res.json(rows);
});
});
// API для получения всех заметок
app.get("/api/notes", requireAuth, (req, res) => {
const sql = "SELECT * FROM notes WHERE user_id = ? ORDER BY created_at ASC";
db.all(sql, [req.session.userId], (err, rows) => {
if (err) {
console.error("Ошибка получения заметок:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
res.json(rows);
});
});
// API для создания новой заметки
app.post("/api/notes", requireAuth, (req, res) => {
const { content, date, time } = req.body;
if (!content || !date || !time) {
return res.status(400).json({ error: "Не все поля заполнены" });
}
const sql =
"INSERT INTO notes (user_id, content, date, time) VALUES (?, ?, ?, ?)";
const params = [req.session.userId, content, date, time];
db.run(sql, params, function (err) {
if (err) {
console.error("Ошибка создания заметки:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
res.json({ id: this.lastID, content, date, time });
});
});
// API для обновления заметки
app.put("/api/notes/:id", requireAuth, (req, res) => {
const { content, date, time } = req.body;
const { id } = req.params;
if (!content || !date || !time) {
return res.status(400).json({ error: "Не все поля заполнены" });
}
// Проверяем, что заметка принадлежит текущему пользователю
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 content = ?, date = ?, time = ? WHERE id = ?";
const params = [content, date, time, id];
db.run(updateSql, params, function (err) {
if (err) {
console.error("Ошибка обновления заметки:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
if (this.changes === 0) {
return res.status(404).json({ error: "Заметка не найдена" });
}
res.json({ id, content, date, time });
});
});
});
// API для удаления заметки
app.delete("/api/notes/:id", 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 deleteSql = "DELETE FROM notes WHERE id = ?";
db.run(deleteSql, id, function (err) {
if (err) {
console.error("Ошибка удаления заметки:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
if (this.changes === 0) {
return res.status(404).json({ error: "Заметка не найдена" });
}
res.json({ message: "Заметка удалена" });
});
});
});
// Страница личного кабинета
app.get("/profile", requireAuth, (req, res) => {
res.sendFile(path.join(__dirname, "public", "profile.html"));
});
// API для обновления профиля
app.put("/api/user/profile", requireAuth, async (req, res) => {
const { username, email, currentPassword, newPassword } = req.body;
const userId = req.session.userId;
try {
// Получаем текущие данные пользователя
const getUserSql = "SELECT * FROM users WHERE id = ?";
db.get(getUserSql, [userId], async (err, user) => {
if (err) {
console.error("Ошибка получения пользователя:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
if (!user) {
return res.status(404).json({ error: "Пользователь не найден" });
}
// Валидация
if (username && username.length < 3) {
return res
.status(400)
.json({ error: "Логин должен быть не менее 3 символов" });
}
// Если пользователь хочет изменить пароль, проверяем текущий пароль
if (newPassword) {
if (!currentPassword) {
return res
.status(400)
.json({ error: "Введите текущий пароль для изменения пароля" });
}
const validPassword = await bcrypt.compare(
currentPassword,
user.password
);
if (!validPassword) {
return res.status(401).json({ error: "Неверный текущий пароль" });
}
if (newPassword.length < 6) {
return res
.status(400)
.json({ error: "Новый пароль должен быть не менее 6 символов" });
}
}
// Формируем SQL запрос для обновления
let updateFields = [];
let params = [];
if (username && username !== user.username) {
updateFields.push("username = ?");
params.push(username);
}
if (email !== undefined) {
updateFields.push("email = ?");
params.push(email || null);
}
if (newPassword) {
const hashedPassword = await bcrypt.hash(newPassword, 10);
updateFields.push("password = ?");
params.push(hashedPassword);
}
if (updateFields.length === 0) {
return res.json({ success: true, message: "Нет изменений" });
}
params.push(userId);
const updateSql = `UPDATE users SET ${updateFields.join(
", "
)} WHERE id = ?`;
db.run(updateSql, params, function (err) {
if (err) {
if (err.message.includes("UNIQUE constraint failed")) {
return res.status(400).json({ error: "Этот логин уже занят" });
}
console.error("Ошибка обновления профиля:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
// Обновляем имя пользователя в сессии, если оно изменилось
if (username && username !== user.username) {
req.session.username = username;
}
res.json({ success: true, message: "Профиль успешно обновлен" });
});
});
} catch (err) {
console.error("Ошибка обновления профиля:", err);
res.status(500).json({ error: "Ошибка сервера" });
}
});
// API для загрузки аватарки
app.post(
"/api/user/avatar",
requireAuth,
upload.single("avatar"),
(req, res) => {
if (!req.file) {
return res.status(400).json({ error: "Файл не загружен" });
}
const avatarPath = "/uploads/" + req.file.filename;
const userId = req.session.userId;
// Получаем старую аватарку для удаления
const getOldAvatarSql = "SELECT avatar FROM users WHERE id = ?";
db.get(getOldAvatarSql, [userId], (err, user) => {
if (err) {
console.error("Ошибка получения старой аватарки:", err.message);
}
// Удаляем старую аватарку, если она существует
if (user && user.avatar) {
const oldAvatarPath = path.join(__dirname, "public", user.avatar);
if (fs.existsSync(oldAvatarPath)) {
fs.unlinkSync(oldAvatarPath);
}
}
// Обновляем путь к аватарке в БД
const updateSql = "UPDATE users SET avatar = ? WHERE id = ?";
db.run(updateSql, [avatarPath, userId], function (err) {
if (err) {
console.error("Ошибка обновления аватарки:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
res.json({ success: true, avatar: avatarPath });
});
});
}
);
// API для удаления аватарки
app.delete("/api/user/avatar", requireAuth, (req, res) => {
const userId = req.session.userId;
// Получаем текущую аватарку
const getAvatarSql = "SELECT avatar FROM users WHERE id = ?";
db.get(getAvatarSql, [userId], (err, user) => {
if (err) {
console.error("Ошибка получения аватарки:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
if (!user || !user.avatar) {
return res.status(400).json({ error: "Аватарка не установлена" });
}
// Удаляем файл аватарки
const avatarPath = path.join(__dirname, "public", user.avatar);
if (fs.existsSync(avatarPath)) {
fs.unlinkSync(avatarPath);
}
// Обновляем БД
const updateSql = "UPDATE users SET avatar = NULL WHERE id = ?";
db.run(updateSql, [userId], function (err) {
if (err) {
console.error("Ошибка удаления аватарки:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
res.json({ success: true, message: "Аватарка удалена" });
});
});
});
// Выход
app.post("/logout", (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: "Ошибка выхода" });
}
res.redirect("/");
});
});
// Запуск сервера
app.listen(PORT, () => {
console.log(`🚀 Сервер запущен на порту ${PORT}`);
console.log(`📝 Приложение для заметок готово к работе!`);
});