const express = require("express"); const sqlite3 = require("sqlite3").verbose(); const bcrypt = require("bcryptjs"); const session = require("express-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:"], }, }, }) ); // Ограничение запросов (отключено для разработки) // 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()); // Настройка сессий app.use( session({ secret: process.env.SESSION_SECRET || "default-secret", resave: false, saveUninitialized: false, cookie: { secure: false }, // в продакшене установить true с HTTPS }) ); // Подключение к базе данных 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/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 для получения всех заметок 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(`📝 Приложение для заметок готово к работе!`); });