Обновлены функции обработки IP-адресов и улучшен интерфейс редактирования заметок

- Настроена обработка IP-адресов с учетом различных заголовков и удалением порта из IPv6 и IPv4 адресов.
- Добавлены новые кнопки для работы с заголовками в редакторе заметок, включая выпадающее меню для выбора уровня заголовка.
- Реализованы индикаторы для дней с созданными и отредактированными заметками в календаре.
- Обновлены стили для улучшения адаптивности интерфейса и визуального отображения элементов управления.
This commit is contained in:
Fovway 2025-10-25 00:51:29 +07:00
parent dd2a6cfa1a
commit 283e8cad63
3 changed files with 267 additions and 50 deletions

View File

@ -473,13 +473,21 @@ function insertMarkdown(tag) {
return; return;
} }
if (selected.startsWith(tag) && selected.endsWith(tag)) { // Определяем, какие теги оборачивают текст (нуждаются в двойных тегах)
// Если теги уже есть, удаляем их const wrappingTags = ["**", "*", "`"];
const isWrappingTag = wrappingTags.some(wrapTag => tag.startsWith(wrapTag));
if (isWrappingTag && selected.startsWith(tag) && selected.endsWith(tag)) {
// Если оборачивающие теги уже есть, удаляем их
noteInput.value = `${before}${selected.slice( noteInput.value = `${before}${selected.slice(
tag.length, tag.length,
-tag.length -tag.length
)}${after}`; )}${after}`;
noteInput.setSelectionRange(start, end - 2 * tag.length); noteInput.setSelectionRange(start, end - 2 * tag.length);
} else if (!isWrappingTag && selected.startsWith(tag)) {
// Если одинарные теги (заголовки, списки) уже есть, удаляем их
noteInput.value = `${before}${selected.slice(tag.length)}${after}`;
noteInput.setSelectionRange(start, end - tag.length);
} else if (selected.trim() === "") { } else if (selected.trim() === "") {
// Если текст не выделен // Если текст не выделен
if (tag === "[Текст ссылки](URL)") { if (tag === "[Текст ссылки](URL)") {
@ -491,7 +499,7 @@ function insertMarkdown(tag) {
tag === "- " || tag === "- " ||
tag === "1. " || tag === "1. " ||
tag === "> " || tag === "> " ||
tag === "# " || /^#{1,6} $/.test(tag) ||
tag === "- [ ] " tag === "- [ ] "
) { ) {
// Для списка, цитаты, заголовка и чекбокса помещаем курсор после тега // Для списка, цитаты, заголовка и чекбокса помещаем курсор после тега
@ -515,7 +523,7 @@ function insertMarkdown(tag) {
tag === "- " || tag === "- " ||
tag === "1. " || tag === "1. " ||
tag === "> " || tag === "> " ||
tag === "# " || /^#{1,6} $/.test(tag) ||
tag === "- [ ] " tag === "- [ ] "
) { ) {
// Для списка, цитаты, заголовка и чекбокса добавляем тег перед выделенным текстом // Для списка, цитаты, заголовка и чекбокса добавляем тег перед выделенным текстом
@ -609,13 +617,21 @@ function insertMarkdownForEdit(textarea, tag) {
return; return;
} }
if (selected.startsWith(tag) && selected.endsWith(tag)) { // Определяем, какие теги оборачивают текст (нуждаются в двойных тегах)
// Если теги уже есть, удаляем их const wrappingTags = ["**", "*", "`"];
const isWrappingTag = wrappingTags.some(wrapTag => tag.startsWith(wrapTag));
if (isWrappingTag && selected.startsWith(tag) && selected.endsWith(tag)) {
// Если оборачивающие теги уже есть, удаляем их
textarea.value = `${before}${selected.slice( textarea.value = `${before}${selected.slice(
tag.length, tag.length,
-tag.length -tag.length
)}${after}`; )}${after}`;
textarea.setSelectionRange(start, end - 2 * tag.length); textarea.setSelectionRange(start, end - 2 * tag.length);
} else if (!isWrappingTag && selected.startsWith(tag)) {
// Если одинарные теги (заголовки, списки) уже есть, удаляем их
textarea.value = `${before}${selected.slice(tag.length)}${after}`;
textarea.setSelectionRange(start, end - tag.length);
} else if (selected.trim() === "") { } else if (selected.trim() === "") {
// Если текст не выделен // Если текст не выделен
if (tag === "[Текст ссылки](URL)") { if (tag === "[Текст ссылки](URL)") {
@ -627,7 +643,7 @@ function insertMarkdownForEdit(textarea, tag) {
tag === "- " || tag === "- " ||
tag === "1. " || tag === "1. " ||
tag === "> " || tag === "> " ||
tag === "# " || /^#{1,6} $/.test(tag) ||
tag === "- [ ] " tag === "- [ ] "
) { ) {
// Для списка, цитаты, заголовка и чекбокса помещаем курсор после тега // Для списка, цитаты, заголовка и чекбокса помещаем курсор после тега
@ -651,7 +667,7 @@ function insertMarkdownForEdit(textarea, tag) {
tag === "- " || tag === "- " ||
tag === "1. " || tag === "1. " ||
tag === "> " || tag === "> " ||
tag === "# " || /^#{1,6} $/.test(tag) ||
tag === "- [ ] " tag === "- [ ] "
) { ) {
// Для списка, цитаты, заголовка и чекбокса добавляем тег перед выделенным текстом // Для списка, цитаты, заголовка и чекбокса добавляем тег перед выделенным текстом
@ -792,6 +808,20 @@ colorBtn.addEventListener("click", function () {
// Обработчик кнопки заголовка - открываем выпадающее меню // Обработчик кнопки заголовка - открываем выпадающее меню
headerBtn.addEventListener("click", function (event) { headerBtn.addEventListener("click", function (event) {
event.stopPropagation(); event.stopPropagation();
// Проверяем позицию и корректируем если нужно
const rect = headerDropdown.getBoundingClientRect();
const viewportWidth = window.innerWidth;
// Если меню выходит за правую границу, позиционируем его слева
if (rect.right > viewportWidth) {
headerDropdown.style.left = 'auto';
headerDropdown.style.right = '0';
} else {
headerDropdown.style.left = '0';
headerDropdown.style.right = 'auto';
}
headerDropdown.classList.toggle("show"); headerDropdown.classList.toggle("show");
}); });
@ -1380,9 +1410,9 @@ async function renderNotes(notes) {
const created = parseSQLiteUtc(note.created_at); const created = parseSQLiteUtc(note.created_at);
if (note.updated_at && note.created_at !== note.updated_at) { if (note.updated_at && note.created_at !== note.updated_at) {
const updated = parseSQLiteUtc(note.updated_at); const updated = parseSQLiteUtc(note.updated_at);
dateDisplay = `Создано: ${formatLocalDateTime( dateDisplay = `${formatLocalDateTime(
created created
)} Изменено: ${formatLocalDateTime(updated)}`; )} <span class="iconify" data-icon="mdi:pencil" style="font-size: 12px; margin-left: 8px;"></span> ${formatLocalDateTime(updated)}`;
} else { } else {
dateDisplay = formatLocalDateTime(created); dateDisplay = formatLocalDateTime(created);
} }
@ -1575,7 +1605,7 @@ function addNoteEventListeners() {
// Обработчик редактирования // Обработчик редактирования
document.querySelectorAll("#editBtn").forEach((btn) => { document.querySelectorAll("#editBtn").forEach((btn) => {
btn.addEventListener("click", function (event) { btn.addEventListener("click", function (event) {
const noteId = event.target.dataset.id; const noteId = event.target.closest("#editBtn").dataset.id;
const noteContainer = event.target.closest("#note"); const noteContainer = event.target.closest("#note");
const noteContent = noteContainer.querySelector(".textNote"); const noteContent = noteContainer.querySelector(".textNote");
@ -1596,8 +1626,9 @@ function addNoteEventListeners() {
const markdownButtons = [ const markdownButtons = [
{ id: "editBoldBtn", icon: "mdi:format-bold", tag: "**" }, { id: "editBoldBtn", icon: "mdi:format-bold", tag: "**" },
{ id: "editItalicBtn", icon: "mdi:format-italic", tag: "*" }, { id: "editItalicBtn", icon: "mdi:format-italic", tag: "*" },
{ id: "editStrikethroughBtn", icon: "mdi:format-strikethrough", tag: "~~" },
{ id: "editColorBtn", icon: "mdi:palette", tag: "color" }, { id: "editColorBtn", icon: "mdi:palette", tag: "color" },
{ id: "editHeaderBtn", icon: "mdi:format-header-1", tag: "# " }, { id: "editHeaderBtn", icon: "mdi:format-header-pound", tag: "header" },
{ id: "editListBtn", icon: "mdi:format-list-bulleted", tag: "- " }, { id: "editListBtn", icon: "mdi:format-list-bulleted", tag: "- " },
{ {
id: "editNumberedListBtn", id: "editNumberedListBtn",
@ -1617,11 +1648,66 @@ function addNoteEventListeners() {
]; ];
markdownButtons.forEach((button) => { markdownButtons.forEach((button) => {
const btn = document.createElement("button"); if (button.tag === "header") {
btn.classList.add("btnMarkdown"); // Создаем контейнер для кнопки заголовка с dropdown
btn.id = button.id; const headerContainer = document.createElement("div");
btn.innerHTML = `<span class="iconify" data-icon="${button.icon}"></span>`; headerContainer.classList.add("header-dropdown");
markdownButtonsContainer.appendChild(btn); headerContainer.style.position = "relative";
headerContainer.style.display = "inline-block";
const headerBtn = document.createElement("button");
headerBtn.classList.add("btnMarkdown");
headerBtn.id = button.id;
headerBtn.innerHTML = `
<span class="iconify" data-icon="${button.icon}"></span>
<span class="iconify" data-icon="mdi:menu-down" style="font-size: 10px; margin-left: -2px"></span>
`;
const headerDropdown = document.createElement("div");
headerDropdown.classList.add("header-dropdown-menu");
headerDropdown.style.display = "none";
// Создаем опции для каждого уровня заголовка
for (let i = 1; i <= 6; i++) {
const headerOption = document.createElement("button");
headerOption.type = "button";
headerOption.textContent = `H${i}`;
headerOption.dataset.level = i;
headerOption.addEventListener("click", function (e) {
e.stopPropagation();
const headerTag = "#".repeat(i) + " ";
insertMarkdownForEdit(textarea, headerTag);
headerDropdown.style.display = "none";
headerBtn.classList.remove("active");
});
headerDropdown.appendChild(headerOption);
}
// Обработчик открытия/закрытия dropdown
headerBtn.addEventListener("click", function (e) {
e.stopPropagation();
headerDropdown.style.display = headerDropdown.style.display === "none" ? "block" : "none";
headerBtn.classList.toggle("active");
});
// Закрытие dropdown при клике вне его
document.addEventListener("click", function closeHeaderDropdown(e) {
if (!headerContainer.contains(e.target)) {
headerDropdown.style.display = "none";
headerBtn.classList.remove("active");
}
});
headerContainer.appendChild(headerBtn);
headerContainer.appendChild(headerDropdown);
markdownButtonsContainer.appendChild(headerContainer);
} else {
const btn = document.createElement("button");
btn.classList.add("btnMarkdown");
btn.id = button.id;
btn.innerHTML = `<span class="iconify" data-icon="${button.icon}"></span>`;
markdownButtonsContainer.appendChild(btn);
}
}); });
// Создаем textarea с уже существующим классом textInput // Создаем textarea с уже существующим классом textInput
@ -2016,6 +2102,10 @@ function addNoteEventListeners() {
// Добавляем обработчики для markdown кнопок редактирования // Добавляем обработчики для markdown кнопок редактирования
markdownButtons.forEach((button) => { markdownButtons.forEach((button) => {
if (button.tag === "header") {
// Header имеет собственный обработчик в dropdown
return;
}
const btn = document.getElementById(button.id); const btn = document.getElementById(button.id);
btn.addEventListener("click", function () { btn.addEventListener("click", function () {
if (button.tag === "image") { if (button.tag === "image") {
@ -2098,6 +2188,7 @@ function addTagClickListeners() {
// Перерисовываем заметки и теги // Перерисовываем заметки и теги
await renderNotes(allNotes); await renderNotes(allNotes);
renderTags(); renderTags();
renderTagsMobile();
updateFilterIndicator(); updateFilterIndicator();
}); });
}); });
@ -2743,7 +2834,7 @@ function renderCalendar() {
const firstDay = new Date(year, month, 1); const firstDay = new Date(year, month, 1);
// Получаем последний день месяца // Получаем последний день месяца
const lastDay = new Date(year, month + 1, 0); const lastDay = new Date(year, month + 1, 0);
// Получаем день недели первого дня (0 - воскресенье, 1 - понедельник и т.д.) // Получаем день недели первого дня (0 - воскресенье, 1 - воскресенье, 1 - понедельник и т.д.)
let firstDayOfWeek = firstDay.getDay(); let firstDayOfWeek = firstDay.getDay();
// Преобразуем так, чтобы понедельник был первым днем (0) // Преобразуем так, чтобы понедельник был первым днем (0)
firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1; firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;
@ -2751,11 +2842,17 @@ function renderCalendar() {
// Очищаем календарь // Очищаем календарь
calendarDays.innerHTML = ""; calendarDays.innerHTML = "";
// Создаём Set дат, когда были созданы заметки (используем created_at) // Создаём Set дат, когда были созданы заметки (зеленые кружки)
const noteDates = new Set(); const createdNoteDates = new Set();
// Создаём Set дат, когда были отредактированы заметки (желтые кружки)
const editedNoteDates = new Set();
allNotes.forEach((note) => { allNotes.forEach((note) => {
if (note.created_at) { if (note.created_at) {
noteDates.add(formatDateFromTimestamp(note.created_at)); createdNoteDates.add(formatDateFromTimestamp(note.created_at));
}
if (note.updated_at && note.created_at !== note.updated_at) {
editedNoteDates.add(formatDateFromTimestamp(note.updated_at));
} }
}); });
@ -2777,9 +2874,12 @@ function renderCalendar() {
dayDiv.dataset.date = dateStr; dayDiv.dataset.date = dateStr;
// Проверяем, есть ли заметки на этот день // Проверяем, есть ли заметки на этот день
if (noteDates.has(dateStr)) { if (createdNoteDates.has(dateStr)) {
dayDiv.classList.add("has-notes"); dayDiv.classList.add("has-notes");
} }
if (editedNoteDates.has(dateStr)) {
dayDiv.classList.add("has-edited-notes");
}
// Проверяем, выбран ли этот день // Проверяем, выбран ли этот день
if (selectedDateFilter === dateStr) { if (selectedDateFilter === dateStr) {
@ -2817,9 +2917,12 @@ function renderCalendar() {
} }
// Проверяем, есть ли заметки на этот день // Проверяем, есть ли заметки на этот день
if (noteDates.has(dateStr)) { if (createdNoteDates.has(dateStr)) {
dayDiv.classList.add("has-notes"); dayDiv.classList.add("has-notes");
} }
if (editedNoteDates.has(dateStr)) {
dayDiv.classList.add("has-edited-notes");
}
// Проверяем, выбран ли этот день // Проверяем, выбран ли этот день
if (selectedDateFilter === dateStr) { if (selectedDateFilter === dateStr) {
@ -2852,9 +2955,12 @@ function renderCalendar() {
dayDiv.dataset.date = dateStr; dayDiv.dataset.date = dateStr;
// Проверяем, есть ли заметки на этот день // Проверяем, есть ли заметки на этот день
if (noteDates.has(dateStr)) { if (createdNoteDates.has(dateStr)) {
dayDiv.classList.add("has-notes"); dayDiv.classList.add("has-notes");
} }
if (editedNoteDates.has(dateStr)) {
dayDiv.classList.add("has-edited-notes");
}
// Проверяем, выбран ли этот день // Проверяем, выбран ли этот день
if (selectedDateFilter === dateStr) { if (selectedDateFilter === dateStr) {
@ -2945,7 +3051,9 @@ window.clearFilter = async function () {
await renderNotes(allNotes); await renderNotes(allNotes);
renderCalendar(); renderCalendar();
renderCalendarMobile();
renderTags(); renderTags();
renderTagsMobile();
updateFilterIndicator(); updateFilterIndicator();
}; };
@ -3072,11 +3180,19 @@ function renderCalendarMobile() {
// Очищаем календарь // Очищаем календарь
calendarDays.innerHTML = ""; calendarDays.innerHTML = "";
// Создаём Set дат, когда были созданы заметки (используем created_at) // Создаём Set дат, когда были созданы заметки (зеленые кружки)
const noteDates = new Set(); const createdNoteDates = new Set();
allNotes.forEach((note) => { allNotes.forEach((note) => {
if (note.created_at) { if (note.created_at) {
noteDates.add(formatDateFromTimestamp(note.created_at)); createdNoteDates.add(formatDateFromTimestamp(note.created_at));
}
});
// Создаём Set дат, когда были отредактированы заметки (желтые кружки)
const editedNoteDates = new Set();
allNotes.forEach((note) => {
if (note.updated_at && note.created_at !== note.updated_at) {
editedNoteDates.add(formatDateFromTimestamp(note.updated_at));
} }
}); });
@ -3098,9 +3214,12 @@ function renderCalendarMobile() {
dayDiv.dataset.date = dateStr; dayDiv.dataset.date = dateStr;
// Проверяем, есть ли заметки на этот день // Проверяем, есть ли заметки на этот день
if (noteDates.has(dateStr)) { if (createdNoteDates.has(dateStr)) {
dayDiv.classList.add("has-notes"); dayDiv.classList.add("has-notes");
} }
if (editedNoteDates.has(dateStr)) {
dayDiv.classList.add("has-edited-notes");
}
// Проверяем, выбран ли этот день // Проверяем, выбран ли этот день
if (selectedDateFilter === dateStr) { if (selectedDateFilter === dateStr) {
@ -3138,9 +3257,12 @@ function renderCalendarMobile() {
} }
// Проверяем, есть ли заметки на этот день // Проверяем, есть ли заметки на этот день
if (noteDates.has(dateStr)) { if (createdNoteDates.has(dateStr)) {
dayDiv.classList.add("has-notes"); dayDiv.classList.add("has-notes");
} }
if (editedNoteDates.has(dateStr)) {
dayDiv.classList.add("has-edited-notes");
}
// Проверяем, выбран ли этот день // Проверяем, выбран ли этот день
if (selectedDateFilter === dateStr) { if (selectedDateFilter === dateStr) {
@ -3173,9 +3295,12 @@ function renderCalendarMobile() {
dayDiv.dataset.date = dateStr; dayDiv.dataset.date = dateStr;
// Проверяем, есть ли заметки на этот день // Проверяем, есть ли заметки на этот день
if (noteDates.has(dateStr)) { if (createdNoteDates.has(dateStr)) {
dayDiv.classList.add("has-notes"); dayDiv.classList.add("has-notes");
} }
if (editedNoteDates.has(dateStr)) {
dayDiv.classList.add("has-edited-notes");
}
// Проверяем, выбран ли этот день // Проверяем, выбран ли этот день
if (selectedDateFilter === dateStr) { if (selectedDateFilter === dateStr) {

View File

@ -419,7 +419,7 @@ header {
box-sizing: border-box; box-sizing: border-box;
word-wrap: break-word; word-wrap: break-word;
overflow-wrap: break-word; overflow-wrap: break-word;
overflow: hidden; overflow: visible;
} }
.login-form { .login-form {
@ -808,6 +808,8 @@ textarea:focus {
.markdown-buttons { .markdown-buttons {
margin-top: 10px; margin-top: 10px;
margin-bottom: 10px; margin-bottom: 10px;
overflow: visible;
position: relative;
} }
.markdown-buttons .btnMarkdown { .markdown-buttons .btnMarkdown {
@ -837,20 +839,22 @@ textarea:focus {
.header-dropdown { .header-dropdown {
position: relative; position: relative;
display: inline-block; display: inline-block;
overflow: visible;
} }
.header-dropdown-menu { .header-dropdown-menu {
display: none; display: none;
position: absolute; position: absolute;
top: 100%; top: 100%;
left: 0; right: 0;
background: white; background: white;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 5px; border-radius: 5px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 100; z-index: 1000;
margin-top: 2px; margin-top: 2px;
min-width: 80px; min-width: 60px;
max-width: 120px;
} }
.header-dropdown-menu.show { .header-dropdown-menu.show {
@ -883,6 +887,14 @@ textarea:focus {
border-radius: 0 0 5px 5px; border-radius: 0 0 5px 5px;
} }
/* Адаптивность для выпадающего меню заголовков */
@media (max-width: 768px) {
.header-dropdown-menu {
min-width: 50px;
right: -10px; /* Смещаем чуть левее для лучшего позиционирования на мобильных */
}
}
.footer { .footer {
text-align: center; text-align: center;
font-size: 12px; font-size: 12px;
@ -1168,7 +1180,7 @@ textarea:focus {
font-weight: bold; font-weight: bold;
} }
/* Индикатор для дней с заметками */ /* Индикатор для дней с заметками (зеленый кружок) */
.calendar-day.has-notes::after { .calendar-day.has-notes::after {
content: ""; content: "";
position: absolute; position: absolute;
@ -1181,14 +1193,68 @@ textarea:focus {
border-radius: 50%; border-radius: 50%;
} }
/* Индикатор для дней с отредактированными заметками (желтый кружок) */
.calendar-day.has-edited-notes::before {
content: "";
position: absolute;
bottom: 2px;
left: 50%;
transform: translateX(-50%);
width: 4px;
height: 4px;
background-color: #ffc107;
border-radius: 50%;
}
/* Когда есть оба типа заметок - сдвигаем кружки в стороны */
.calendar-day.has-notes.has-edited-notes::before {
transform: translateX(-150%);
}
.calendar-day.has-notes.has-edited-notes::after {
transform: translateX(50%);
}
/* Индикатор для выбранного дня с заметками */ /* Индикатор для выбранного дня с заметками */
.calendar-day.selected.has-notes::after { .calendar-day.selected.has-notes::after {
background-color: #fff; background-color: #28a745;
border: 1px solid #fff;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
}
/* Индикатор для выбранного дня с отредактированными заметками */
.calendar-day.selected.has-edited-notes::before {
background-color: #ffc107;
border: 1px solid #fff;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
} }
/* Индикатор для сегодняшнего дня с заметками */ /* Индикатор для сегодняшнего дня с заметками */
.calendar-day.today.has-notes::after { .calendar-day.today.has-notes::after {
background-color: #fff; background-color: #28a745;
border: 1px solid #fff;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
}
/* Индикатор для сегодняшнего дня с отредактированными заметками */
.calendar-day.today.has-edited-notes::before {
background-color: #ffc107;
border: 1px solid #fff;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
}
/* Для дней с обоими типами заметок в выбранном состоянии */
.calendar-day.selected.has-notes.has-edited-notes::before,
.calendar-day.selected.has-notes.has-edited-notes::after {
border: 1px solid #fff;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
}
/* Для дней с обоими типами заметок в сегодняшнем состоянии */
.calendar-day.today.has-notes.has-edited-notes::before,
.calendar-day.today.has-notes.has-edited-notes::after {
border: 1px solid #fff;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
} }
/* Стили для секции тегов */ /* Стили для секции тегов */

View File

@ -14,6 +14,9 @@ require("dotenv").config();
const app = express(); const app = express();
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
// Настройка trust proxy для правильного получения IP адресов через прокси
app.set('trust proxy', true);
// Создаем директорию для аватарок, если её нет // Создаем директорию для аватарок, если её нет
const uploadsDir = path.join(__dirname, "public", "uploads"); const uploadsDir = path.join(__dirname, "public", "uploads");
if (!fs.existsSync(uploadsDir)) { if (!fs.existsSync(uploadsDir)) {
@ -293,28 +296,51 @@ function logAction(userId, actionType, details, ipAddress) {
// Функция для получения IP-адреса клиента // Функция для получения IP-адреса клиента
function getClientIP(req) { function getClientIP(req) {
// Проверяем различные заголовки, которые могут содержать внешний IP-адрес // Проверяем различные заголовки, которые могут содержать внешний IP-адрес
// Приоритет: x-forwarded-for, x-real-ip, cf-connecting-ip (Cloudflare) // Приоритет: x-forwarded-for, x-real-ip, x-client-ip, cf-connecting-ip (Cloudflare)
let ip = let ip =
req.headers["x-forwarded-for"]?.split(",")[0].trim() || req.headers["x-forwarded-for"]?.split(",")[0].trim() ||
req.headers["x-real-ip"] || req.headers["x-real-ip"] ||
req.headers["x-client-ip"] ||
req.headers["cf-connecting-ip"] || // Для Cloudflare req.headers["cf-connecting-ip"] || // Для Cloudflare
req.headers["x-forwarded-for"]?.split(",")[0].trim() || // Повтор для надежности
req.headers["x-cluster-client-ip"] || // Для кластеров req.headers["x-cluster-client-ip"] || // Для кластеров
req.connection?.remoteAddress || req.connection?.remoteAddress ||
req.socket?.remoteAddress || req.socket?.remoteAddress ||
req.connection?.socket?.remoteAddress || req.connection?.socket?.remoteAddress ||
"unknown"; "unknown";
// Убираем порт из IPv6 адреса, если есть // Очищаем IP от скобок IPv6 и портов
if (ip.includes(":") && ip.split(":").length > 2) { if (ip && ip !== "unknown") {
// Это IPv6 адрес, убираем порт // Убираем скобки IPv6
ip = ip.split(":").slice(0, -1).join(":").replace(/[[\]]/g, ""); ip = ip.replace(/[[\]]/g, "");
} else if (ip.includes(":")) {
// Это IPv4 адрес с портом, убираем порт // Проверяем, является ли это IPv6 адресом
ip = ip.split(":")[0]; if (ip.includes("::")) {
// Это IPv6 адрес (содержит "::" или несколько ":")
// IPv6 адреса могут быть в формате [::1]:port или ::1
const ipv6Match = ip.match(/^(\[)?([^\]]+)(\])?(:(\d+))?$/);
if (ipv6Match) {
ip = ipv6Match[2]; // Берем IPv6 адрес без скобок и порта
}
} else if (ip.includes(":") && !ip.includes(".")) {
// IPv6 адрес без "::" но с несколькими ":"
// Например, 2001:db8::1
// Оставляем как есть
} else if (ip.includes(":")) {
// IPv4 с портом (например, 192.168.1.1:3000)
const parts = ip.split(":");
if (parts.length === 2 && /^\d+$/.test(parts[1])) {
ip = parts[0];
}
}
// IPv4 без порта оставляем как есть
} }
return ip; // Конвертируем IPv6 localhost в IPv4 для лучшей читаемости
if (ip === "::1") {
ip = "127.0.0.1";
}
return ip || "unknown";
} }
// Миграции базы данных // Миграции базы данных