- Добавлена директория `database/` в .gitignore для исключения файлов баз данных из репозитория - Удалены устаревшие файлы, включая `CALENDAR_FEATURE.md`, `DEPLOYMENT.md`, и другие, чтобы очистить проект от ненужных артефактов
687 lines
22 KiB
JavaScript
687 lines
22 KiB
JavaScript
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 });
|
||
}
|
||
|
||
// Создаем директорию для баз данных, если её нет
|
||
const databaseDir = path.join(__dirname, "database");
|
||
if (!fs.existsSync(databaseDir)) {
|
||
fs.mkdirSync(databaseDir, { 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://cdnjs.cloudflare.com",
|
||
"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: path.join(__dirname, "database")
|
||
}),
|
||
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("./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(`📝 Приложение для заметок готово к работе!`);
|
||
});
|