Переход к серверному варианту приложения

This commit is contained in:
Fovway 2025-10-17 00:38:53 +07:00
parent 56c0d6e648
commit 54b5608d1a
11 changed files with 3597 additions and 0 deletions

5
.cursor/rules/style.mdc Normal file
View File

@ -0,0 +1,5 @@
---
alwaysApply: true
---
Save the design of my application with notes and try to create as similar a design as possible in new modules.

34
.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
# Зависимости
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# База данных (можно исключить если не хотите хранить в репозитории)
notes.db
*.db
# Переменные окружения
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Логи
logs/
*.log
# Системные файлы
.DS_Store
Thumbs.db
# Редакторские файлы
.vscode/
.idea/
*.swp
*.swo
# Временные файлы сборки
dist/
build/

138
README.md Normal file
View File

@ -0,0 +1,138 @@
# NoteJS - Приложение для быстрых заметок
Простое веб-приложение для создания и управления заметками с поддержкой Markdown форматирования.
## Особенности
- 🚀 Создано на Node.js + Express
- 🔐 Аутентификация по паролю (без логина)
- 💾 Хранение данных в SQLite базе данных
- 📝 Поддержка Markdown форматирования
- 🎨 Простой и интуитивный интерфейс
- 📱 Адаптивный дизайн
## Установка и запуск
### Предварительные требования
- Node.js (версия 14 или выше)
- npm
### Установка
1. Клонируйте репозиторий:
```bash
git clone <repository-url>
cd NoteJS
```
2. Установите зависимости:
```bash
npm install
```
3. Настройте аутентификацию:
- Откройте файл `.env`
- Установите пароль для входа в переменной `APP_PASSWORD`
- Установите секрет сессии в переменной `SESSION_SECRET`
- Установите порт в переменной `PORT` (по умолчанию 3000)
Пример файла `.env`:
```env
APP_PASSWORD=your_secure_password_here
SESSION_SECRET=your_session_secret_here
PORT=3000
```
4. Запустите сервер:
```bash
npm start
```
5. Откройте браузер и перейдите по адресу `http://localhost:3000`
## Использование
### Вход в систему
1. При первом запуске введите пароль, указанный в файле `.env`
2. После успешного входа вы попадете в интерфейс заметок
### Создание заметок
1. Введите текст заметки в поле ввода
2. Используйте кнопки форматирования для добавления Markdown элементов:
- **B** - жирный текст
- _I_ - курсив
- H - заголовок
- 📋 - элемент списка
- " - цитата
- `</>` - код
- 🔗 - ссылка
3. Нажмите кнопку "Сохранить"
### Редактирование заметок
1. Нажмите кнопку "Редактировать" рядом с заметкой
2. Отредактируйте текст в появившемся поле ввода
3. Нажмите кнопку "Сохранить"
### Удаление заметок
1. Нажмите кнопку "Удалить" рядом с заметкой
2. Подтвердите удаление в появившемся диалоговом окне
## Структура проекта
```
NoteJS/
├── public/ # Статические файлы (клиентская часть)
│ ├── index.html # Страница входа
│ ├── notes.html # Страница заметок
│ ├── style.css # Стили
│ └── app.js # Клиентский JavaScript
├── server.js # Express сервер
├── .env # Конфигурация (не включать в git!)
├── package.json # Зависимости проекта
├── notes.db # SQLite база данных (создается автоматически)
└── README.md # Документация
```
## API Endpoints
### Аутентификация
- `POST /login` - вход в систему
- `POST /logout` - выход из системы
### Заметки (требуют аутентификации)
- `GET /api/notes` - получить все заметки
- `POST /api/notes` - создать новую заметку
- `PUT /api/notes/:id` - обновить заметку
- `DELETE /api/notes/:id` - удалить заметку
## Безопасность
- Helmet для защиты от распространенных уязвимостей
- Ограничение запросов (rate limiting)
- Сессионная аутентификация
- Защищенные заголовки
## Разработка
Для разработки используйте:
```bash
npm run dev
```
Этот скрипт использует **nodemon** для автоматической перезагрузки сервера при изменении файлов. Больше не нужно вручную перезапускать сервер при каждом изменении кода!
## Лицензия
Этот проект создан Fovway.

