4020 lines
147 KiB
JavaScript
4020 lines
147 KiB
JavaScript
// Загружаем .env файл ПЕРВЫМ, до всех остальных импортов
|
||
require("dotenv").config();
|
||
|
||
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");
|
||
const https = require("https");
|
||
const http = require("http");
|
||
const speakeasy = require("speakeasy");
|
||
const QRCode = require("qrcode");
|
||
const { encrypt, decrypt } = require("./utils/encryption");
|
||
|
||
const app = express();
|
||
const PORT = process.env.PORT || 3001;
|
||
|
||
// Настройка trust proxy для nginx proxy manager
|
||
// Доверяем всем прокси (nginx proxy manager должен передавать X-Forwarded-For)
|
||
app.set("trust proxy", true);
|
||
|
||
// Создаем директорию для аватарок, если её нет
|
||
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) разрешены!"));
|
||
},
|
||
});
|
||
|
||
// Настройка multer для загрузки файлов заметок
|
||
const noteFileStorage = 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-file-" +
|
||
req.session.userId +
|
||
"-" +
|
||
uniqueSuffix +
|
||
path.extname(file.originalname)
|
||
);
|
||
},
|
||
});
|
||
|
||
const uploadNoteFiles = multer({
|
||
storage: noteFileStorage,
|
||
limits: { fileSize: 50 * 1024 * 1024 }, // 50MB максимум для файлов заметок
|
||
fileFilter: function (req, file, cb) {
|
||
// Разрешенные типы файлов
|
||
const allowedMimes = [
|
||
"application/pdf",
|
||
"application/msword",
|
||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||
"application/vnd.ms-excel",
|
||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||
"text/plain",
|
||
"application/zip",
|
||
"application/x-zip-compressed",
|
||
"application/x-rar-compressed",
|
||
"application/x-7z-compressed",
|
||
];
|
||
|
||
const allowedExtensions = /pdf|doc|docx|xls|xlsx|txt|zip|rar|7z/;
|
||
const extname = allowedExtensions.test(
|
||
path.extname(file.originalname).toLowerCase()
|
||
);
|
||
|
||
if (allowedMimes.includes(file.mimetype) || extname) {
|
||
return cb(null, true);
|
||
}
|
||
cb(
|
||
new Error(
|
||
"Только файлы форматов pdf, doc, docx, xls, xlsx, txt, zip, rar, 7z разрешены!"
|
||
)
|
||
);
|
||
},
|
||
});
|
||
|
||
// 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 createNoteFilesTable = `
|
||
CREATE TABLE IF NOT EXISTS note_files (
|
||
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(createNoteFilesTable, (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_notes_is_pinned ON notes(is_pinned)",
|
||
"CREATE INDEX IF NOT EXISTS idx_notes_is_archived ON notes(is_archived)",
|
||
"CREATE INDEX IF NOT EXISTS idx_notes_pinned_at ON notes(pinned_at)",
|
||
"CREATE INDEX IF NOT EXISTS idx_note_images_note_id ON note_images(note_id)",
|
||
"CREATE INDEX IF NOT EXISTS idx_note_files_note_id ON note_files(note_id)",
|
||
"CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)",
|
||
"CREATE INDEX IF NOT EXISTS idx_action_logs_user_id ON action_logs(user_id)",
|
||
"CREATE INDEX IF NOT EXISTS idx_action_logs_created_at ON action_logs(created_at)",
|
||
];
|
||
|
||
indexes.forEach((indexSql, i) => {
|
||
db.run(indexSql, (err) => {
|
||
if (err) {
|
||
console.error(`Ошибка создания индекса ${i + 1}:`, err.message);
|
||
} else {
|
||
console.log(`Индекс ${i + 1} создан успешно`);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// Функция для логирования действий пользователя
|
||
function logAction(userId, actionType, details) {
|
||
const sql = `
|
||
INSERT INTO action_logs (user_id, action_type, details, ip_address)
|
||
VALUES (?, ?, ?, NULL)
|
||
`;
|
||
db.run(sql, [userId, actionType, details], (err) => {
|
||
if (err) {
|
||
console.error("Ошибка логирования действия:", err.message);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Миграции базы данных
|
||
function runMigrations() {
|
||
// Создаем таблицу логов действий, если её нет
|
||
const createLogsTable = `
|
||
CREATE TABLE IF NOT EXISTS action_logs (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
action_type TEXT NOT NULL,
|
||
details TEXT,
|
||
ip_address TEXT,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||
)
|
||
`;
|
||
|
||
db.run(createLogsTable, (err) => {
|
||
if (err) {
|
||
console.error("Ошибка создания таблицы логов:", err.message);
|
||
} else {
|
||
console.log("Таблица action_logs готова");
|
||
}
|
||
});
|
||
|
||
// Проверяем существование колонки 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");
|
||
}
|
||
}
|
||
);
|
||
}
|
||
|
||
// Проверяем существование колонок для AI настроек
|
||
const hasOpenaiApiKey = columns.some(
|
||
(col) => col.name === "openai_api_key"
|
||
);
|
||
const hasOpenaiBaseUrl = columns.some(
|
||
(col) => col.name === "openai_base_url"
|
||
);
|
||
const hasOpenaiModel = columns.some((col) => col.name === "openai_model");
|
||
|
||
if (!hasOpenaiApiKey) {
|
||
db.run("ALTER TABLE users ADD COLUMN openai_api_key TEXT", (err) => {
|
||
if (err) {
|
||
console.error(
|
||
"Ошибка добавления колонки openai_api_key:",
|
||
err.message
|
||
);
|
||
} else {
|
||
console.log("Колонка openai_api_key добавлена в таблицу users");
|
||
}
|
||
});
|
||
}
|
||
|
||
if (!hasOpenaiBaseUrl) {
|
||
db.run("ALTER TABLE users ADD COLUMN openai_base_url TEXT", (err) => {
|
||
if (err) {
|
||
console.error(
|
||
"Ошибка добавления колонки openai_base_url:",
|
||
err.message
|
||
);
|
||
} else {
|
||
console.log("Колонка openai_base_url добавлена в таблицу users");
|
||
}
|
||
});
|
||
}
|
||
|
||
if (!hasOpenaiModel) {
|
||
db.run("ALTER TABLE users ADD COLUMN openai_model TEXT", (err) => {
|
||
if (err) {
|
||
console.error("Ошибка добавления колонки openai_model:", err.message);
|
||
} else {
|
||
console.log("Колонка openai_model добавлена в таблицу users");
|
||
}
|
||
});
|
||
}
|
||
|
||
// Проверяем существование колонки show_edit_date
|
||
const hasShowEditDate = columns.some(
|
||
(col) => col.name === "show_edit_date"
|
||
);
|
||
if (!hasShowEditDate) {
|
||
db.run(
|
||
"ALTER TABLE users ADD COLUMN show_edit_date INTEGER DEFAULT 1",
|
||
(err) => {
|
||
if (err) {
|
||
console.error(
|
||
"Ошибка добавления колонки show_edit_date:",
|
||
err.message
|
||
);
|
||
} else {
|
||
console.log("Колонка show_edit_date добавлена в таблицу users");
|
||
}
|
||
}
|
||
);
|
||
}
|
||
|
||
// Проверяем существование колонки colored_icons
|
||
const hasColoredIcons = columns.some((col) => col.name === "colored_icons");
|
||
if (!hasColoredIcons) {
|
||
db.run(
|
||
"ALTER TABLE users ADD COLUMN colored_icons INTEGER DEFAULT 1",
|
||
(err) => {
|
||
if (err) {
|
||
console.error(
|
||
"Ошибка добавления колонки colored_icons:",
|
||
err.message
|
||
);
|
||
} else {
|
||
console.log("Колонка colored_icons добавлена в таблицу users");
|
||
}
|
||
}
|
||
);
|
||
}
|
||
|
||
// Проверяем существование колонки floating_toolbar_enabled
|
||
const hasFloatingToolbarEnabled = columns.some(
|
||
(col) => col.name === "floating_toolbar_enabled"
|
||
);
|
||
if (!hasFloatingToolbarEnabled) {
|
||
db.run(
|
||
"ALTER TABLE users ADD COLUMN floating_toolbar_enabled INTEGER DEFAULT 1",
|
||
(err) => {
|
||
if (err) {
|
||
console.error(
|
||
"Ошибка добавления колонки floating_toolbar_enabled:",
|
||
err.message
|
||
);
|
||
} else {
|
||
console.log(
|
||
"Колонка floating_toolbar_enabled добавлена в таблицу users"
|
||
);
|
||
}
|
||
}
|
||
);
|
||
}
|
||
|
||
// Проверяем существование колонки ai_enabled
|
||
const hasAiEnabled = columns.some((col) => col.name === "ai_enabled");
|
||
if (!hasAiEnabled) {
|
||
db.run(
|
||
"ALTER TABLE users ADD COLUMN ai_enabled INTEGER DEFAULT 0",
|
||
(err) => {
|
||
if (err) {
|
||
console.error("Ошибка добавления колонки ai_enabled:", err.message);
|
||
} else {
|
||
console.log("Колонка ai_enabled добавлена в таблицу users");
|
||
}
|
||
}
|
||
);
|
||
}
|
||
|
||
// Проверяем существование колонок для 2FA
|
||
const hasTwoFactorEnabled = columns.some(
|
||
(col) => col.name === "two_factor_enabled"
|
||
);
|
||
if (!hasTwoFactorEnabled) {
|
||
db.run(
|
||
"ALTER TABLE users ADD COLUMN two_factor_enabled INTEGER DEFAULT 0",
|
||
(err) => {
|
||
if (err) {
|
||
console.error(
|
||
"Ошибка добавления колонки two_factor_enabled:",
|
||
err.message
|
||
);
|
||
} else {
|
||
console.log(
|
||
"Колонка two_factor_enabled добавлена в таблицу users"
|
||
);
|
||
}
|
||
}
|
||
);
|
||
}
|
||
|
||
const hasTwoFactorSecret = columns.some(
|
||
(col) => col.name === "two_factor_secret"
|
||
);
|
||
if (!hasTwoFactorSecret) {
|
||
db.run(
|
||
"ALTER TABLE users ADD COLUMN two_factor_secret TEXT",
|
||
(err) => {
|
||
if (err) {
|
||
console.error(
|
||
"Ошибка добавления колонки two_factor_secret:",
|
||
err.message
|
||
);
|
||
} else {
|
||
console.log(
|
||
"Колонка two_factor_secret добавлена в таблицу users"
|
||
);
|
||
}
|
||
}
|
||
);
|
||
}
|
||
|
||
const hasTwoFactorBackupCodes = columns.some(
|
||
(col) => col.name === "two_factor_backup_codes"
|
||
);
|
||
if (!hasTwoFactorBackupCodes) {
|
||
db.run(
|
||
"ALTER TABLE users ADD COLUMN two_factor_backup_codes TEXT",
|
||
(err) => {
|
||
if (err) {
|
||
console.error(
|
||
"Ошибка добавления колонки two_factor_backup_codes:",
|
||
err.message
|
||
);
|
||
} else {
|
||
console.log(
|
||
"Колонка two_factor_backup_codes добавлена в таблицу users"
|
||
);
|
||
}
|
||
}
|
||
);
|
||
}
|
||
|
||
// Проверяем существование колонки is_public_profile
|
||
const hasIsPublicProfile = columns.some(
|
||
(col) => col.name === "is_public_profile"
|
||
);
|
||
if (!hasIsPublicProfile) {
|
||
db.run(
|
||
"ALTER TABLE users ADD COLUMN is_public_profile INTEGER DEFAULT 0",
|
||
(err) => {
|
||
if (err) {
|
||
console.error(
|
||
"Ошибка добавления колонки is_public_profile:",
|
||
err.message
|
||
);
|
||
} else {
|
||
console.log(
|
||
"Колонка is_public_profile добавлена в таблицу users"
|
||
);
|
||
}
|
||
}
|
||
);
|
||
}
|
||
});
|
||
|
||
// Проверяем существование колонок в таблице 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");
|
||
const hasPinned = columns.some((col) => col.name === "is_pinned");
|
||
const hasArchived = columns.some((col) => col.name === "is_archived");
|
||
const hasPinnedAt = columns.some((col) => col.name === "pinned_at");
|
||
|
||
// Добавляем 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");
|
||
db.run(
|
||
"UPDATE notes SET updated_at = created_at WHERE updated_at IS NULL",
|
||
(err) => {
|
||
if (err) {
|
||
console.error(
|
||
"Ошибка обновления updated_at для существующих записей:",
|
||
err.message
|
||
);
|
||
}
|
||
}
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Добавляем is_pinned если нужно
|
||
if (!hasPinned) {
|
||
db.run(
|
||
"ALTER TABLE notes ADD COLUMN is_pinned INTEGER DEFAULT 0",
|
||
(err) => {
|
||
if (err) {
|
||
console.error("Ошибка добавления колонки is_pinned:", err.message);
|
||
} else {
|
||
console.log("Колонка is_pinned добавлена в таблицу notes");
|
||
}
|
||
}
|
||
);
|
||
}
|
||
|
||
// Добавляем is_archived если нужно
|
||
if (!hasArchived) {
|
||
db.run(
|
||
"ALTER TABLE notes ADD COLUMN is_archived INTEGER DEFAULT 0",
|
||
(err) => {
|
||
if (err) {
|
||
console.error(
|
||
"Ошибка добавления колонки is_archived:",
|
||
err.message
|
||
);
|
||
} else {
|
||
console.log("Колонка is_archived добавлена в таблицу notes");
|
||
}
|
||
}
|
||
);
|
||
}
|
||
|
||
// Добавляем pinned_at если нужно
|
||
if (!hasPinnedAt) {
|
||
db.run("ALTER TABLE notes ADD COLUMN pinned_at DATETIME", (err) => {
|
||
if (err) {
|
||
console.error("Ошибка добавления колонки pinned_at:", err.message);
|
||
} else {
|
||
console.log("Колонка pinned_at добавлена в таблицу notes");
|
||
}
|
||
});
|
||
}
|
||
|
||
// Создаем индексы после всех изменений
|
||
if (hasUpdatedAt && hasPinned && hasArchived) {
|
||
createIndexes();
|
||
} else {
|
||
// Задержка для завершения миграций
|
||
setTimeout(createIndexes, 1000);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Middleware для аутентификации
|
||
function requireAuth(req, res, next) {
|
||
if (req.session.authenticated) {
|
||
return next();
|
||
} else {
|
||
return res.redirect("/");
|
||
}
|
||
}
|
||
|
||
// Middleware для аутентификации API (возвращает JSON вместо редиректа)
|
||
function requireApiAuth(req, res, next) {
|
||
if (req.session.authenticated) {
|
||
return next();
|
||
} else {
|
||
return res.status(401).json({ error: "Не аутентифицирован" });
|
||
}
|
||
}
|
||
|
||
// Маршруты
|
||
|
||
// Главная страница с формой входа
|
||
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", "index.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;
|
||
|
||
// Логируем регистрацию
|
||
logAction(this.lastID, "register", `Регистрация нового пользователя`);
|
||
|
||
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: "Неверный логин или пароль" });
|
||
}
|
||
|
||
// Проверяем, включена ли 2FA
|
||
if (user.two_factor_enabled === 1) {
|
||
// Создаем промежуточную сессию для проверки 2FA
|
||
req.session.twoFactorPending = true;
|
||
req.session.twoFactorUsername = username;
|
||
req.session.twoFactorUserId = user.id;
|
||
|
||
// НЕ создаем полную сессию, только промежуточную
|
||
res.json({
|
||
success: true,
|
||
requires2FA: true,
|
||
message: "Требуется код двухфакторной аутентификации",
|
||
});
|
||
return;
|
||
}
|
||
|
||
// Если 2FA не включена, создаем полную сессию как обычно
|
||
req.session.userId = user.id;
|
||
req.session.username = user.username;
|
||
req.session.authenticated = true;
|
||
|
||
// Логируем вход
|
||
logAction(user.id, "login", `Вход в систему`);
|
||
|
||
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 {
|
||
// Возвращаем 200, так как неавторизованное состояние - это норма, а не ошибка
|
||
res.status(200).json({ authenticated: false });
|
||
}
|
||
});
|
||
|
||
// API для получения информации о пользователе
|
||
app.get("/api/user", requireApiAuth, (req, res) => {
|
||
if (!req.session.userId) {
|
||
return res.status(401).json({ error: "Не аутентифицирован" });
|
||
}
|
||
|
||
const sql =
|
||
"SELECT username, email, avatar, accent_color, show_edit_date, colored_icons, floating_toolbar_enabled, two_factor_enabled, is_public_profile 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);
|
||
});
|
||
});
|
||
|
||
// Публичный API для получения профиля пользователя по логину
|
||
app.get("/api/public/user/:username", (req, res) => {
|
||
const { username } = req.params;
|
||
|
||
const sql =
|
||
"SELECT username, avatar, is_public_profile FROM users WHERE username = ?";
|
||
db.get(sql, [username], (err, user) => {
|
||
if (err) {
|
||
console.error("Ошибка получения публичного профиля:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
if (!user) {
|
||
return res.status(404).json({ error: "Пользователь не найден" });
|
||
}
|
||
|
||
if (user.is_public_profile !== 1) {
|
||
return res.status(403).json({ error: "Профиль не является публичным" });
|
||
}
|
||
|
||
// Возвращаем только публичную информацию
|
||
res.json({
|
||
username: user.username,
|
||
avatar: user.avatar,
|
||
});
|
||
});
|
||
});
|
||
|
||
// Публичный API для получения изображения заметки (для публичных профилей)
|
||
app.get("/api/public/notes/:id/images/:imageId", (req, res) => {
|
||
const { id, imageId } = req.params;
|
||
|
||
// Проверяем, что заметка принадлежит пользователю с публичным профилем
|
||
const checkNoteSql = `
|
||
SELECT n.user_id, u.is_public_profile
|
||
FROM notes n
|
||
INNER JOIN users u ON n.user_id = u.id
|
||
WHERE n.id = ? AND n.is_archived = 0
|
||
`;
|
||
db.get(checkNoteSql, [id], (err, note) => {
|
||
if (err) {
|
||
console.error("Ошибка проверки заметки:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
if (!note) {
|
||
return res.status(404).json({ error: "Заметка не найдена" });
|
||
}
|
||
|
||
if (note.is_public_profile !== 1) {
|
||
return res.status(403).json({ error: "Профиль не является публичным" });
|
||
}
|
||
|
||
// Получаем информацию об изображении
|
||
const imageSql = "SELECT file_path FROM note_images WHERE id = ? AND note_id = ?";
|
||
db.get(imageSql, [imageId, id], (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)) {
|
||
res.sendFile(imagePath);
|
||
} else {
|
||
res.status(404).json({ error: "Файл изображения не найден" });
|
||
}
|
||
});
|
||
});
|
||
});
|
||
|
||
// Публичный API для получения файла заметки (для публичных профилей)
|
||
app.get("/api/public/notes/:id/files/:fileId", (req, res) => {
|
||
const { id, fileId } = req.params;
|
||
|
||
// Проверяем, что заметка принадлежит пользователю с публичным профилем
|
||
const checkNoteSql = `
|
||
SELECT n.user_id, u.is_public_profile
|
||
FROM notes n
|
||
INNER JOIN users u ON n.user_id = u.id
|
||
WHERE n.id = ? AND n.is_archived = 0
|
||
`;
|
||
db.get(checkNoteSql, [id], (err, note) => {
|
||
if (err) {
|
||
console.error("Ошибка проверки заметки:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
if (!note) {
|
||
return res.status(404).json({ error: "Заметка не найдена" });
|
||
}
|
||
|
||
if (note.is_public_profile !== 1) {
|
||
return res.status(403).json({ error: "Профиль не является публичным" });
|
||
}
|
||
|
||
// Получаем информацию о файле
|
||
const fileSql = "SELECT file_path, original_name FROM note_files WHERE id = ? AND note_id = ?";
|
||
db.get(fileSql, [fileId, id], (err, file) => {
|
||
if (err) {
|
||
console.error("Ошибка получения файла:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
if (!file) {
|
||
return res.status(404).json({ error: "Файл не найден" });
|
||
}
|
||
|
||
const filePath = path.join(__dirname, "public", file.file_path);
|
||
if (fs.existsSync(filePath)) {
|
||
res.download(filePath, file.original_name);
|
||
} else {
|
||
res.status(404).json({ error: "Файл не найден" });
|
||
}
|
||
});
|
||
});
|
||
});
|
||
|
||
// Публичный API для получения заметок пользователя по логину
|
||
app.get("/api/public/user/:username/notes", (req, res) => {
|
||
const { username } = req.params;
|
||
|
||
// Сначала проверяем, что пользователь существует и профиль публичный
|
||
const checkUserSql =
|
||
"SELECT id, is_public_profile FROM users WHERE username = ?";
|
||
db.get(checkUserSql, [username], (err, user) => {
|
||
if (err) {
|
||
console.error("Ошибка проверки пользователя:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
if (!user) {
|
||
return res.status(404).json({ error: "Пользователь не найден" });
|
||
}
|
||
|
||
if (user.is_public_profile !== 1) {
|
||
return res.status(403).json({ error: "Профиль не является публичным" });
|
||
}
|
||
|
||
// Получаем публичные заметки пользователя (не архивные)
|
||
const sql = `
|
||
SELECT
|
||
n.*,
|
||
CASE
|
||
WHEN COUNT(DISTINCT ni.id) = 0 THEN '[]'
|
||
ELSE json_group_array(
|
||
DISTINCT 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
|
||
)
|
||
) FILTER (WHERE ni.id IS NOT NULL)
|
||
END as images,
|
||
CASE
|
||
WHEN COUNT(DISTINCT nf.id) = 0 THEN '[]'
|
||
ELSE json_group_array(
|
||
DISTINCT json_object(
|
||
'id', nf.id,
|
||
'filename', nf.filename,
|
||
'original_name', nf.original_name,
|
||
'file_path', nf.file_path,
|
||
'file_size', nf.file_size,
|
||
'mime_type', nf.mime_type,
|
||
'created_at', nf.created_at
|
||
)
|
||
) FILTER (WHERE nf.id IS NOT NULL)
|
||
END as files
|
||
FROM notes n
|
||
LEFT JOIN note_images ni ON n.id = ni.note_id
|
||
LEFT JOIN note_files nf ON n.id = nf.note_id
|
||
WHERE n.user_id = ? AND n.is_archived = 0
|
||
GROUP BY n.id
|
||
ORDER BY n.is_pinned DESC, n.pinned_at DESC, n.created_at DESC
|
||
`;
|
||
|
||
db.all(sql, [user.id], (err, rows) => {
|
||
if (err) {
|
||
console.error("Ошибка получения публичных заметок:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
// Парсим JSON строки изображений и файлов
|
||
const notesWithImagesAndFiles = rows.map((row) => ({
|
||
...row,
|
||
content: decrypt(row.content), // Дешифруем содержимое заметки
|
||
images: row.images === "[]" ? [] : JSON.parse(row.images),
|
||
files: row.files === "[]" ? [] : JSON.parse(row.files),
|
||
}));
|
||
|
||
res.json(notesWithImagesAndFiles);
|
||
});
|
||
});
|
||
});
|
||
|
||
// API для получения статуса 2FA
|
||
app.get("/api/user/2fa/status", requireApiAuth, (req, res) => {
|
||
const userId = req.session.userId;
|
||
|
||
const sql = "SELECT two_factor_enabled FROM users WHERE id = ?";
|
||
db.get(sql, [userId], (err, user) => {
|
||
if (err) {
|
||
console.error("Ошибка получения статуса 2FA:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
if (!user) {
|
||
return res.status(404).json({ error: "Пользователь не найден" });
|
||
}
|
||
|
||
res.json({
|
||
twoFactorEnabled: user.two_factor_enabled === 1,
|
||
});
|
||
});
|
||
});
|
||
|
||
// Публичная страница профиля (не требует аутентификации)
|
||
app.get("/public/:username", (req, res) => {
|
||
// Просто возвращаем index.html для SPA
|
||
// React Router на фронтенде обработает маршрут
|
||
res.sendFile(path.join(__dirname, "public", "index.html"));
|
||
});
|
||
|
||
// Страница с заметками (требует аутентификации)
|
||
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", "index.html"));
|
||
}
|
||
|
||
const accentColor = user?.accent_color || "#007bff";
|
||
|
||
// Читаем HTML файл
|
||
fs.readFile(
|
||
path.join(__dirname, "public", "index.html"),
|
||
"utf8",
|
||
(err, html) => {
|
||
if (err) {
|
||
console.error("Ошибка чтения файла index.html:", err.message);
|
||
return res.sendFile(path.join(__dirname, "public", "index.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", requireApiAuth, (req, res) => {
|
||
if (!req.session.userId) {
|
||
return res.status(401).json({ error: "Не аутентифицирован" });
|
||
}
|
||
|
||
const { q, tag, date } = req.query;
|
||
|
||
let whereClause = "WHERE n.user_id = ? AND n.is_archived = 0";
|
||
let params = [req.session.userId];
|
||
|
||
// Поиск по тексту - убран из SQL, так как зашифрованный текст нельзя искать
|
||
// Поиск будет выполняться на клиенте после дешифрования
|
||
// if (q && q.trim()) {
|
||
// whereClause += " AND LOWER(n.content) LIKE ?";
|
||
// params.push(`%${q.trim().toLowerCase()}%`);
|
||
// }
|
||
|
||
// Поиск по тегу (регистронезависимый)
|
||
// SQLite LOWER() плохо работает с кириллицей, поэтому фильтрация по тегу выполняется на клиенте
|
||
// Здесь не фильтруем по тегу, чтобы получить все заметки для последующей фильтрации на клиенте
|
||
// Если есть другие фильтры (дата, поиск), они применяются здесь
|
||
// if (tag && 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(DISTINCT ni.id) = 0 THEN '[]'
|
||
ELSE json_group_array(
|
||
DISTINCT 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
|
||
)
|
||
) FILTER (WHERE ni.id IS NOT NULL)
|
||
END as images,
|
||
CASE
|
||
WHEN COUNT(DISTINCT nf.id) = 0 THEN '[]'
|
||
ELSE json_group_array(
|
||
DISTINCT json_object(
|
||
'id', nf.id,
|
||
'filename', nf.filename,
|
||
'original_name', nf.original_name,
|
||
'file_path', nf.file_path,
|
||
'file_size', nf.file_size,
|
||
'mime_type', nf.mime_type,
|
||
'created_at', nf.created_at
|
||
)
|
||
) FILTER (WHERE nf.id IS NOT NULL)
|
||
END as files
|
||
FROM notes n
|
||
LEFT JOIN note_images ni ON n.id = ni.note_id
|
||
LEFT JOIN note_files nf ON n.id = nf.note_id
|
||
${whereClause}
|
||
GROUP BY n.id
|
||
ORDER BY n.is_pinned DESC, n.pinned_at DESC, n.created_at DESC
|
||
`;
|
||
|
||
db.all(sql, params, (err, rows) => {
|
||
if (err) {
|
||
console.error("Ошибка поиска заметок:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
// Логируем для отладки
|
||
console.log(
|
||
`Поиск заметок: userId=${req.session.userId}, date=${
|
||
date || "none"
|
||
}, найдено: ${rows.length}`
|
||
);
|
||
|
||
// Парсим JSON строки изображений и файлов
|
||
const notesWithImagesAndFiles = rows.map((row) => ({
|
||
...row,
|
||
content: decrypt(row.content), // Дешифруем содержимое заметки
|
||
images: row.images === "[]" ? [] : JSON.parse(row.images),
|
||
files: row.files === "[]" ? [] : JSON.parse(row.files),
|
||
}));
|
||
|
||
// Отключаем кэширование ответов
|
||
res.set("Cache-Control", "no-store, no-cache, must-revalidate, private");
|
||
res.set("Pragma", "no-cache");
|
||
res.set("Expires", "0");
|
||
|
||
res.json(notesWithImagesAndFiles);
|
||
});
|
||
});
|
||
|
||
// API для получения всех заметок с изображениями и файлами (исключая архивные)
|
||
app.get("/api/notes", requireApiAuth, (req, res) => {
|
||
const sql = `
|
||
SELECT
|
||
n.*,
|
||
CASE
|
||
WHEN COUNT(DISTINCT ni.id) = 0 THEN '[]'
|
||
ELSE json_group_array(
|
||
DISTINCT 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
|
||
)
|
||
) FILTER (WHERE ni.id IS NOT NULL)
|
||
END as images,
|
||
CASE
|
||
WHEN COUNT(DISTINCT nf.id) = 0 THEN '[]'
|
||
ELSE json_group_array(
|
||
DISTINCT json_object(
|
||
'id', nf.id,
|
||
'filename', nf.filename,
|
||
'original_name', nf.original_name,
|
||
'file_path', nf.file_path,
|
||
'file_size', nf.file_size,
|
||
'mime_type', nf.mime_type,
|
||
'created_at', nf.created_at
|
||
)
|
||
) FILTER (WHERE nf.id IS NOT NULL)
|
||
END as files
|
||
FROM notes n
|
||
LEFT JOIN note_images ni ON n.id = ni.note_id
|
||
LEFT JOIN note_files nf ON n.id = nf.note_id
|
||
WHERE n.user_id = ? AND n.is_archived = 0
|
||
GROUP BY n.id
|
||
ORDER BY n.is_pinned DESC, n.pinned_at DESC, 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 notesWithImagesAndFiles = rows.map((row) => ({
|
||
...row,
|
||
content: decrypt(row.content), // Дешифруем содержимое заметки
|
||
images: row.images === "[]" ? [] : JSON.parse(row.images),
|
||
files: row.files === "[]" ? [] : JSON.parse(row.files),
|
||
}));
|
||
|
||
res.json(notesWithImagesAndFiles);
|
||
});
|
||
});
|
||
|
||
// API для создания новой заметки
|
||
app.post("/api/notes", requireApiAuth, (req, res) => {
|
||
const { content, date, time } = req.body;
|
||
|
||
if (!content || !date || !time) {
|
||
return res.status(400).json({ error: "Не все поля заполнены" });
|
||
}
|
||
|
||
// Шифруем содержимое заметки перед сохранением
|
||
const encryptedContent = encrypt(content);
|
||
|
||
const sql =
|
||
"INSERT INTO notes (user_id, content, date, time, created_at, updated_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)";
|
||
const params = [req.session.userId, encryptedContent, date, time];
|
||
|
||
db.run(sql, params, function (err) {
|
||
if (err) {
|
||
console.error("Ошибка создания заметки:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
// Логируем создание заметки
|
||
const noteId = this.lastID;
|
||
logAction(req.session.userId, "note_create", `Создана заметка #${noteId}`);
|
||
|
||
res.json({ id: noteId, content, date, time });
|
||
});
|
||
});
|
||
|
||
// API для обновления заметки
|
||
app.put("/api/notes/:id", requireApiAuth, (req, res) => {
|
||
const { content, skipTimestamp } = req.body;
|
||
const { id } = req.params;
|
||
|
||
if (!content) {
|
||
return res.status(400).json({ error: "Содержание заметки обязательно" });
|
||
}
|
||
|
||
// Шифруем содержимое заметки перед сохранением
|
||
const encryptedContent = encrypt(content);
|
||
|
||
// Проверяем, что заметка принадлежит текущему пользователю
|
||
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: "Нет доступа к этой заметке" });
|
||
}
|
||
|
||
// Если skipTimestamp=true (обновление чекбокса), не обновляем updated_at
|
||
const updateSql = skipTimestamp
|
||
? "UPDATE notes SET content = ? WHERE id = ?"
|
||
: "UPDATE notes SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?";
|
||
const params = [encryptedContent, 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: "Заметка не найдена" });
|
||
}
|
||
|
||
// Логируем обновление заметки только если это не обновление чекбокса
|
||
if (!skipTimestamp) {
|
||
logAction(
|
||
req.session.userId,
|
||
"note_update",
|
||
`Обновлена заметка #${id}`
|
||
);
|
||
}
|
||
|
||
res.json({ id, content, date: row.date, time: row.time });
|
||
});
|
||
});
|
||
});
|
||
|
||
// API для получения версии данных (последний updated_at)
|
||
app.get("/api/notes/version", requireApiAuth, (req, res) => {
|
||
const sql = `
|
||
SELECT
|
||
MAX(updated_at) as last_updated_at,
|
||
MAX(created_at) as last_created_at,
|
||
COUNT(*) as total_notes
|
||
FROM notes
|
||
WHERE user_id = ? AND is_archived = 0
|
||
`;
|
||
|
||
db.get(sql, [req.session.userId], (err, row) => {
|
||
if (err) {
|
||
console.error("Ошибка получения версии данных:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
res.json({
|
||
last_updated_at: row?.last_updated_at || null,
|
||
last_created_at: row?.last_created_at || null,
|
||
total_notes: row?.total_notes || 0,
|
||
timestamp: Date.now(),
|
||
});
|
||
});
|
||
});
|
||
|
||
// API для удаления заметки
|
||
app.delete("/api/notes/:id", requireApiAuth, (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 getFilesSql = "SELECT file_path FROM note_files WHERE note_id = ?";
|
||
db.all(getFilesSql, [id], (err, files) => {
|
||
if (err) {
|
||
console.error("Ошибка получения файлов:", err.message);
|
||
} else {
|
||
// Удаляем файлы
|
||
files.forEach((file) => {
|
||
const filePath = path.join(__dirname, "public", file.file_path);
|
||
if (fs.existsSync(filePath)) {
|
||
fs.unlinkSync(filePath);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Удаляем записи об изображениях из БД
|
||
const deleteImagesSql = "DELETE FROM note_images WHERE note_id = ?";
|
||
db.run(deleteImagesSql, [id], (err) => {
|
||
if (err) {
|
||
console.error("Ошибка удаления изображений:", err.message);
|
||
}
|
||
});
|
||
|
||
// Удаляем записи о файлах из БД
|
||
const deleteFilesSql = "DELETE FROM note_files WHERE note_id = ?";
|
||
db.run(deleteFilesSql, [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: "Заметка не найдена" });
|
||
}
|
||
|
||
// Логируем удаление заметки
|
||
logAction(
|
||
req.session.userId,
|
||
"note_delete",
|
||
`Удалена заметка #${id}`
|
||
);
|
||
|
||
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;
|
||
// Ensure proper UTF-8 encoding for original filename
|
||
const originalName = Buffer.from(file.originalname, "latin1").toString(
|
||
"utf8"
|
||
);
|
||
const params = [
|
||
id,
|
||
file.filename,
|
||
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", requireApiAuth, (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", requireApiAuth, (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: "Изображение удалено" });
|
||
});
|
||
});
|
||
});
|
||
});
|
||
|
||
// API для загрузки файлов к заметке
|
||
app.post(
|
||
"/api/notes/:id/files",
|
||
requireAuth,
|
||
uploadNoteFiles.array("files", 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_files (note_id, filename, original_name, file_path, file_size, mime_type)
|
||
VALUES (?, ?, ?, ?, ?, ?)
|
||
`;
|
||
|
||
const uploadedFiles = [];
|
||
let completed = 0;
|
||
|
||
files.forEach((file) => {
|
||
const filePath = "/uploads/" + file.filename;
|
||
// Ensure proper UTF-8 encoding for original filename
|
||
const originalName = Buffer.from(file.originalname, "latin1").toString(
|
||
"utf8"
|
||
);
|
||
const params = [
|
||
id,
|
||
file.filename,
|
||
originalName,
|
||
filePath,
|
||
file.size,
|
||
file.mimetype,
|
||
];
|
||
|
||
db.run(insertSql, params, function (err) {
|
||
if (err) {
|
||
console.error("Ошибка сохранения файла:", err.message);
|
||
// Удаляем файл, если не удалось сохранить в БД
|
||
const fileFullPath = path.join(__dirname, "public", filePath);
|
||
if (fs.existsSync(fileFullPath)) {
|
||
fs.unlinkSync(fileFullPath);
|
||
}
|
||
} else {
|
||
uploadedFiles.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 (uploadedFiles.length === 0) {
|
||
return res
|
||
.status(500)
|
||
.json({ error: "Не удалось загрузить файлы" });
|
||
}
|
||
res.json({
|
||
success: true,
|
||
message: `Загружено ${uploadedFiles.length} файлов`,
|
||
files: uploadedFiles,
|
||
});
|
||
}
|
||
});
|
||
});
|
||
});
|
||
}
|
||
);
|
||
|
||
// API для получения файлов заметки
|
||
app.get("/api/notes/:id/files", requireApiAuth, (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 getFilesSql = `
|
||
SELECT id, filename, original_name, file_path, file_size, mime_type, created_at
|
||
FROM note_files
|
||
WHERE note_id = ?
|
||
ORDER BY created_at ASC
|
||
`;
|
||
|
||
db.all(getFilesSql, [id], (err, files) => {
|
||
if (err) {
|
||
console.error("Ошибка получения файлов:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
res.json(files);
|
||
});
|
||
});
|
||
});
|
||
|
||
// API для удаления файла заметки
|
||
app.delete("/api/notes/:noteId/files/:fileId", requireApiAuth, (req, res) => {
|
||
const { noteId, fileId } = 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 getFileSql =
|
||
"SELECT file_path FROM note_files WHERE id = ? AND note_id = ?";
|
||
db.get(getFileSql, [fileId, noteId], (err, file) => {
|
||
if (err) {
|
||
console.error("Ошибка получения файла:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
if (!file) {
|
||
return res.status(404).json({ error: "Файл не найден" });
|
||
}
|
||
|
||
// Удаляем файл
|
||
const filePath = path.join(__dirname, "public", file.file_path);
|
||
if (fs.existsSync(filePath)) {
|
||
fs.unlinkSync(filePath);
|
||
}
|
||
|
||
// Удаляем запись из БД
|
||
const deleteSql = "DELETE FROM note_files WHERE id = ? AND note_id = ?";
|
||
db.run(deleteSql, [fileId, 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", "index.html"));
|
||
}
|
||
|
||
const accentColor = user?.accent_color || "#007bff";
|
||
|
||
// Читаем HTML файл
|
||
fs.readFile(
|
||
path.join(__dirname, "public", "index.html"),
|
||
"utf8",
|
||
(err, html) => {
|
||
if (err) {
|
||
console.error("Ошибка чтения файла index.html:", err.message);
|
||
return res.sendFile(path.join(__dirname, "public", "index.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", requireApiAuth, async (req, res) => {
|
||
const {
|
||
username,
|
||
email,
|
||
currentPassword,
|
||
newPassword,
|
||
accent_color,
|
||
show_edit_date,
|
||
colored_icons,
|
||
floating_toolbar_enabled,
|
||
is_public_profile,
|
||
} = 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 (show_edit_date !== undefined) {
|
||
updateFields.push("show_edit_date = ?");
|
||
params.push(show_edit_date ? 1 : 0);
|
||
}
|
||
|
||
if (colored_icons !== undefined) {
|
||
updateFields.push("colored_icons = ?");
|
||
params.push(colored_icons ? 1 : 0);
|
||
}
|
||
|
||
if (floating_toolbar_enabled !== undefined) {
|
||
updateFields.push("floating_toolbar_enabled = ?");
|
||
params.push(floating_toolbar_enabled ? 1 : 0);
|
||
}
|
||
|
||
if (is_public_profile !== undefined) {
|
||
updateFields.push("is_public_profile = ?");
|
||
params.push(is_public_profile ? 1 : 0);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
// Логируем обновление профиля
|
||
const changes = [];
|
||
if (username && username !== user.username) changes.push("логин");
|
||
if (email !== undefined) changes.push("email");
|
||
if (accent_color !== undefined) changes.push("цвет темы");
|
||
if (show_edit_date !== undefined)
|
||
changes.push("настройка показа даты редактирования");
|
||
if (colored_icons !== undefined) changes.push("цветные иконки");
|
||
if (is_public_profile !== undefined) changes.push("публичный профиль");
|
||
if (newPassword) changes.push("пароль");
|
||
const details = `Обновлен профиль: ${changes.join(", ")}`;
|
||
logAction(userId, "profile_update", details);
|
||
|
||
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", requireApiAuth, (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: "Аватарка удалена" });
|
||
});
|
||
});
|
||
});
|
||
|
||
// API для закрепления заметки
|
||
app.put("/api/notes/:id/pin", requireApiAuth, (req, res) => {
|
||
const { id } = req.params;
|
||
|
||
// Проверяем, что заметка принадлежит текущему пользователю
|
||
const checkSql = "SELECT user_id, is_pinned 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 newPinState = row.is_pinned ? 0 : 1;
|
||
const updateSql = newPinState
|
||
? "UPDATE notes SET is_pinned = 1, pinned_at = CURRENT_TIMESTAMP WHERE id = ?"
|
||
: "UPDATE notes SET is_pinned = 0, pinned_at = NULL WHERE id = ?";
|
||
|
||
db.run(updateSql, [id], function (err) {
|
||
if (err) {
|
||
console.error("Ошибка изменения закрепления:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
// Логируем действие
|
||
const action = newPinState ? "закреплена" : "откреплена";
|
||
logAction(req.session.userId, "note_pin", `Заметка #${id} ${action}`);
|
||
|
||
res.json({ success: true, is_pinned: newPinState });
|
||
});
|
||
});
|
||
});
|
||
|
||
// API для архивирования заметки
|
||
app.put("/api/notes/:id/archive", requireApiAuth, (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 updateSql =
|
||
"UPDATE notes SET is_archived = 1, is_pinned = 0, pinned_at = NULL WHERE id = ?";
|
||
|
||
db.run(updateSql, [id], function (err) {
|
||
if (err) {
|
||
console.error("Ошибка архивирования:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
// Логируем действие
|
||
logAction(
|
||
req.session.userId,
|
||
"note_archive",
|
||
`Заметка #${id} архивирована`
|
||
);
|
||
|
||
res.json({ success: true, message: "Заметка архивирована" });
|
||
});
|
||
});
|
||
});
|
||
|
||
// API для восстановления заметки из архива
|
||
app.put("/api/notes/:id/unarchive", requireApiAuth, (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 updateSql = "UPDATE notes SET is_archived = 0 WHERE id = ?";
|
||
|
||
db.run(updateSql, [id], function (err) {
|
||
if (err) {
|
||
console.error("Ошибка восстановления:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
// Логируем действие
|
||
logAction(
|
||
req.session.userId,
|
||
"note_unarchive",
|
||
`Заметка #${id} восстановлена из архива`
|
||
);
|
||
|
||
res.json({ success: true, message: "Заметка восстановлена" });
|
||
});
|
||
});
|
||
});
|
||
|
||
// API для получения архивных заметок
|
||
app.get("/api/notes/archived", requireApiAuth, (req, res) => {
|
||
const sql = `
|
||
SELECT
|
||
n.*,
|
||
CASE
|
||
WHEN COUNT(DISTINCT ni.id) = 0 THEN '[]'
|
||
ELSE json_group_array(
|
||
DISTINCT 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
|
||
)
|
||
) FILTER (WHERE ni.id IS NOT NULL)
|
||
END as images,
|
||
CASE
|
||
WHEN COUNT(DISTINCT nf.id) = 0 THEN '[]'
|
||
ELSE json_group_array(
|
||
DISTINCT json_object(
|
||
'id', nf.id,
|
||
'filename', nf.filename,
|
||
'original_name', nf.original_name,
|
||
'file_path', nf.file_path,
|
||
'file_size', nf.file_size,
|
||
'mime_type', nf.mime_type,
|
||
'created_at', nf.created_at
|
||
)
|
||
) FILTER (WHERE nf.id IS NOT NULL)
|
||
END as files
|
||
FROM notes n
|
||
LEFT JOIN note_images ni ON n.id = ni.note_id
|
||
LEFT JOIN note_files nf ON n.id = nf.note_id
|
||
WHERE n.user_id = ? AND n.is_archived = 1
|
||
GROUP BY n.id
|
||
ORDER BY n.is_pinned DESC, n.pinned_at DESC, 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 notesWithImagesAndFiles = rows.map((row) => ({
|
||
...row,
|
||
content: decrypt(row.content), // Дешифруем содержимое заметки
|
||
images: row.images === "[]" ? [] : JSON.parse(row.images),
|
||
files: row.files === "[]" ? [] : JSON.parse(row.files),
|
||
}));
|
||
|
||
res.json(notesWithImagesAndFiles);
|
||
});
|
||
});
|
||
|
||
// API для окончательного удаления всех архивных заметок пользователя
|
||
app.delete("/api/notes/archived/all", requireApiAuth, async (req, res) => {
|
||
const { password } = req.body;
|
||
|
||
if (!password) {
|
||
return res.status(400).json({ error: "Пароль обязателен" });
|
||
}
|
||
|
||
try {
|
||
// Получаем хеш пароля пользователя
|
||
const userSql = "SELECT password FROM users WHERE id = ?";
|
||
db.get(userSql, [req.session.userId], async (err, user) => {
|
||
if (err) {
|
||
console.error("Ошибка получения пользователя:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
if (!user) {
|
||
return res.status(404).json({ error: "Пользователь не найден" });
|
||
}
|
||
|
||
// Проверяем пароль
|
||
const validPassword = await bcrypt.compare(password, user.password);
|
||
if (!validPassword) {
|
||
return res.status(401).json({ error: "Неверный пароль" });
|
||
}
|
||
|
||
// Получаем все архивные заметки пользователя
|
||
const getNotesSql =
|
||
"SELECT id FROM notes WHERE user_id = ? AND is_archived = 1";
|
||
db.all(getNotesSql, [req.session.userId], (err, notes) => {
|
||
if (err) {
|
||
console.error("Ошибка получения архивных заметок:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
if (notes.length === 0) {
|
||
return res.json({
|
||
success: true,
|
||
message: "Архив уже пуст",
|
||
deletedCount: 0,
|
||
});
|
||
}
|
||
|
||
// Удаляем изображения для всех заметок
|
||
const noteIds = notes.map((note) => note.id);
|
||
const placeholders = noteIds.map(() => "?").join(",");
|
||
|
||
const getImagesSql = `SELECT file_path FROM note_images WHERE note_id IN (${placeholders})`;
|
||
db.all(getImagesSql, noteIds, (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)) {
|
||
try {
|
||
fs.unlinkSync(imagePath);
|
||
} catch (err) {
|
||
console.error("Ошибка удаления файла изображения:", err);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Получаем и удаляем файлы заметок
|
||
const getFilesSql = `SELECT file_path FROM note_files WHERE note_id IN (${placeholders})`;
|
||
db.all(getFilesSql, noteIds, (err, files) => {
|
||
if (err) {
|
||
console.error("Ошибка получения файлов:", err.message);
|
||
} else {
|
||
// Удаляем файлы
|
||
files.forEach((file) => {
|
||
const filePath = path.join(__dirname, "public", file.file_path);
|
||
if (fs.existsSync(filePath)) {
|
||
try {
|
||
fs.unlinkSync(filePath);
|
||
} catch (err) {
|
||
console.error("Ошибка удаления файла:", err);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Удаляем записи об изображениях
|
||
const deleteImagesSql = `DELETE FROM note_images WHERE note_id IN (${placeholders})`;
|
||
db.run(deleteImagesSql, noteIds, (err) => {
|
||
if (err) {
|
||
console.error("Ошибка удаления изображений:", err.message);
|
||
}
|
||
|
||
// Удаляем записи о файлах
|
||
const deleteFilesSql = `DELETE FROM note_files WHERE note_id IN (${placeholders})`;
|
||
db.run(deleteFilesSql, noteIds, (err) => {
|
||
if (err) {
|
||
console.error("Ошибка удаления файлов:", err.message);
|
||
}
|
||
|
||
// Удаляем сами заметки
|
||
const deleteNotesSql =
|
||
"DELETE FROM notes WHERE user_id = ? AND is_archived = 1";
|
||
db.run(deleteNotesSql, [req.session.userId], function (err) {
|
||
if (err) {
|
||
console.error("Ошибка удаления заметок:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
const deletedCount = this.changes;
|
||
|
||
// Логируем действие
|
||
logAction(
|
||
req.session.userId,
|
||
"note_delete_permanent",
|
||
`Удалены все архивные заметки (${deletedCount} шт.)`
|
||
);
|
||
|
||
res.json({
|
||
success: true,
|
||
message: `Удалено ${deletedCount} архивных заметок`,
|
||
deletedCount,
|
||
});
|
||
});
|
||
});
|
||
});
|
||
});
|
||
});
|
||
});
|
||
});
|
||
} catch (error) {
|
||
console.error("Ошибка при проверке пароля:", error);
|
||
res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
});
|
||
|
||
// API для окончательного удаления архивной заметки
|
||
app.delete("/api/notes/archived/:id", requireApiAuth, (req, res) => {
|
||
const { id } = req.params;
|
||
|
||
// Проверяем, что заметка принадлежит текущему пользователю и архивирована
|
||
const checkSql = "SELECT user_id, is_archived 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: "Нет доступа к этой заметке" });
|
||
}
|
||
|
||
if (!row.is_archived) {
|
||
return res.status(400).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 getFilesSql = "SELECT file_path FROM note_files WHERE note_id = ?";
|
||
db.all(getFilesSql, [id], (err, files) => {
|
||
if (err) {
|
||
console.error("Ошибка получения файлов:", err.message);
|
||
} else {
|
||
files.forEach((file) => {
|
||
const filePath = path.join(__dirname, "public", file.file_path);
|
||
if (fs.existsSync(filePath)) {
|
||
fs.unlinkSync(filePath);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Удаляем записи об изображениях
|
||
const deleteImagesSql = "DELETE FROM note_images WHERE note_id = ?";
|
||
db.run(deleteImagesSql, [id], (err) => {
|
||
if (err) {
|
||
console.error("Ошибка удаления изображений:", err.message);
|
||
}
|
||
});
|
||
|
||
// Удаляем записи о файлах
|
||
const deleteFilesSql = "DELETE FROM note_files WHERE note_id = ?";
|
||
db.run(deleteFilesSql, [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: "Ошибка сервера" });
|
||
}
|
||
|
||
// Логируем действие
|
||
logAction(
|
||
req.session.userId,
|
||
"note_delete_permanent",
|
||
`Заметка #${id} окончательно удалена из архива`
|
||
);
|
||
|
||
res.json({ success: true, message: "Заметка удалена окончательно" });
|
||
});
|
||
});
|
||
});
|
||
});
|
||
});
|
||
|
||
// API для получения логов пользователя
|
||
app.get("/api/logs", requireApiAuth, (req, res) => {
|
||
const { action_type, limit = 100, offset = 0 } = req.query;
|
||
|
||
let sql = `
|
||
SELECT id, action_type, details, created_at
|
||
FROM action_logs
|
||
WHERE user_id = ?
|
||
`;
|
||
const params = [req.session.userId];
|
||
|
||
// Фильтр по типу действия
|
||
if (action_type) {
|
||
sql += " AND action_type = ?";
|
||
params.push(action_type);
|
||
}
|
||
|
||
sql += " ORDER BY created_at DESC LIMIT ? OFFSET ?";
|
||
params.push(parseInt(limit), parseInt(offset));
|
||
|
||
db.all(sql, params, (err, rows) => {
|
||
if (err) {
|
||
console.error("Ошибка получения логов:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
res.json(rows);
|
||
});
|
||
});
|
||
|
||
// Страница настроек
|
||
app.get("/settings", 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", "index.html"));
|
||
}
|
||
|
||
const accentColor = user?.accent_color || "#007bff";
|
||
|
||
// Читаем HTML файл
|
||
fs.readFile(
|
||
path.join(__dirname, "public", "index.html"),
|
||
"utf8",
|
||
(err, html) => {
|
||
if (err) {
|
||
console.error("Ошибка чтения файла index.html:", err.message);
|
||
return res.sendFile(path.join(__dirname, "public", "index.html"));
|
||
}
|
||
|
||
// Вставляем inline CSS с правильным цветом
|
||
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 для получения AI настроек
|
||
app.get("/api/user/ai-settings", requireApiAuth, (req, res) => {
|
||
if (!req.session.userId) {
|
||
return res.status(401).json({ error: "Не аутентифицирован" });
|
||
}
|
||
|
||
const sql =
|
||
"SELECT openai_api_key, openai_base_url, openai_model, ai_enabled FROM users WHERE id = ?";
|
||
db.get(sql, [req.session.userId], (err, settings) => {
|
||
if (err) {
|
||
console.error("Ошибка получения AI настроек:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
if (!settings) {
|
||
return res.status(404).json({ error: "Настройки не найдены" });
|
||
}
|
||
|
||
res.json({
|
||
openai_api_key: settings.openai_api_key || "",
|
||
openai_base_url: settings.openai_base_url || "",
|
||
openai_model: settings.openai_model || "",
|
||
ai_enabled: settings.ai_enabled || 0,
|
||
});
|
||
});
|
||
});
|
||
|
||
// API для сохранения AI настроек
|
||
app.put("/api/user/ai-settings", requireApiAuth, (req, res) => {
|
||
const { openai_api_key, openai_base_url, openai_model, ai_enabled } =
|
||
req.body;
|
||
const userId = req.session.userId;
|
||
|
||
// ai_enabled может быть передан отдельно от остальных настроек
|
||
if (
|
||
ai_enabled !== undefined &&
|
||
!openai_api_key &&
|
||
!openai_base_url &&
|
||
!openai_model
|
||
) {
|
||
const updateSql = "UPDATE users SET ai_enabled = ? WHERE id = ?";
|
||
db.run(updateSql, [ai_enabled ? 1 : 0, userId], function (err) {
|
||
if (err) {
|
||
console.error("Ошибка сохранения статуса AI:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
// Логируем обновление AI настроек
|
||
logAction(userId, "profile_update", "Изменен статус AI");
|
||
|
||
res.json({ success: true, message: "Настройки AI успешно сохранены" });
|
||
});
|
||
} else {
|
||
// Если сохраняем полные настройки
|
||
if (!openai_api_key || !openai_base_url || !openai_model) {
|
||
return res.status(400).json({ error: "Все поля обязательны" });
|
||
}
|
||
|
||
// При сохранении полных настроек также проверяем ai_enabled
|
||
// Если все поля заполнены и ai_enabled не передан, оставляем текущее значение
|
||
// Если передан ai_enabled, используем его
|
||
const finalAiEnabled =
|
||
ai_enabled !== undefined ? (ai_enabled ? 1 : 0) : undefined;
|
||
|
||
let updateSql, updateParams;
|
||
|
||
if (finalAiEnabled !== undefined) {
|
||
updateSql =
|
||
"UPDATE users SET openai_api_key = ?, openai_base_url = ?, openai_model = ?, ai_enabled = ? WHERE id = ?";
|
||
updateParams = [
|
||
openai_api_key,
|
||
openai_base_url,
|
||
openai_model,
|
||
finalAiEnabled,
|
||
userId,
|
||
];
|
||
} else {
|
||
updateSql =
|
||
"UPDATE users SET openai_api_key = ?, openai_base_url = ?, openai_model = ? WHERE id = ?";
|
||
updateParams = [openai_api_key, openai_base_url, openai_model, userId];
|
||
}
|
||
|
||
db.run(updateSql, updateParams, function (err) {
|
||
if (err) {
|
||
console.error("Ошибка сохранения AI настроек:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
// Логируем обновление AI настроек
|
||
logAction(userId, "profile_update", "Обновлены AI настройки");
|
||
|
||
res.json({ success: true, message: "AI настройки успешно сохранены" });
|
||
});
|
||
}
|
||
});
|
||
|
||
// API для получения настроек 2FA (генерация секрета и QR-кода)
|
||
app.get("/api/user/2fa/setup", requireApiAuth, async (req, res) => {
|
||
const userId = req.session.userId;
|
||
|
||
try {
|
||
// Получаем информацию о пользователе
|
||
const getUserSql = "SELECT username 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: "Пользователь не найден" });
|
||
}
|
||
|
||
// Генерируем секретный ключ
|
||
const secret = speakeasy.generateSecret({
|
||
name: `NoteJS (${user.username})`,
|
||
issuer: "NoteJS",
|
||
length: 32,
|
||
});
|
||
|
||
// Генерируем QR-код
|
||
try {
|
||
const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);
|
||
|
||
res.json({
|
||
success: true,
|
||
secret: secret.base32, // Возвращаем base32 секрет для ручного ввода
|
||
qrCode: qrCodeUrl, // Возвращаем QR-код как data URL
|
||
otpauthUrl: secret.otpauth_url,
|
||
});
|
||
} catch (qrError) {
|
||
console.error("Ошибка генерации QR-кода:", qrError);
|
||
return res.status(500).json({ error: "Ошибка генерации QR-кода" });
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error("Ошибка настройки 2FA:", error);
|
||
res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
});
|
||
|
||
// API для включения 2FA
|
||
app.post("/api/user/2fa/enable", requireApiAuth, async (req, res) => {
|
||
const { secret, token } = req.body;
|
||
const userId = req.session.userId;
|
||
|
||
if (!secret || !token) {
|
||
return res.status(400).json({ error: "Секрет и код обязательны" });
|
||
}
|
||
|
||
// Проверяем токен
|
||
const verified = speakeasy.totp.verify({
|
||
secret: secret,
|
||
encoding: "base32",
|
||
token: token,
|
||
window: 2, // Разрешаем небольшое отклонение по времени
|
||
});
|
||
|
||
if (!verified) {
|
||
return res.status(400).json({ error: "Неверный код подтверждения" });
|
||
}
|
||
|
||
try {
|
||
// Генерируем резервные коды (10 штук)
|
||
const backupCodes = [];
|
||
for (let i = 0; i < 10; i++) {
|
||
backupCodes.push(
|
||
Math.random().toString(36).substring(2, 10).toUpperCase()
|
||
);
|
||
}
|
||
|
||
// Хешируем резервные коды
|
||
const hashedBackupCodes = await Promise.all(
|
||
backupCodes.map((code) => bcrypt.hash(code, 10))
|
||
);
|
||
|
||
// Шифруем секрет перед сохранением
|
||
const encryptedSecret = encrypt(secret);
|
||
|
||
// Сохраняем в БД
|
||
const updateSql =
|
||
"UPDATE users SET two_factor_enabled = 1, two_factor_secret = ?, two_factor_backup_codes = ? WHERE id = ?";
|
||
db.run(
|
||
updateSql,
|
||
[encryptedSecret, JSON.stringify(hashedBackupCodes), userId],
|
||
function (err) {
|
||
if (err) {
|
||
console.error("Ошибка включения 2FA:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
// Логируем включение 2FA
|
||
logAction(userId, "profile_update", "Включена двухфакторная аутентификация");
|
||
|
||
res.json({
|
||
success: true,
|
||
message: "2FA успешно включена",
|
||
backupCodes: backupCodes, // Возвращаем незахешированные коды один раз
|
||
});
|
||
}
|
||
);
|
||
} catch (error) {
|
||
console.error("Ошибка включения 2FA:", error);
|
||
res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
});
|
||
|
||
// API для отключения 2FA
|
||
app.post("/api/user/2fa/disable", requireApiAuth, async (req, res) => {
|
||
const { password } = req.body;
|
||
const userId = req.session.userId;
|
||
|
||
if (!password) {
|
||
return res.status(400).json({ error: "Пароль обязателен" });
|
||
}
|
||
|
||
try {
|
||
// Проверяем пароль
|
||
const getUserSql = "SELECT password 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: "Пользователь не найден" });
|
||
}
|
||
|
||
const validPassword = await bcrypt.compare(password, user.password);
|
||
if (!validPassword) {
|
||
return res.status(401).json({ error: "Неверный пароль" });
|
||
}
|
||
|
||
// Отключаем 2FA
|
||
const updateSql =
|
||
"UPDATE users SET two_factor_enabled = 0, two_factor_secret = NULL, two_factor_backup_codes = NULL WHERE id = ?";
|
||
db.run(updateSql, [userId], function (err) {
|
||
if (err) {
|
||
console.error("Ошибка отключения 2FA:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
// Логируем отключение 2FA
|
||
logAction(userId, "profile_update", "Отключена двухфакторная аутентификация");
|
||
|
||
res.json({ success: true, message: "2FA успешно отключена" });
|
||
});
|
||
});
|
||
} catch (error) {
|
||
console.error("Ошибка отключения 2FA:", error);
|
||
res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
});
|
||
|
||
// API для проверки 2FA кода при входе
|
||
app.post("/api/user/2fa/verify", async (req, res) => {
|
||
const { username, token } = req.body;
|
||
|
||
if (!username || !token) {
|
||
return res.status(400).json({ error: "Логин и код обязательны" });
|
||
}
|
||
|
||
// Проверяем, есть ли промежуточная сессия для этого пользователя
|
||
if (!req.session.twoFactorPending || req.session.twoFactorUsername !== username) {
|
||
return res.status(400).json({ error: "Сессия не найдена. Начните вход заново" });
|
||
}
|
||
|
||
try {
|
||
// Получаем пользователя
|
||
const getUserSql =
|
||
"SELECT id, username, two_factor_secret, two_factor_backup_codes FROM users WHERE username = ? AND two_factor_enabled = 1";
|
||
db.get(getUserSql, [username], async (err, user) => {
|
||
if (err) {
|
||
console.error("Ошибка проверки 2FA:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
if (!user || !user.two_factor_secret) {
|
||
return res.status(400).json({ error: "2FA не настроена для этого пользователя" });
|
||
}
|
||
|
||
// Расшифровываем секрет
|
||
const secret = decrypt(user.two_factor_secret);
|
||
|
||
// Проверяем TOTP код
|
||
const verified = speakeasy.totp.verify({
|
||
secret: secret,
|
||
encoding: "base32",
|
||
token: token,
|
||
window: 2,
|
||
});
|
||
|
||
let backupCodeUsed = false;
|
||
|
||
// Если TOTP не прошел, проверяем резервные коды
|
||
if (!verified && user.two_factor_backup_codes) {
|
||
try {
|
||
const backupCodes = JSON.parse(user.two_factor_backup_codes);
|
||
let codeIndex = -1;
|
||
|
||
// Проверяем каждый резервный код
|
||
for (let i = 0; i < backupCodes.length; i++) {
|
||
const match = await bcrypt.compare(token, backupCodes[i]);
|
||
if (match) {
|
||
codeIndex = i;
|
||
backupCodeUsed = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Если резервный код использован, удаляем его
|
||
if (codeIndex !== -1) {
|
||
backupCodes.splice(codeIndex, 1);
|
||
const updateSql =
|
||
"UPDATE users SET two_factor_backup_codes = ? WHERE id = ?";
|
||
db.run(updateSql, [JSON.stringify(backupCodes), user.id], (err) => {
|
||
if (err) {
|
||
console.error("Ошибка обновления резервных кодов:", err.message);
|
||
}
|
||
});
|
||
}
|
||
} catch (parseError) {
|
||
console.error("Ошибка парсинга резервных кодов:", parseError);
|
||
}
|
||
}
|
||
|
||
if (!verified && !backupCodeUsed) {
|
||
return res.status(401).json({ error: "Неверный код" });
|
||
}
|
||
|
||
// Создаем полную сессию
|
||
req.session.userId = user.id;
|
||
req.session.username = user.username;
|
||
req.session.authenticated = true;
|
||
delete req.session.twoFactorPending;
|
||
delete req.session.twoFactorUsername;
|
||
|
||
// Логируем вход
|
||
logAction(user.id, "login", `Вход в систему${backupCodeUsed ? " (резервный код)" : ""}`);
|
||
|
||
res.json({
|
||
success: true,
|
||
message: "Вход успешен",
|
||
backupCodeUsed: backupCodeUsed,
|
||
});
|
||
});
|
||
} catch (error) {
|
||
console.error("Ошибка проверки 2FA:", error);
|
||
res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
});
|
||
|
||
// API для генерации новых резервных кодов
|
||
app.post("/api/user/2fa/backup-codes", requireApiAuth, async (req, res) => {
|
||
const userId = req.session.userId;
|
||
|
||
try {
|
||
// Проверяем, что 2FA включена
|
||
const getUserSql =
|
||
"SELECT two_factor_enabled 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 || !user.two_factor_enabled) {
|
||
return res.status(400).json({ error: "2FA не включена" });
|
||
}
|
||
|
||
// Генерируем новые резервные коды
|
||
const backupCodes = [];
|
||
for (let i = 0; i < 10; i++) {
|
||
backupCodes.push(
|
||
Math.random().toString(36).substring(2, 10).toUpperCase()
|
||
);
|
||
}
|
||
|
||
// Хешируем резервные коды
|
||
const hashedBackupCodes = await Promise.all(
|
||
backupCodes.map((code) => bcrypt.hash(code, 10))
|
||
);
|
||
|
||
// Сохраняем в БД
|
||
const updateSql =
|
||
"UPDATE users SET two_factor_backup_codes = ? WHERE id = ?";
|
||
db.run(updateSql, [JSON.stringify(hashedBackupCodes), userId], function (err) {
|
||
if (err) {
|
||
console.error("Ошибка генерации резервных кодов:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
// Логируем генерацию резервных кодов
|
||
logAction(userId, "profile_update", "Сгенерированы новые резервные коды 2FA");
|
||
|
||
res.json({
|
||
success: true,
|
||
backupCodes: backupCodes,
|
||
});
|
||
});
|
||
});
|
||
} catch (error) {
|
||
console.error("Ошибка генерации резервных кодов:", error);
|
||
res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
});
|
||
|
||
// API для улучшения текста через AI
|
||
app.post("/api/ai/improve", requireApiAuth, async (req, res) => {
|
||
const { text } = req.body;
|
||
|
||
if (!text) {
|
||
return res.status(400).json({ error: "Текст обязателен" });
|
||
}
|
||
|
||
try {
|
||
// Получаем AI настройки пользователя
|
||
const getSettingsSql =
|
||
"SELECT openai_api_key, openai_base_url, openai_model, ai_enabled FROM users WHERE id = ?";
|
||
db.get(getSettingsSql, [req.session.userId], async (err, settings) => {
|
||
if (err) {
|
||
console.error("Ошибка получения AI настроек:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
if (
|
||
!settings ||
|
||
!settings.openai_api_key ||
|
||
!settings.openai_base_url ||
|
||
!settings.openai_model
|
||
) {
|
||
return res
|
||
.status(400)
|
||
.json({ error: "Настройте AI настройки в параметрах" });
|
||
}
|
||
|
||
// Проверяем, включены ли функции ИИ
|
||
if (!settings.ai_enabled || settings.ai_enabled === 0) {
|
||
return res
|
||
.status(403)
|
||
.json({ error: "Функции ИИ отключены в настройках" });
|
||
}
|
||
|
||
try {
|
||
// Парсим URL
|
||
const url = new URL(settings.openai_base_url);
|
||
const isHttps = url.protocol === "https:";
|
||
const hostname = url.hostname;
|
||
const port = url.port || (isHttps ? 443 : 80);
|
||
// Формируем путь, избегая дублирования
|
||
let path = url.pathname || "";
|
||
if (!path.endsWith("/chat/completions")) {
|
||
path = path.endsWith("/")
|
||
? path + "chat/completions"
|
||
: path + "/chat/completions";
|
||
}
|
||
|
||
// Подготавливаем данные для запроса
|
||
const requestData = JSON.stringify({
|
||
model: settings.openai_model,
|
||
messages: [
|
||
{
|
||
role: "system",
|
||
content:
|
||
"Ты универсальный помощник для работы с текстом. Твоя задача:\n1. Если пользователь просит что-то придумать или сгенерировать (например, поздравление, письмо, пост) - создай качественный текст на заданную тему\n2. Если пользователь предоставил готовый текст - улучши его: исправь ошибки, улучши стиль и грамотность, сделай текст более понятным и выразительным, сохрани основную мысль\n3. Если пользователь просит создать список, используй markdown формат:\n - Для маркированных списков используй: - или *\n - Для нумерованных: 1. 2. 3.\n - Для заголовков: # ## ###\n4. Используй markdown форматирование (жирный **текст**, курсив *текст*, списки, заголовки) когда это уместно\n5. Обращай внимание на контекст и намерение пользователя\nВерни только готовый текст без дополнительных пояснений.",
|
||
},
|
||
{
|
||
role: "user",
|
||
content: text,
|
||
},
|
||
],
|
||
temperature: 0.5,
|
||
max_tokens: 4000,
|
||
});
|
||
|
||
// Выполняем HTTP запрос
|
||
const improvedText = await new Promise((resolve, reject) => {
|
||
const options = {
|
||
hostname: hostname,
|
||
port: port,
|
||
path: path,
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Authorization: `Bearer ${settings.openai_api_key}`,
|
||
"Content-Length": Buffer.byteLength(requestData),
|
||
},
|
||
};
|
||
|
||
const client = isHttps ? https : http;
|
||
const req = client.request(options, (res) => {
|
||
let data = "";
|
||
|
||
res.on("data", (chunk) => {
|
||
data += chunk;
|
||
});
|
||
|
||
res.on("end", () => {
|
||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||
try {
|
||
const responseData = JSON.parse(data);
|
||
const improvedText =
|
||
responseData.choices[0]?.message?.content || text;
|
||
resolve(improvedText);
|
||
} catch (err) {
|
||
console.error("Ошибка парсинга ответа:", err);
|
||
reject(new Error("Ошибка обработки ответа от AI"));
|
||
}
|
||
} else {
|
||
console.error("Ошибка OpenAI API:", res.statusCode, data);
|
||
reject(
|
||
new Error(
|
||
`Ошибка OpenAI API: ${res.statusCode} - ${data.substring(
|
||
0,
|
||
100
|
||
)}`
|
||
)
|
||
);
|
||
}
|
||
});
|
||
});
|
||
|
||
req.on("error", (error) => {
|
||
console.error("Ошибка запроса к OpenAI:", error);
|
||
reject(new Error("Ошибка подключения к OpenAI API"));
|
||
});
|
||
|
||
req.write(requestData);
|
||
req.end();
|
||
});
|
||
|
||
// Логируем использование AI
|
||
logAction(req.session.userId, "ai_improve", "Улучшен текст через AI");
|
||
|
||
res.json({ success: true, improvedText });
|
||
} catch (error) {
|
||
console.error("Ошибка вызова OpenAI API:", error);
|
||
return res
|
||
.status(500)
|
||
.json({ error: error.message || "Ошибка подключения к OpenAI API" });
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error("Ошибка улучшения текста:", error);
|
||
res.status(500).json({ error: "Ошибка улучшения текста" });
|
||
}
|
||
});
|
||
|
||
// API для генерации тегов через AI
|
||
app.post("/api/ai/generate-tags", requireApiAuth, async (req, res) => {
|
||
const { text } = req.body;
|
||
|
||
if (!text) {
|
||
return res.status(400).json({ error: "Текст обязателен" });
|
||
}
|
||
|
||
// Проверяем минимальную длину текста
|
||
if (text.trim().length < 10) {
|
||
return res.status(400).json({
|
||
error: "Текст слишком короткий для генерации тегов. Минимум 10 символов.",
|
||
});
|
||
}
|
||
|
||
try {
|
||
// Получаем AI настройки пользователя
|
||
const getSettingsSql =
|
||
"SELECT openai_api_key, openai_base_url, openai_model, ai_enabled FROM users WHERE id = ?";
|
||
db.get(getSettingsSql, [req.session.userId], async (err, settings) => {
|
||
if (err) {
|
||
console.error("Ошибка получения AI настроек:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
if (
|
||
!settings ||
|
||
!settings.openai_api_key ||
|
||
!settings.openai_base_url ||
|
||
!settings.openai_model
|
||
) {
|
||
return res
|
||
.status(400)
|
||
.json({ error: "Настройте AI настройки в параметрах" });
|
||
}
|
||
|
||
// Проверяем, включены ли функции ИИ
|
||
if (!settings.ai_enabled || settings.ai_enabled === 0) {
|
||
return res
|
||
.status(403)
|
||
.json({ error: "Функции ИИ отключены в настройках" });
|
||
}
|
||
|
||
try {
|
||
// Парсим URL
|
||
const url = new URL(settings.openai_base_url);
|
||
const isHttps = url.protocol === "https:";
|
||
const hostname = url.hostname;
|
||
const port = url.port || (isHttps ? 443 : 80);
|
||
// Формируем путь, избегая дублирования
|
||
let path = url.pathname || "";
|
||
if (!path.endsWith("/chat/completions")) {
|
||
path = path.endsWith("/")
|
||
? path + "chat/completions"
|
||
: path + "/chat/completions";
|
||
}
|
||
|
||
// Подготавливаем данные для запроса
|
||
const requestBody = {
|
||
model: settings.openai_model,
|
||
messages: [
|
||
{
|
||
role: "system",
|
||
content:
|
||
"Ты помощник для генерации тегов. Проанализируй текст и предложи 3-8 релевантных тегов на русском языке через запятую. Теги должны быть краткими (1-3 слова) и написаны БЕЗ ПРОБЕЛОВ - слова объединяются в одно (например: списокПокупок, работаНадПроектом, важныеЗадачи). Избегай общих слов типа 'текст', 'заметка', 'информация'. Верни ТОЛЬКО теги через запятую, без знаков #, без нумерации, без точек. Пример: работа, проект, задачи, дедлайн, списокПокупок",
|
||
},
|
||
{
|
||
role: "user",
|
||
content: text.substring(0, 2000), // Ограничиваем длину текста
|
||
},
|
||
],
|
||
temperature: 0.7,
|
||
max_tokens: 500, // Увеличено для моделей с reasoning tokens
|
||
};
|
||
|
||
const requestData = JSON.stringify(requestBody);
|
||
|
||
console.log("Отправляем запрос к AI API:");
|
||
console.log("URL:", settings.openai_base_url);
|
||
console.log("Модель:", settings.openai_model);
|
||
console.log("Длина текста:", text.length);
|
||
console.log(
|
||
"Запрос (без ключа):",
|
||
JSON.stringify(
|
||
{
|
||
...requestBody,
|
||
messages: requestBody.messages.map((m) => ({
|
||
...m,
|
||
content:
|
||
m.content.length > 100
|
||
? m.content.substring(0, 100) + "..."
|
||
: m.content,
|
||
})),
|
||
},
|
||
null,
|
||
2
|
||
)
|
||
);
|
||
|
||
// Выполняем HTTP запрос
|
||
const tagsResponse = await new Promise((resolve, reject) => {
|
||
const options = {
|
||
hostname: hostname,
|
||
port: port,
|
||
path: path,
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Authorization: `Bearer ${settings.openai_api_key}`,
|
||
"Content-Length": Buffer.byteLength(requestData),
|
||
},
|
||
};
|
||
|
||
const client = isHttps ? https : http;
|
||
const req = client.request(options, (res) => {
|
||
let data = "";
|
||
|
||
res.on("data", (chunk) => {
|
||
data += chunk;
|
||
});
|
||
|
||
res.on("end", () => {
|
||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||
try {
|
||
const responseData = JSON.parse(data);
|
||
console.log(
|
||
"Полный ответ от AI API:",
|
||
JSON.stringify(responseData, null, 2)
|
||
);
|
||
|
||
// Проверяем различные форматы ответа
|
||
let tagsText = "";
|
||
|
||
// Стандартный формат OpenAI
|
||
if (
|
||
responseData.choices &&
|
||
responseData.choices[0] &&
|
||
responseData.choices[0].message
|
||
) {
|
||
tagsText = responseData.choices[0].message.content || "";
|
||
// Если content пустой, пробуем извлечь из reasoning (для моделей с reasoning tokens)
|
||
if (!tagsText && responseData.choices[0].message.reasoning) {
|
||
const reasoning = responseData.choices[0].message.reasoning;
|
||
// Пытаемся найти теги в reasoning тексте
|
||
// Ищем паттерны типа "теги: ..." или просто список тегов через запятую
|
||
const tagsMatch = reasoning.match(/(?:теги?|tags?)[:\s]+([^.\n]+)/i);
|
||
if (tagsMatch && tagsMatch[1]) {
|
||
tagsText = tagsMatch[1].trim();
|
||
} else {
|
||
// Если не нашли паттерн, пробуем взять последнюю строку reasoning
|
||
const lines = reasoning.split('\n').filter(l => l.trim());
|
||
if (lines.length > 0) {
|
||
const lastLine = lines[lines.length - 1];
|
||
// Проверяем, похоже ли на список тегов (содержит запятые и короткие слова)
|
||
if (lastLine.includes(',') && lastLine.length < 200) {
|
||
tagsText = lastLine.trim();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// Альтернативный формат (некоторые API)
|
||
else if (
|
||
responseData.choices &&
|
||
responseData.choices[0] &&
|
||
responseData.choices[0].text
|
||
) {
|
||
tagsText = responseData.choices[0].text || "";
|
||
}
|
||
// Прямой content в корне (некоторые API)
|
||
else if (responseData.content) {
|
||
tagsText = responseData.content || "";
|
||
}
|
||
// Прямой text в корне (некоторые API)
|
||
else if (responseData.text) {
|
||
tagsText = responseData.text || "";
|
||
}
|
||
// Пробуем найти любой text или content в ответе
|
||
else {
|
||
const responseString = JSON.stringify(responseData);
|
||
console.log(
|
||
"Пробуем найти content/text в ответе:",
|
||
responseString
|
||
);
|
||
// Если ничего не нашли, пробуем извлечь из ответа
|
||
if (
|
||
responseData.choices &&
|
||
responseData.choices.length > 0
|
||
) {
|
||
const firstChoice = responseData.choices[0];
|
||
tagsText =
|
||
firstChoice.message?.content ||
|
||
firstChoice.text ||
|
||
firstChoice.content ||
|
||
firstChoice.message?.text ||
|
||
"";
|
||
// Fallback на reasoning
|
||
if (!tagsText && firstChoice.message?.reasoning) {
|
||
const reasoning = firstChoice.message.reasoning;
|
||
const tagsMatch = reasoning.match(/(?:теги?|tags?)[:\s]+([^.\n]+)/i);
|
||
if (tagsMatch && tagsMatch[1]) {
|
||
tagsText = tagsMatch[1].trim();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log("Извлеченный текст тегов:", tagsText);
|
||
console.log("Длина текста:", tagsText.length);
|
||
|
||
if (!tagsText || tagsText.trim().length === 0) {
|
||
console.error("Пустой content в ответе AI:", responseData);
|
||
console.error(
|
||
"Структура ответа:",
|
||
Object.keys(responseData)
|
||
);
|
||
if (responseData.choices) {
|
||
console.error(
|
||
"Choices структура:",
|
||
JSON.stringify(responseData.choices, null, 2)
|
||
);
|
||
}
|
||
reject(
|
||
new Error(
|
||
"ИИ вернул пустой ответ. Проверьте настройки модели и попробуйте еще раз."
|
||
)
|
||
);
|
||
return;
|
||
}
|
||
|
||
resolve(tagsText);
|
||
} catch (err) {
|
||
console.error("Ошибка парсинга ответа:", err);
|
||
console.error("Данные ответа:", data);
|
||
reject(
|
||
new Error("Ошибка обработки ответа от AI: " + err.message)
|
||
);
|
||
}
|
||
} else {
|
||
console.error("Ошибка OpenAI API:", res.statusCode, data);
|
||
let errorMessage = `Ошибка OpenAI API: ${res.statusCode}`;
|
||
try {
|
||
const errorData = JSON.parse(data);
|
||
if (errorData.error && errorData.error.message) {
|
||
errorMessage += " - " + errorData.error.message;
|
||
}
|
||
} catch (e) {
|
||
errorMessage += " - " + data.substring(0, 200);
|
||
}
|
||
reject(new Error(errorMessage));
|
||
}
|
||
});
|
||
});
|
||
|
||
req.on("error", (error) => {
|
||
console.error("Ошибка запроса к OpenAI:", error);
|
||
reject(new Error("Ошибка подключения к OpenAI API"));
|
||
});
|
||
|
||
req.write(requestData);
|
||
req.end();
|
||
});
|
||
|
||
// Логируем сырой ответ от AI для отладки
|
||
console.log("AI ответ для генерации тегов:", tagsResponse);
|
||
console.log("Тип ответа:", typeof tagsResponse);
|
||
console.log("Длина ответа:", tagsResponse ? tagsResponse.length : 0);
|
||
|
||
// Проверяем, что ответ не пустой
|
||
if (
|
||
!tagsResponse ||
|
||
typeof tagsResponse !== "string" ||
|
||
tagsResponse.trim().length === 0
|
||
) {
|
||
console.error("AI вернул пустой ответ");
|
||
return res.status(500).json({
|
||
error:
|
||
"ИИ вернул пустой ответ. Попробуйте еще раз или проверьте настройки AI.",
|
||
});
|
||
}
|
||
|
||
// Парсим теги из ответа
|
||
// Пробуем разные разделители: запятая, точка с запятой, перенос строки
|
||
let tags = [];
|
||
const cleanResponse = tagsResponse.trim();
|
||
|
||
// Сначала пробуем разделить по запятой
|
||
if (cleanResponse.includes(",")) {
|
||
tags = cleanResponse.split(",");
|
||
}
|
||
// Затем пробуем точку с запятой
|
||
else if (cleanResponse.includes(";")) {
|
||
tags = cleanResponse.split(";");
|
||
}
|
||
// Затем пробуем перенос строки
|
||
else if (cleanResponse.includes("\n")) {
|
||
tags = cleanResponse.split("\n");
|
||
}
|
||
// Если ничего не найдено, пробуем пробел (как последний вариант)
|
||
else {
|
||
tags = cleanResponse.split(/\s+/);
|
||
}
|
||
|
||
console.log("Теги до обработки:", tags);
|
||
|
||
// Функция для объединения слов в теге (убирает пробелы и делает camelCase)
|
||
const normalizeTag = (tag) => {
|
||
// Убираем # в начале и конце
|
||
tag = tag.replace(/^#+\s*/, "").replace(/\s*#+$/, "");
|
||
// Убираем точки, дефисы в начале/конце если они есть
|
||
tag = tag.replace(/^[.\-\s]+|[.\-\s]+$/g, "");
|
||
// Убираем кавычки если есть
|
||
tag = tag.replace(/^["']+|["']+$/g, "");
|
||
tag = tag.trim();
|
||
|
||
// Если есть пробелы, объединяем слова в camelCase
|
||
if (tag.includes(" ")) {
|
||
const words = tag.split(/\s+/).filter(w => w.length > 0);
|
||
if (words.length > 0) {
|
||
// Первое слово в нижнем регистре, остальные с заглавной первой буквой
|
||
const firstWord = words[0].toLowerCase();
|
||
const restWords = words.slice(1).map(word => {
|
||
if (word.length === 0) return "";
|
||
return word[0].toUpperCase() + word.slice(1).toLowerCase();
|
||
});
|
||
tag = firstWord + restWords.join("");
|
||
}
|
||
}
|
||
|
||
return tag;
|
||
};
|
||
|
||
// Обрабатываем теги
|
||
tags = tags
|
||
.map((tag) => tag.trim())
|
||
.filter((tag) => tag.length > 0)
|
||
.map((tag) => normalizeTag(tag))
|
||
.filter((tag) => {
|
||
// Фильтруем слишком короткие и слишком длинные теги
|
||
return tag.length > 0 && tag.length <= 50;
|
||
})
|
||
.filter((tag) => {
|
||
// Фильтруем общие слова, которые не являются тегами
|
||
const commonWords = [
|
||
"текст",
|
||
"заметка",
|
||
"информация",
|
||
"данные",
|
||
"файл",
|
||
"документ",
|
||
];
|
||
return !commonWords.includes(tag.toLowerCase());
|
||
})
|
||
.slice(0, 10); // Максимум 10 тегов
|
||
|
||
console.log("Распарсенные теги после обработки:", tags);
|
||
|
||
// Если тегов нет, это тоже ошибка
|
||
if (!tags || tags.length === 0) {
|
||
console.error(
|
||
"Не удалось распарсить теги из ответа AI. Исходный ответ:",
|
||
tagsResponse
|
||
);
|
||
return res.status(500).json({
|
||
error:
|
||
"ИИ вернул ответ, но не удалось извлечь теги. Попробуйте еще раз или добавьте больше текста в заметку.",
|
||
});
|
||
}
|
||
|
||
// Логируем использование AI
|
||
logAction(
|
||
req.session.userId,
|
||
"ai_generate_tags",
|
||
`Сгенерированы теги через AI: ${tags.length} тегов`
|
||
);
|
||
|
||
res.json({ success: true, tags });
|
||
} catch (error) {
|
||
console.error("Ошибка вызова OpenAI API:", error);
|
||
console.error("Стек ошибки:", error.stack);
|
||
return res.status(500).json({
|
||
error: error.message || "Ошибка подключения к OpenAI API",
|
||
details:
|
||
process.env.NODE_ENV === "development" ? error.stack : undefined,
|
||
});
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error("Ошибка генерации тегов:", error);
|
||
console.error("Стек ошибки:", error.stack);
|
||
res.status(500).json({
|
||
error: error.message || "Ошибка генерации тегов",
|
||
details: process.env.NODE_ENV === "development" ? error.stack : undefined,
|
||
});
|
||
}
|
||
});
|
||
|
||
// API для объединения заметок через AI
|
||
app.post("/api/ai/merge", requireApiAuth, async (req, res) => {
|
||
const { notes } = req.body;
|
||
|
||
if (!notes || !Array.isArray(notes) || notes.length < 2) {
|
||
return res
|
||
.status(400)
|
||
.json({ error: "Необходимо передать минимум 2 заметки" });
|
||
}
|
||
|
||
try {
|
||
// Получаем AI настройки пользователя
|
||
const getSettingsSql =
|
||
"SELECT openai_api_key, openai_base_url, openai_model, ai_enabled FROM users WHERE id = ?";
|
||
db.get(getSettingsSql, [req.session.userId], async (err, settings) => {
|
||
if (err) {
|
||
console.error("Ошибка получения AI настроек:", err.message);
|
||
return res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
|
||
if (
|
||
!settings ||
|
||
!settings.openai_api_key ||
|
||
!settings.openai_base_url ||
|
||
!settings.openai_model
|
||
) {
|
||
return res
|
||
.status(400)
|
||
.json({ error: "Настройте AI настройки в параметрах" });
|
||
}
|
||
|
||
// Проверяем, включены ли функции ИИ
|
||
if (!settings.ai_enabled || settings.ai_enabled === 0) {
|
||
return res
|
||
.status(403)
|
||
.json({ error: "Функции ИИ отключены в настройках" });
|
||
}
|
||
|
||
try {
|
||
// Парсим URL
|
||
const url = new URL(settings.openai_base_url);
|
||
const isHttps = url.protocol === "https:";
|
||
const hostname = url.hostname;
|
||
const port = url.port || (isHttps ? 443 : 80);
|
||
// Формируем путь, избегая дублирования
|
||
let path = url.pathname || "";
|
||
if (!path.endsWith("/chat/completions")) {
|
||
path = path.endsWith("/")
|
||
? path + "chat/completions"
|
||
: path + "/chat/completions";
|
||
}
|
||
|
||
// Формируем текст со всеми заметками
|
||
const notesText = notes
|
||
.map((note, index) => `=== Заметка ${index + 1} ===\n${note}`)
|
||
.join("\n\n");
|
||
|
||
// Подготавливаем данные для запроса
|
||
const requestData = JSON.stringify({
|
||
model: settings.openai_model,
|
||
messages: [
|
||
{
|
||
role: "system",
|
||
content:
|
||
"Ты помощник для объединения заметок. Твоя задача:\n1. Объедини следующие заметки в одну связную заметку\n2. Сохрани всю важную информацию из каждой заметки\n3. Структурируй текст логично, используй markdown форматирование (заголовки, списки, выделения)\n4. Удали дубликаты информации\n5. Если заметки на разные темы, раздели их по разделам с заголовками\n6. Сохрани даты и временные метки, если они важны\nВерни только готовый текст объединенной заметки без дополнительных пояснений.",
|
||
},
|
||
{
|
||
role: "user",
|
||
content: notesText,
|
||
},
|
||
],
|
||
temperature: 0.5,
|
||
max_tokens: 4000,
|
||
});
|
||
|
||
// Выполняем HTTP запрос
|
||
const mergedText = await new Promise((resolve, reject) => {
|
||
const options = {
|
||
hostname: hostname,
|
||
port: port,
|
||
path: path,
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Authorization: `Bearer ${settings.openai_api_key}`,
|
||
"Content-Length": Buffer.byteLength(requestData),
|
||
},
|
||
};
|
||
|
||
const client = isHttps ? https : http;
|
||
const req = client.request(options, (res) => {
|
||
let data = "";
|
||
|
||
res.on("data", (chunk) => {
|
||
data += chunk;
|
||
});
|
||
|
||
res.on("end", () => {
|
||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||
try {
|
||
const responseData = JSON.parse(data);
|
||
const mergedText =
|
||
responseData.choices[0]?.message?.content || "";
|
||
resolve(mergedText);
|
||
} catch (err) {
|
||
console.error("Ошибка парсинга ответа:", err);
|
||
reject(new Error("Ошибка обработки ответа от AI"));
|
||
}
|
||
} else {
|
||
console.error("Ошибка OpenAI API:", res.statusCode, data);
|
||
reject(
|
||
new Error(
|
||
`Ошибка OpenAI API: ${res.statusCode} - ${data.substring(
|
||
0,
|
||
100
|
||
)}`
|
||
)
|
||
);
|
||
}
|
||
});
|
||
});
|
||
|
||
req.on("error", (error) => {
|
||
console.error("Ошибка запроса к OpenAI:", error);
|
||
reject(new Error("Ошибка подключения к OpenAI API"));
|
||
});
|
||
|
||
req.write(requestData);
|
||
req.end();
|
||
});
|
||
|
||
// Логируем использование AI
|
||
logAction(
|
||
req.session.userId,
|
||
"ai_merge",
|
||
`Объединено ${notes.length} заметок через AI`
|
||
);
|
||
|
||
res.json({ success: true, mergedText });
|
||
} catch (error) {
|
||
console.error("Ошибка вызова OpenAI API:", error);
|
||
return res
|
||
.status(500)
|
||
.json({ error: error.message || "Ошибка подключения к OpenAI API" });
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error("Ошибка объединения заметок:", error);
|
||
res.status(500).json({ error: "Ошибка объединения заметок" });
|
||
}
|
||
});
|
||
|
||
// Выход
|
||
app.post("/logout", (req, res) => {
|
||
const userId = req.session.userId;
|
||
|
||
// Логируем выход
|
||
if (userId) {
|
||
logAction(userId, "logout", "Выход из системы");
|
||
}
|
||
|
||
req.session.destroy((err) => {
|
||
if (err) {
|
||
return res.status(500).json({ error: "Ошибка выхода" });
|
||
}
|
||
res.redirect("/");
|
||
});
|
||
});
|
||
|
||
// API для выхода (для фронтенда)
|
||
app.post("/api/logout", (req, res) => {
|
||
const userId = req.session.userId;
|
||
|
||
// Логируем выход
|
||
if (userId) {
|
||
logAction(userId, "logout", "Выход из системы");
|
||
}
|
||
|
||
req.session.destroy((err) => {
|
||
if (err) {
|
||
return res.status(500).json({ error: "Ошибка выхода" });
|
||
}
|
||
res.json({ success: true, message: "Выход выполнен успешно" });
|
||
});
|
||
});
|
||
|
||
// API для удаления аккаунта
|
||
app.delete("/api/user/delete-account", requireApiAuth, async (req, res) => {
|
||
const { password } = req.body;
|
||
const userId = req.session.userId;
|
||
|
||
if (!password) {
|
||
return res.status(400).json({ error: "Пароль обязателен" });
|
||
}
|
||
|
||
try {
|
||
// Получаем пользователя и проверяем пароль
|
||
const getUserSql = "SELECT id, password 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: "Пользователь не найден" });
|
||
}
|
||
|
||
// Проверяем пароль
|
||
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||
if (!isPasswordValid) {
|
||
return res.status(401).json({ error: "Неверный пароль" });
|
||
}
|
||
|
||
// Получаем аватарку и все файлы пользователя перед удалением
|
||
const getAvatarSql = "SELECT avatar FROM users WHERE id = ?";
|
||
db.get(getAvatarSql, [userId], (err, userData) => {
|
||
if (err) {
|
||
console.error("Ошибка получения аватарки:", err.message);
|
||
return res.status(500).json({ error: "Ошибка получения аватарки" });
|
||
}
|
||
|
||
// Получаем все изображения заметок пользователя
|
||
const getImagesSql = `
|
||
SELECT ni.file_path
|
||
FROM note_images ni
|
||
JOIN notes n ON ni.note_id = n.id
|
||
WHERE n.user_id = ?
|
||
`;
|
||
db.all(getImagesSql, [userId], (err, images) => {
|
||
if (err) {
|
||
console.error(
|
||
"Ошибка получения изображений пользователя:",
|
||
err.message
|
||
);
|
||
}
|
||
|
||
// Получаем все файлы заметок пользователя
|
||
const getFilesSql = `
|
||
SELECT nf.file_path
|
||
FROM note_files nf
|
||
JOIN notes n ON nf.note_id = n.id
|
||
WHERE n.user_id = ?
|
||
`;
|
||
db.all(getFilesSql, [userId], (err, files) => {
|
||
if (err) {
|
||
console.error(
|
||
"Ошибка получения файлов пользователя:",
|
||
err.message
|
||
);
|
||
}
|
||
|
||
// Удаляем файл аватарки
|
||
if (userData && userData.avatar) {
|
||
const avatarPath = path.join(
|
||
__dirname,
|
||
"public",
|
||
userData.avatar
|
||
);
|
||
if (fs.existsSync(avatarPath)) {
|
||
try {
|
||
fs.unlinkSync(avatarPath);
|
||
} catch (err) {
|
||
console.error("Ошибка удаления файла аватарки:", err.message);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Удаляем все изображения
|
||
if (images) {
|
||
images.forEach((image) => {
|
||
const imagePath = path.join(
|
||
__dirname,
|
||
"public",
|
||
image.file_path
|
||
);
|
||
if (fs.existsSync(imagePath)) {
|
||
try {
|
||
fs.unlinkSync(imagePath);
|
||
} catch (err) {
|
||
console.error("Ошибка удаления изображения:", err.message);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Удаляем все файлы
|
||
if (files) {
|
||
files.forEach((file) => {
|
||
const filePath = path.join(__dirname, "public", file.file_path);
|
||
if (fs.existsSync(filePath)) {
|
||
try {
|
||
fs.unlinkSync(filePath);
|
||
} catch (err) {
|
||
console.error("Ошибка удаления файла:", err.message);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Начинаем транзакцию для удаления всех данных из БД
|
||
db.serialize(() => {
|
||
db.run("BEGIN TRANSACTION");
|
||
|
||
// Удаляем все изображения заметок пользователя
|
||
const deleteImagesSql = `
|
||
DELETE FROM note_images
|
||
WHERE note_id IN (SELECT id FROM notes WHERE user_id = ?)
|
||
`;
|
||
db.run(deleteImagesSql, [userId], (err) => {
|
||
if (err) {
|
||
console.error(
|
||
"Ошибка удаления изображений из БД:",
|
||
err.message
|
||
);
|
||
db.run("ROLLBACK");
|
||
return res
|
||
.status(500)
|
||
.json({ error: "Ошибка удаления изображений" });
|
||
}
|
||
});
|
||
|
||
// Удаляем все файлы заметок пользователя
|
||
const deleteFilesSql = `
|
||
DELETE FROM note_files
|
||
WHERE note_id IN (SELECT id FROM notes WHERE user_id = ?)
|
||
`;
|
||
db.run(deleteFilesSql, [userId], (err) => {
|
||
if (err) {
|
||
console.error("Ошибка удаления файлов из БД:", err.message);
|
||
db.run("ROLLBACK");
|
||
return res
|
||
.status(500)
|
||
.json({ error: "Ошибка удаления файлов" });
|
||
}
|
||
});
|
||
|
||
// Удаляем все заметки пользователя
|
||
db.run("DELETE FROM notes WHERE user_id = ?", [userId], (err) => {
|
||
if (err) {
|
||
console.error("Ошибка удаления заметок:", err.message);
|
||
db.run("ROLLBACK");
|
||
return res
|
||
.status(500)
|
||
.json({ error: "Ошибка удаления заметок" });
|
||
}
|
||
});
|
||
|
||
// Удаляем логи пользователя
|
||
db.run(
|
||
"DELETE FROM action_logs WHERE user_id = ?",
|
||
[userId],
|
||
(err) => {
|
||
if (err) {
|
||
console.error("Ошибка удаления логов:", err.message);
|
||
db.run("ROLLBACK");
|
||
return res
|
||
.status(500)
|
||
.json({ error: "Ошибка удаления логов" });
|
||
}
|
||
}
|
||
);
|
||
|
||
// Удаляем пользователя из базы данных
|
||
db.run("DELETE FROM users WHERE id = ?", [userId], (err) => {
|
||
if (err) {
|
||
console.error("Ошибка удаления пользователя:", err.message);
|
||
db.run("ROLLBACK");
|
||
return res
|
||
.status(500)
|
||
.json({ error: "Ошибка удаления пользователя" });
|
||
}
|
||
|
||
// Подтверждаем транзакцию
|
||
db.run("COMMIT", (err) => {
|
||
if (err) {
|
||
console.error(
|
||
"Ошибка подтверждения транзакции:",
|
||
err.message
|
||
);
|
||
return res
|
||
.status(500)
|
||
.json({ error: "Ошибка подтверждения транзакции" });
|
||
}
|
||
|
||
// Уничтожаем сессию
|
||
req.session.destroy((err) => {
|
||
if (err) {
|
||
console.error("Ошибка уничтожения сессии:", err.message);
|
||
}
|
||
|
||
res.json({
|
||
message: "Аккаунт успешно удален",
|
||
success: true,
|
||
});
|
||
});
|
||
});
|
||
});
|
||
});
|
||
});
|
||
});
|
||
});
|
||
});
|
||
} catch (error) {
|
||
console.error("Ошибка удаления аккаунта:", error);
|
||
res.status(500).json({ error: "Ошибка сервера" });
|
||
}
|
||
});
|
||
|
||
// Запуск сервера
|
||
app.listen(PORT, () => {
|
||
console.log(`🚀 Сервер запущен на порту ${PORT}`);
|
||
console.log(`📝 Приложение для заметок готово к работе!`);
|
||
});
|