Переход к серверному варианту приложения
This commit is contained in:
parent
56c0d6e648
commit
54b5608d1a
5
.cursor/rules/style.mdc
Normal file
5
.cursor/rules/style.mdc
Normal 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
34
.gitignore
vendored
Normal 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
138
README.md
Normal 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
2436
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
package.json
Normal file
32
package.json
Normal 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
391
public/app.js
Normal 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,
|
||||
"""
|
||||
)}">${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
40
public/index.html
Normal 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
5
public/login.js
Normal 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
65
public/notes.html
Normal 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
237
public/style.css
Normal 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
214
server.js
Normal 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(`📝 Приложение для заметок готово к работе!`);
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user