2436
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View File

@ -0,0 +1,32 @@
{
"name": "notejs",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"repository": {
"type": "git",
"url": "https://git.fovway.ru/Fovway/NoteJS.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"bcryptjs": "^3.0.2",
"body-parser": "^2.2.0",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"express-rate-limit": "^8.1.0",
"express-session": "^1.18.2",
"helmet": "^8.1.0",
"marked": "^16.4.0",
"sqlite3": "^5.1.7"
},
"devDependencies": {
"nodemon": "^3.1.10"
}
}

391
public/app.js Normal file
View File

@ -0,0 +1,391 @@
// DOM элементы
const noteInput = document.getElementById("noteInput");
const saveBtn = document.getElementById("saveBtn");
const notesList = document.getElementById("notesList");
// Получаем кнопки markdown
const boldBtn = document.getElementById("boldBtn");
const italicBtn = document.getElementById("italicBtn");
const headerBtn = document.getElementById("headerBtn");
const listBtn = document.getElementById("listBtn");
const quoteBtn = document.getElementById("quoteBtn");
const codeBtn = document.getElementById("codeBtn");
const linkBtn = document.getElementById("linkBtn");
// Функция для получения текущей даты и времени
function getFormattedDateTime() {
let now = new Date();
let day = String(now.getDate()).padStart(2, "0");
let month = String(now.getMonth() + 1).padStart(2, "0");
let year = now.getFullYear();
let hours = String(now.getHours()).padStart(2, "0");
let minutes = String(now.getMinutes()).padStart(2, "0");
return {
date: `${day}.${month}.${year}`,
time: `${hours}:${minutes}`,
};
}
// Функция для авторасширения текстового поля
function autoExpandTextarea(textarea) {
textarea.style.height = "auto";
textarea.style.height = textarea.scrollHeight + "px";
}
// Привязываем авторасширение к текстовому полю для создания заметки
noteInput.addEventListener("input", function () {
autoExpandTextarea(noteInput);
});
// Изначально запускаем для установки правильной высоты
autoExpandTextarea(noteInput);
// Функция для вставки markdown
function insertMarkdown(tag) {
const start = noteInput.selectionStart;
const end = noteInput.selectionEnd;
const text = noteInput.value;
const before = text.substring(0, start);
const selected = text.substring(start, end);
const after = text.substring(end);
if (selected.startsWith(tag) && selected.endsWith(tag)) {
// Если теги уже есть, удаляем их
noteInput.value = `${before}${selected.slice(
tag.length,
-tag.length
)}${after}`;
noteInput.setSelectionRange(start, end - 2 * tag.length);
} else if (selected.trim() === "") {
// Если текст не выделен
if (tag === "[Текст ссылки](URL)") {
// Для ссылок создаем шаблон с двумя кавычками
noteInput.value = `${before}[Текст ссылки](URL)${after}`;
const cursorPosition = start + 1; // Помещаем курсор внутрь текста ссылки
noteInput.setSelectionRange(cursorPosition, cursorPosition + 12);
} else if (tag === "- " || tag === "> " || tag === "# ") {
// Для списка, цитаты и заголовка помещаем курсор после `- `, `> ` или `# `
noteInput.value = `${before}${tag}${after}`;
const cursorPosition = start + tag.length;
noteInput.setSelectionRange(cursorPosition, cursorPosition);
} else {
// Для остальных типов создаем два тега
noteInput.value = `${before}${tag}${tag}${after}`;
const cursorPosition = start + tag.length;
noteInput.setSelectionRange(cursorPosition, cursorPosition);
}
} else {
// Если текст выделен
if (tag === "[Текст ссылки](URL)") {
// Для ссылок используем выделенный текст вместо "Текст ссылки"
noteInput.value = `${before}[${selected}](URL)${after}`;
const cursorPosition = start + selected.length + 3; // Помещаем курсор в URL
noteInput.setSelectionRange(cursorPosition, cursorPosition + 3);
} else if (tag === "- " || tag === "> " || tag === "# ") {
// Для списка, цитаты и заголовка добавляем `- `, `> ` или `# ` перед выделенным текстом
noteInput.value = `${before}${tag}${selected}${after}`;
const cursorPosition = start + tag.length + selected.length;
noteInput.setSelectionRange(cursorPosition, cursorPosition);
} else {
// Для остальных типов оборачиваем выделенный текст
noteInput.value = `${before}${tag}${selected}${tag}${after}`;
const cursorPosition = start + tag.length + selected.length + tag.length;
noteInput.setSelectionRange(cursorPosition, cursorPosition);
}
}
noteInput.focus();
}
// Функция для вставки markdown в режиме редактирования
function insertMarkdownForEdit(textarea, tag) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const text = textarea.value;
const before = text.substring(0, start);
const selected = text.substring(start, end);
const after = text.substring(end);
if (selected.startsWith(tag) && selected.endsWith(tag)) {
// Если теги уже есть, удаляем их
textarea.value = `${before}${selected.slice(
tag.length,
-tag.length
)}${after}`;
textarea.setSelectionRange(start, end - 2 * tag.length);
} else if (selected.trim() === "") {
// Если текст не выделен
if (tag === "[Текст ссылки](URL)") {
// Для ссылок создаем шаблон с двумя кавычками
textarea.value = `${before}[Текст ссылки](URL)${after}`;
const cursorPosition = start + 1; // Помещаем курсор внутрь текста ссылки
textarea.setSelectionRange(cursorPosition, cursorPosition + 12);
} else if (tag === "- " || tag === "> " || tag === "# ") {
// Для списка, цитаты и заголовка помещаем курсор после `- `, `> ` или `# `
textarea.value = `${before}${tag}${after}`;
const cursorPosition = start + tag.length;
textarea.setSelectionRange(cursorPosition, cursorPosition);
} else {
// Для остальных типов создаем два тега
textarea.value = `${before}${tag}${tag}${after}`;
const cursorPosition = start + tag.length;
textarea.setSelectionRange(cursorPosition, cursorPosition);
}
} else {
// Если текст выделен
if (tag === "[Текст ссылки](URL)") {
// Для ссылок используем выделенный текст вместо "Текст ссылки"
textarea.value = `${before}[${selected}](URL)${after}`;
const cursorPosition = start + selected.length + 3; // Помещаем курсор в URL
textarea.setSelectionRange(cursorPosition, cursorPosition + 3);
} else if (tag === "- " || tag === "> " || tag === "# ") {
// Для списка, цитаты и заголовка добавляем `- `, `> ` или `# ` перед выделенным текстом
textarea.value = `${before}${tag}${selected}${after}`;
const cursorPosition = start + tag.length + selected.length;
textarea.setSelectionRange(cursorPosition, cursorPosition);
} else {
// Для остальных типов оборачиваем выделенный текст
textarea.value = `${before}${tag}${selected}${tag}${after}`;
const cursorPosition = start + tag.length + selected.length + tag.length;
textarea.setSelectionRange(cursorPosition, cursorPosition);
}
}
textarea.focus();
}
// Обработчики для кнопок markdown
boldBtn.addEventListener("click", function () {
insertMarkdown("**");
});
italicBtn.addEventListener("click", function () {
insertMarkdown("*");
});
headerBtn.addEventListener("click", function () {
insertMarkdown("# ");
});
listBtn.addEventListener("click", function () {
insertMarkdown("- ");
});
quoteBtn.addEventListener("click", function () {
insertMarkdown("> ");
});
codeBtn.addEventListener("click", function () {
insertMarkdown("`");
});
linkBtn.addEventListener("click", function () {
insertMarkdown("[Текст ссылки](URL)");
});
// Функция для загрузки заметок с сервера
async function loadNotes() {
try {
const response = await fetch("/api/notes");
if (!response.ok) {
throw new Error("Ошибка загрузки заметок");
}
const notes = await response.json();
renderNotes(notes);
} catch (error) {
console.error("Ошибка:", error);
notesList.innerHTML = "<p>Ошибка загрузки заметок</p>";
}
}
// Функция для отображения заметок
function renderNotes(notes) {
notesList.innerHTML = "";
// Итерируемся по заметкам в обычном порядке, чтобы новые были сверху
notes.forEach(function (note) {
const noteHtml = `
<div id="note" class="container">
<div class="date">
${note.date} ${note.time}
<div id="editBtn" class="notesHeaderBtn" data-id="${
note.id
}">Редактировать</div>
<div id="deleteBtn" class="notesHeaderBtn" data-id="${
note.id
}">Удалить</div>
</div>
<div class="textNote" data-original-content="${note.content.replace(
/"/g,
"&quot;"
)}">${marked.parse(note.content)}</div>
</div>
`;
notesList.insertAdjacentHTML("afterbegin", noteHtml);
});
// Добавляем обработчики событий для кнопок редактирования и удаления
addNoteEventListeners();
}
// Функция для добавления обработчиков событий к заметкам
function addNoteEventListeners() {
// Обработчик удаления
document.querySelectorAll("#deleteBtn").forEach((btn) => {
btn.addEventListener("click", async function (event) {
const noteId = event.target.dataset.id;
if (confirm("Вы уверены, что хотите удалить эту заметку?")) {
try {
const response = await fetch(`/api/notes/${noteId}`, {
method: "DELETE",
});
if (!response.ok) {
throw new Error("Ошибка удаления заметки");
}
// Перезагружаем заметки
loadNotes();
} catch (error) {
console.error("Ошибка:", error);
alert("Ошибка удаления заметки");
}
}
});
});
// Обработчик редактирования
document.querySelectorAll("#editBtn").forEach((btn) => {
btn.addEventListener("click", function (event) {
const noteId = event.target.dataset.id;
const noteContainer = event.target.closest("#note");
const noteContent = noteContainer.querySelector(".textNote");
// Создаем контейнер для markdown кнопок
const markdownButtonsContainer = document.createElement("div");
markdownButtonsContainer.classList.add("markdown-buttons");
// Создаем markdown кнопки
const markdownButtons = [
{ id: "editBoldBtn", icon: "fas fa-bold", tag: "**" },
{ id: "editItalicBtn", icon: "fas fa-italic", tag: "*" },
{ id: "editHeaderBtn", icon: "fas fa-heading", tag: "# " },
{ id: "editListBtn", icon: "fas fa-list-ul", tag: "- " },
{ id: "editQuoteBtn", icon: "fas fa-quote-right", tag: "> " },
{ id: "editCodeBtn", icon: "fas fa-code", tag: "`" },
{ id: "editLinkBtn", icon: "fas fa-link", tag: "[Текст ссылки](URL)" },
];
markdownButtons.forEach((button) => {
const btn = document.createElement("button");
btn.classList.add("btnMarkdown");
btn.id = button.id;
btn.innerHTML = `<i class="${button.icon}"></i>`;
markdownButtonsContainer.appendChild(btn);
});
// Создаем textarea с уже существующим классом textInput
const textarea = document.createElement("textarea");
textarea.classList.add("textInput");
// Получаем исходный markdown контент из data-атрибута или используем textContent как fallback
textarea.value =
noteContent.dataset.originalContent || noteContent.textContent;
// Привязываем авторасширение к textarea для редактирования
textarea.addEventListener("input", function () {
autoExpandTextarea(textarea);
});
autoExpandTextarea(textarea);
// Кнопка сохранить
const saveEditBtn = document.createElement("button");
saveEditBtn.textContent = "Сохранить";
saveEditBtn.classList.add("btnSave");
// Очищаем текущий контент и вставляем markdown кнопки, textarea и кнопку сохранить
noteContent.innerHTML = "";
noteContent.appendChild(markdownButtonsContainer);
noteContent.appendChild(textarea);
noteContent.appendChild(saveEditBtn);
// Добавляем обработчики для markdown кнопок редактирования
markdownButtons.forEach((button) => {
const btn = document.getElementById(button.id);
btn.addEventListener("click", function () {
insertMarkdownForEdit(textarea, button.tag);
});
});
// Обработчик сохранения редактирования
saveEditBtn.addEventListener("click", async function () {
if (textarea.value.trim() !== "") {
try {
const { date, time } = getFormattedDateTime();
const response = await fetch(`/api/notes/${noteId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
content: textarea.value,
date: date,
time: time,
}),
});
if (!response.ok) {
throw new Error("Ошибка сохранения заметки");
}
// Перезагружаем заметки
loadNotes();
} catch (error) {
console.error("Ошибка:", error);
alert("Ошибка сохранения заметки");
}
}
});
});
});
}
// Обработчик сохранения новой заметки
saveBtn.addEventListener("click", async function () {
if (noteInput.value.trim() !== "") {
try {
const { date, time } = getFormattedDateTime();
const response = await fetch("/api/notes", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
content: noteInput.value,
date: date,
time: time,
}),
});
if (!response.ok) {
throw new Error("Ошибка сохранения заметки");
}
// Очищаем поле ввода и перезагружаем заметки
noteInput.value = "";
noteInput.style.height = "auto";
loadNotes();
} catch (error) {
console.error("Ошибка:", error);
alert("Ошибка сохранения заметки");
}
}
});
// Загружаем заметки при загрузке страницы
document.addEventListener("DOMContentLoaded", function () {
loadNotes();
});

40
public/index.html Normal file
View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Вход в систему заметок</title>
<link rel="stylesheet" href="/style.css" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"
/>
</head>
<body>
<div class="container">
<header>Вход в систему заметок</header>
<div class="login-form">
<form action="/login" method="POST">
<div class="form-group">
<label for="password">Пароль:</label>
<input
type="password"
id="password"
name="password"
required
placeholder="Введите пароль"
/>
</div>
<button type="submit" class="btnSave">Войти</button>
</form>
<div id="errorMessage" class="error-message" style="display: none">
Неверный пароль!
</div>
</div>
</div>
<div class="footer">
<p>Создатель: <span>Fovway</span></p>
</div>
<script src="/login.js"></script>
</body>
</html>

5
public/login.js Normal file
View File

@ -0,0 +1,5 @@
// Проверяем наличие ошибки в URL
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get("error") === "invalid_password") {
document.getElementById("errorMessage").style.display = "block";
}

65
public/notes.html Normal file
View File

@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Заметки</title>
<link rel="stylesheet" href="/style.css" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"
/>
</head>
<body>
<div class="container">
<header class="notes-header">
<span>Заметки</span>
<form action="/logout" method="POST" style="display: inline">
<button type="submit" class="logout-btn">Выйти</button>
</form>
</header>
<div class="main">
<div class="markdown-buttons">
<button class="btnMarkdown" id="boldBtn">
<i class="fas fa-bold"></i>
</button>
<button class="btnMarkdown" id="italicBtn">
<i class="fas fa-italic"></i>
</button>
<button class="btnMarkdown" id="headerBtn">
<i class="fas fa-heading"></i>
</button>
<button class="btnMarkdown" id="listBtn">
<i class="fas fa-list-ul"></i>
</button>
<button class="btnMarkdown" id="quoteBtn">
<i class="fas fa-quote-right"></i>
</button>
<button class="btnMarkdown" id="codeBtn">
<i class="fas fa-code"></i>
</button>
<button class="btnMarkdown" id="linkBtn">
<i class="fas fa-link"></i>
</button>
</div>
<textarea
class="textInput"
id="noteInput"
placeholder="Ваша заметка..."
></textarea>
<button class="btnSave" id="saveBtn">Сохранить</button>
</div>
</div>
<div class="notes-container">
<div id="notesList">
<!-- Заметки будут загружаться здесь -->
</div>
</div>
<div class="footer">
<p>Создатель: <span>Fovway</span></p>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/11.1.0/marked.min.js"></script>
<script src="/app.js"></script>
</body>
</html>

237
public/style.css Normal file
View File

@ -0,0 +1,237 @@
body {
font-family: "Open Sans", sans-serif;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
justify-content: center;
background: #f5f5f5;
}
header {
font-size: 20px;
font-weight: bold;
margin-bottom: 20px;
}
.notes-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.logout-btn {
padding: 8px 16px;
cursor: pointer;
border: 1px solid #ddd;
background-color: #f8f9fa;
border-radius: 5px;
font-size: 14px;
color: #dc3545;
transition: all 0.3s ease;
}
.logout-btn:hover {
background-color: #dc3545;
color: white;
}
.container {
width: 90%;
max-width: 600px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
border-radius: 8px;
padding: 15px;
margin-top: 20px;
background: white;
}
.login-form {
margin-top: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
box-sizing: border-box;
}
.form-group input:focus {
outline: none;
border-color: #007bff;
}
.error-message {
color: #dc3545;
margin-top: 10px;
padding: 10px;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 5px;
}
textarea {
width: 100%;
min-height: 50px;
resize: none;
border: none;
background: white;
margin-bottom: 5px;
overflow-y: hidden;
}
textarea:focus {
outline: none;
}
.btnSave {
padding: 10px 20px;
cursor: pointer;
border-width: 1px;
background: white;
border-radius: 5px;
font-family: "Open Sans", sans-serif;
transition: all 0.3s ease;
font-size: 16px;
}
.btnSave:hover {
background-color: #007bff;
color: white;
border-color: #007bff;
}
.date {
font-size: 11px;
color: grey;
}
.notesHeaderBtn {
display: inline-block;
cursor: pointer;
color: black;
font-weight: bold;
margin-left: 10px;
}
.textNote {
margin-top: 10px;
white-space: pre-wrap;
}
/* Убираем стандартные отступы для абзацев */
.textNote p {
margin: 0;
padding: 0;
}
/* Убираем маргины у заголовков */
.textNote h1,
.textNote h2,
.textNote h3,
.textNote h4,
.textNote h5,
.textNote h6 {
margin: 0;
padding: 0;
}
/* Убираем отступы у списков */
.textNote ul,
.textNote ol {
margin: 0;
padding-left: 20px;
}
/* Убираем маргины у элементов списка */
.textNote li {
margin: 0;
padding: 0;
}
/* Стили для ссылок */
.textNote a {
color: #007bff;
text-decoration: none;
}
.textNote a:hover {
text-decoration: underline;
}
/* Стили для кода */
.textNote pre {
background-color: #f5f5f5;
padding: 10px;
border-radius: 5px;
font-size: 14px;
overflow-x: auto;
}
.textNote code {
background-color: #f5f5f5;
padding: 2px 4px;
border-radius: 5px;
font-size: 14px;
}
.notes-container {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 60px; /* Отступ снизу для footer */
}
#notesList {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.markdown-buttons {
margin-top: 10px;
margin-bottom: 10px;
}
.markdown-buttons .btnMarkdown {
padding: 5px 10px;
margin-right: 5px;
cursor: pointer;
border: 1px solid #ddd;
background-color: #f0f0f0;
border-radius: 5px;
font-size: 14px;
}
.markdown-buttons .btnMarkdown:hover {
background-color: #e0e0e0;
}
.footer {
text-align: center;
font-size: 12px;
color: #999;
position: fixed;
bottom: 0;
width: 100%;
padding: 10px 0;
}
.footer span {
font-weight: bold;
}

214
server.js Normal file
View File

@ -0,0 +1,214 @@
const express = require("express");
const sqlite3 = require("sqlite3").verbose();
const bcrypt = require("bcryptjs");
const session = require("express-session");
const path = require("path");
const helmet = require("helmet");
const rateLimit = require("express-rate-limit");
const bodyParser = require("body-parser");
require("dotenv").config();
const app = express();
const PORT = process.env.PORT || 3000;
// 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:"],
},
},
})
);
// Ограничение запросов
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 минут
max: 100, // максимум 100 запросов с одного IP
});
app.use(limiter);
// Статические файлы
app.use(express.static(path.join(__dirname, "public")));
// Парсинг тела запроса
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
// Настройка сессий
app.use(
session({
secret: process.env.SESSION_SECRET || "default-secret",
resave: false,
saveUninitialized: false,
cookie: { secure: false }, // в продакшене установить true с HTTPS
})
);
// Подключение к базе данных
const db = new sqlite3.Database("./notes.db", (err) => {
if (err) {
console.error("Ошибка подключения к базе данных:", err.message);
} else {
console.log("Подключено к SQLite базе данных");
createTables();
}
});
// Создание таблиц
function createTables() {
const createNotesTable = `
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL,
date TEXT NOT NULL,
time TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`;
db.run(createNotesTable, (err) => {
if (err) {
console.error("Ошибка создания таблицы заметок:", err.message);
} else {
console.log("Таблица заметок готова");
}
});
}
// Middleware для аутентификации
function requireAuth(req, res, next) {
if (req.session.authenticated) {
return next();
} else {
return res.redirect("/");
}
}
// Маршруты
// Главная страница с формой входа
app.get("/", (req, res) => {
if (req.session.authenticated) {
return res.redirect("/notes");
}
res.sendFile(path.join(__dirname, "public", "index.html"));
});
// Обработка входа
app.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");
}
});
// Страница с заметками (требует аутентификации)
app.get("/notes", requireAuth, (req, res) => {
res.sendFile(path.join(__dirname, "public", "notes.html"));
});
// API для получения всех заметок
app.get("/api/notes", requireAuth, (req, res) => {
const sql = "SELECT * FROM notes ORDER BY created_at ASC";
db.all(sql, [], (err, rows) => {
if (err) {
console.error("Ошибка получения заметок:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
res.json(rows);
});
});
// API для создания новой заметки
app.post("/api/notes", requireAuth, (req, res) => {
const { content, date, time } = req.body;
if (!content || !date || !time) {
return res.status(400).json({ error: "Не все поля заполнены" });
}
const sql = "INSERT INTO notes (content, date, time) VALUES (?, ?, ?)";
const params = [content, date, time];
db.run(sql, params, function (err) {
if (err) {
console.error("Ошибка создания заметки:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
res.json({ id: this.lastID, content, date, time });
});
});
// API для обновления заметки
app.put("/api/notes/:id", requireAuth, (req, res) => {
const { content, date, time } = req.body;
const { id } = req.params;
if (!content || !date || !time) {
return res.status(400).json({ error: "Не все поля заполнены" });
}
const sql = "UPDATE notes SET content = ?, date = ?, time = ? WHERE id = ?";
const params = [content, date, time, id];
db.run(sql, params, function (err) {
if (err) {
console.error("Ошибка обновления заметки:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
if (this.changes === 0) {
return res.status(404).json({ error: "Заметка не найдена" });
}
res.json({ id, content, date, time });
});
});
// API для удаления заметки
app.delete("/api/notes/:id", requireAuth, (req, res) => {
const { id } = req.params;
const sql = "DELETE FROM notes WHERE id = ?";
db.run(sql, id, function (err) {
if (err) {
console.error("Ошибка удаления заметки:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
if (this.changes === 0) {
return res.status(404).json({ error: "Заметка не найдена" });
}
res.json({ message: "Заметка удалена" });
});
});
// Выход
app.post("/logout", (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: "Ошибка выхода" });
}
res.redirect("/");
});
});
// Запуск сервера
app.listen(PORT, () => {
console.log(`🚀 Сервер запущен на порту ${PORT}`);
console.log(`📝 Приложение для заметок готово к работе!`);
});