NoteJS/server.js
Fovway 083ac11ab1 Добавлены функции для работы с многострочными списками и улучшено отображение дат заметок
- Реализована возможность создания нумерованных списков и улучшены функции для работы с многострочными списками.
- Обновлены фильтры для отображения заметок по дате, используя поле created_at вместо date.
- Оптимизировано отображение дат создания и изменения заметок в единой строке.
- Добавлены новые кнопки и обработчики событий для поддержки новых функций в интерфейсе редактирования заметок.
2025-10-22 12:56:43 +07:00

1195 lines
40 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 });
}
// Создаем директорию для баз данных, если её нет
const databaseDir = path.join(__dirname, "database");
if (!fs.existsSync(databaseDir)) {
fs.mkdirSync(databaseDir, { recursive: true });
}
// Настройка multer для загрузки аватарок
const avatarStorage = 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)
);
},
});
// Настройка multer для загрузки изображений заметок
const noteImageStorage = 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,
"note-image-" +
req.session.userId +
"-" +
uniqueSuffix +
path.extname(file.originalname)
);
},
});
const upload = multer({
storage: avatarStorage,
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) разрешены!"));
},
});
const uploadNoteImages = multer({
storage: noteImageStorage,
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB максимум для изображений заметок
fileFilter: function (req, file, cb) {
const filetypes = /jpeg|jpg|png|gif|webp/;
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, webp) разрешены!"));
},
});
// 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")));
// PWA файлы с правильными заголовками
app.get("/manifest.json", (req, res) => {
res.setHeader("Content-Type", "application/manifest+json");
res.setHeader("Cache-Control", "public, max-age=86400"); // 24 часа
res.sendFile(path.join(__dirname, "public", "manifest.json"));
});
app.get("/sw.js", (req, res) => {
res.setHeader("Content-Type", "application/javascript");
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.sendFile(path.join(__dirname, "public", "sw.js"));
});
app.get("/browserconfig.xml", (req, res) => {
res.setHeader("Content-Type", "application/xml");
res.setHeader("Cache-Control", "public, max-age=86400"); // 24 часа
res.sendFile(path.join(__dirname, "public", "browserconfig.xml"));
});
// Парсинг тела запроса
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 createNoteImagesTable = `
CREATE TABLE IF NOT EXISTS note_images (
id INTEGER PRIMARY KEY AUTOINCREMENT,
note_id INTEGER NOT NULL,
filename TEXT NOT NULL,
original_name TEXT NOT NULL,
file_path TEXT NOT NULL,
file_size INTEGER NOT NULL,
mime_type TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (note_id) REFERENCES notes(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(createNoteImagesTable, (err) => {
if (err) {
console.error(
"Ошибка создания таблицы изображений заметок:",
err.message
);
} else {
console.log("Таблица изображений заметок готова");
}
});
db.run(createUsersTable, (err) => {
if (err) {
console.error("Ошибка создания таблицы пользователей:", err.message);
} else {
console.log("Таблица пользователей готова");
runMigrations();
}
});
}
// Создание индексов для оптимизации запросов
function createIndexes() {
const indexes = [
"CREATE INDEX IF NOT EXISTS idx_notes_user_id ON notes(user_id)",
"CREATE INDEX IF NOT EXISTS idx_notes_created_at ON notes(created_at)",
"CREATE INDEX IF NOT EXISTS idx_notes_updated_at ON notes(updated_at)",
"CREATE INDEX IF NOT EXISTS idx_notes_date ON notes(date)",
"CREATE INDEX IF NOT EXISTS idx_note_images_note_id ON note_images(note_id)",
"CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)",
];
indexes.forEach((indexSql, i) => {
db.run(indexSql, (err) => {
if (err) {
console.error(`Ошибка создания индекса ${i + 1}:`, err.message);
} else {
console.log(`Индекс ${i + 1} создан успешно`);
}
});
});
}
// Миграции базы данных
function runMigrations() {
// Проверяем существование колонки accent_color и добавляем её если нужно
db.all("PRAGMA table_info(users)", (err, columns) => {
if (err) {
console.error("Ошибка проверки структуры таблицы users:", err.message);
return;
}
const hasAccentColor = columns.some((col) => col.name === "accent_color");
if (!hasAccentColor) {
db.run(
"ALTER TABLE users ADD COLUMN accent_color TEXT DEFAULT '#007bff'",
(err) => {
if (err) {
console.error(
"Ошибка добавления колонки accent_color:",
err.message
);
} else {
console.log("Колонка accent_color добавлена в таблицу users");
}
}
);
}
});
// Проверяем существование колонки updated_at в таблице notes и добавляем её если нужно
db.all("PRAGMA table_info(notes)", (err, columns) => {
if (err) {
console.error("Ошибка проверки структуры таблицы notes:", err.message);
return;
}
const hasUpdatedAt = columns.some((col) => col.name === "updated_at");
if (!hasUpdatedAt) {
db.run("ALTER TABLE notes ADD COLUMN updated_at DATETIME", (err) => {
if (err) {
console.error("Ошибка добавления колонки updated_at:", err.message);
} else {
console.log("Колонка updated_at добавлена в таблицу notes");
// Устанавливаем updated_at = created_at для существующих записей
db.run(
"UPDATE notes SET updated_at = created_at WHERE updated_at IS NULL",
(err) => {
if (err) {
console.error(
"Ошибка обновления updated_at для существующих записей:",
err.message
);
} else {
console.log(
"Колонка updated_at обновлена для существующих записей"
);
// Создаем индексы после добавления колонки
createIndexes();
}
}
);
}
});
} else {
// Если колонка уже существует, просто создаем индексы
createIndexes();
}
});
}
// 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, accent_color 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) => {
// Получаем цвет пользователя для предотвращения FOUC
const sql = "SELECT accent_color FROM users WHERE id = ?";
db.get(sql, [req.session.userId], (err, user) => {
if (err) {
console.error("Ошибка получения цвета пользователя:", err.message);
return res.sendFile(path.join(__dirname, "public", "notes.html"));
}
const accentColor = user?.accent_color || "#007bff";
// Читаем HTML файл
fs.readFile(
path.join(__dirname, "public", "notes.html"),
"utf8",
(err, html) => {
if (err) {
console.error("Ошибка чтения файла notes.html:", err.message);
return res.sendFile(path.join(__dirname, "public", "notes.html"));
}
// Вставляем inline CSS с правильным цветом в самое начало head для предотвращения FOUC
const inlineCSS = `<style>
:root, html { --accent-color: ${accentColor} !important; }
* { --accent-color: ${accentColor} !important; }
</style>`;
const modifiedHtml = html.replace(
/<head>/i,
`<head>\n ${inlineCSS}`
);
res.send(modifiedHtml);
}
);
});
});
// API для поиска заметок с изображениями (должен быть ПЕРЕД /api/notes/:id)
app.get("/api/notes/search", requireAuth, (req, res) => {
const { q, tag, date } = req.query;
let whereClause = "WHERE n.user_id = ?";
let params = [req.session.userId];
// Поиск по тексту
if (q && q.trim()) {
whereClause += " AND n.content LIKE ?";
params.push(`%${q.trim()}%`);
}
// Поиск по тегу
if (tag && tag.trim()) {
whereClause += " AND n.content LIKE ?";
params.push(`%#${tag.trim()}%`);
}
// Поиск по дате (используем created_at вместо date)
if (date && date.trim()) {
whereClause += " AND strftime('%d.%m.%Y', n.created_at) = ?";
params.push(date.trim());
}
const sql = `
SELECT
n.*,
CASE
WHEN COUNT(ni.id) = 0 THEN '[]'
ELSE json_group_array(
json_object(
'id', ni.id,
'filename', ni.filename,
'original_name', ni.original_name,
'file_path', ni.file_path,
'file_size', ni.file_size,
'mime_type', ni.mime_type,
'created_at', ni.created_at
)
)
END as images
FROM notes n
LEFT JOIN note_images ni ON n.id = ni.note_id
${whereClause}
GROUP BY n.id
ORDER BY n.created_at DESC
`;
db.all(sql, params, (err, rows) => {
if (err) {
console.error("Ошибка поиска заметок:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
// Парсим JSON строки изображений
const notesWithImages = rows.map((row) => ({
...row,
images: row.images === "[]" ? [] : JSON.parse(row.images),
}));
res.json(notesWithImages);
});
});
// API для получения всех заметок с изображениями
app.get("/api/notes", requireAuth, (req, res) => {
const sql = `
SELECT
n.*,
CASE
WHEN COUNT(ni.id) = 0 THEN '[]'
ELSE json_group_array(
json_object(
'id', ni.id,
'filename', ni.filename,
'original_name', ni.original_name,
'file_path', ni.file_path,
'file_size', ni.file_size,
'mime_type', ni.mime_type,
'created_at', ni.created_at
)
)
END as images
FROM notes n
LEFT JOIN note_images ni ON n.id = ni.note_id
WHERE n.user_id = ?
GROUP BY n.id
ORDER BY n.created_at DESC
`;
db.all(sql, [req.session.userId], (err, rows) => {
if (err) {
console.error("Ошибка получения заметок:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
// Парсим JSON строки изображений
const notesWithImages = rows.map((row) => ({
...row,
images: row.images === "[]" ? [] : JSON.parse(row.images),
}));
res.json(notesWithImages);
});
});
// 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, created_at, updated_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)";
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 } = req.body;
const { id } = req.params;
if (!content) {
return res.status(400).json({ error: "Содержание заметки обязательно" });
}
// Проверяем, что заметка принадлежит текущему пользователю
const checkSql = "SELECT user_id, date, time 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 = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?";
const params = [content, 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: row.date, time: row.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 getImagesSql = "SELECT file_path FROM note_images WHERE note_id = ?";
db.all(getImagesSql, [id], (err, images) => {
if (err) {
console.error("Ошибка получения изображений:", err.message);
} else {
// Удаляем файлы изображений
images.forEach((image) => {
const imagePath = path.join(__dirname, "public", image.file_path);
if (fs.existsSync(imagePath)) {
fs.unlinkSync(imagePath);
}
});
}
// Удаляем записи об изображениях из БД
const deleteImagesSql = "DELETE FROM note_images WHERE note_id = ?";
db.run(deleteImagesSql, [id], (err) => {
if (err) {
console.error("Ошибка удаления изображений:", err.message);
}
});
// Удаляем саму заметку
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: "Заметка удалена" });
});
});
});
});
// API для загрузки изображений к заметке
app.post(
"/api/notes/:id/images",
requireAuth,
uploadNoteImages.array("images", 10), // Максимум 10 изображений
(req, res) => {
const { id } = req.params;
const files = req.files;
if (!files || files.length === 0) {
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 insertSql = `
INSERT INTO note_images (note_id, filename, original_name, file_path, file_size, mime_type)
VALUES (?, ?, ?, ?, ?, ?)
`;
const uploadedImages = [];
let completed = 0;
files.forEach((file) => {
const filePath = "/uploads/" + file.filename;
const params = [
id,
file.filename,
file.originalname,
filePath,
file.size,
file.mimetype,
];
db.run(insertSql, params, function (err) {
if (err) {
console.error("Ошибка сохранения изображения:", err.message);
// Удаляем файл, если не удалось сохранить в БД
const imagePath = path.join(__dirname, "public", filePath);
if (fs.existsSync(imagePath)) {
fs.unlinkSync(imagePath);
}
} else {
uploadedImages.push({
id: this.lastID,
filename: file.filename,
original_name: file.originalname,
file_path: filePath,
file_size: file.size,
mime_type: file.mimetype,
});
}
completed++;
if (completed === files.length) {
if (uploadedImages.length === 0) {
return res
.status(500)
.json({ error: "Не удалось загрузить изображения" });
}
res.json({
success: true,
message: `Загружено ${uploadedImages.length} изображений`,
images: uploadedImages,
});
}
});
});
});
}
);
// API для получения изображений заметки
app.get("/api/notes/:id/images", 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 getImagesSql = `
SELECT id, filename, original_name, file_path, file_size, mime_type, created_at
FROM note_images
WHERE note_id = ?
ORDER BY created_at ASC
`;
db.all(getImagesSql, [id], (err, images) => {
if (err) {
console.error("Ошибка получения изображений:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
res.json(images);
});
});
});
// API для удаления изображения заметки
app.delete("/api/notes/:noteId/images/:imageId", requireAuth, (req, res) => {
const { noteId, imageId } = req.params;
// Проверяем, что заметка принадлежит текущему пользователю
const checkSql = "SELECT user_id FROM notes WHERE id = ?";
db.get(checkSql, [noteId], (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 getImageSql =
"SELECT file_path FROM note_images WHERE id = ? AND note_id = ?";
db.get(getImageSql, [imageId, noteId], (err, image) => {
if (err) {
console.error("Ошибка получения изображения:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
if (!image) {
return res.status(404).json({ error: "Изображение не найдено" });
}
// Удаляем файл изображения
const imagePath = path.join(__dirname, "public", image.file_path);
if (fs.existsSync(imagePath)) {
fs.unlinkSync(imagePath);
}
// Удаляем запись из БД
const deleteSql = "DELETE FROM note_images WHERE id = ? AND note_id = ?";
db.run(deleteSql, [imageId, noteId], 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({ success: true, message: "Изображение удалено" });
});
});
});
});
// Страница личного кабинета
app.get("/profile", requireAuth, (req, res) => {
// Получаем цвет пользователя для предотвращения FOUC
const sql = "SELECT accent_color FROM users WHERE id = ?";
db.get(sql, [req.session.userId], (err, user) => {
if (err) {
console.error("Ошибка получения цвета пользователя:", err.message);
return res.sendFile(path.join(__dirname, "public", "profile.html"));
}
const accentColor = user?.accent_color || "#007bff";
// Читаем HTML файл
fs.readFile(
path.join(__dirname, "public", "profile.html"),
"utf8",
(err, html) => {
if (err) {
console.error("Ошибка чтения файла profile.html:", err.message);
return res.sendFile(path.join(__dirname, "public", "profile.html"));
}
// Вставляем inline CSS с правильным цветом в самое начало head для предотвращения FOUC
const inlineCSS = `<style>
:root, html { --accent-color: ${accentColor} !important; }
* { --accent-color: ${accentColor} !important; }
</style>`;
const modifiedHtml = html.replace(
/<head>/i,
`<head>\n ${inlineCSS}`
);
res.send(modifiedHtml);
}
);
});
});
// API для обновления профиля
app.put("/api/user/profile", requireAuth, async (req, res) => {
const { username, email, currentPassword, newPassword, accent_color } =
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 (accent_color !== undefined) {
updateFields.push("accent_color = ?");
params.push(accent_color || "#007bff");
}
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(`📝 Приложение для заметок готово к работе!`);
});