✨ Обновлены функции обработки IP-адресов и улучшен интерфейс редактирования заметок
- Настроена обработка IP-адресов с учетом различных заголовков и удалением порта из IPv6 и IPv4 адресов. - Добавлены новые кнопки для работы с заголовками в редакторе заметок, включая выпадающее меню для выбора уровня заголовка. - Реализованы индикаторы для дней с созданными и отредактированными заметками в календаре. - Обновлены стили для улучшения адаптивности интерфейса и визуального отображения элементов управления.
This commit is contained in:
parent
dd2a6cfa1a
commit
283e8cad63
185
public/app.js
185
public/app.js
@ -473,13 +473,21 @@ function insertMarkdown(tag) {
|
||||
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(
|
||||
tag.length,
|
||||
-tag.length
|
||||
)}${after}`;
|
||||
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() === "") {
|
||||
// Если текст не выделен
|
||||
if (tag === "[Текст ссылки](URL)") {
|
||||
@ -491,7 +499,7 @@ function insertMarkdown(tag) {
|
||||
tag === "- " ||
|
||||
tag === "1. " ||
|
||||
tag === "> " ||
|
||||
tag === "# " ||
|
||||
/^#{1,6} $/.test(tag) ||
|
||||
tag === "- [ ] "
|
||||
) {
|
||||
// Для списка, цитаты, заголовка и чекбокса помещаем курсор после тега
|
||||
@ -515,7 +523,7 @@ function insertMarkdown(tag) {
|
||||
tag === "- " ||
|
||||
tag === "1. " ||
|
||||
tag === "> " ||
|
||||
tag === "# " ||
|
||||
/^#{1,6} $/.test(tag) ||
|
||||
tag === "- [ ] "
|
||||
) {
|
||||
// Для списка, цитаты, заголовка и чекбокса добавляем тег перед выделенным текстом
|
||||
@ -609,13 +617,21 @@ function insertMarkdownForEdit(textarea, tag) {
|
||||
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(
|
||||
tag.length,
|
||||
-tag.length
|
||||
)}${after}`;
|
||||
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() === "") {
|
||||
// Если текст не выделен
|
||||
if (tag === "[Текст ссылки](URL)") {
|
||||
@ -627,7 +643,7 @@ function insertMarkdownForEdit(textarea, tag) {
|
||||
tag === "- " ||
|
||||
tag === "1. " ||
|
||||
tag === "> " ||
|
||||
tag === "# " ||
|
||||
/^#{1,6} $/.test(tag) ||
|
||||
tag === "- [ ] "
|
||||
) {
|
||||
// Для списка, цитаты, заголовка и чекбокса помещаем курсор после тега
|
||||
@ -651,7 +667,7 @@ function insertMarkdownForEdit(textarea, tag) {
|
||||
tag === "- " ||
|
||||
tag === "1. " ||
|
||||
tag === "> " ||
|
||||
tag === "# " ||
|
||||
/^#{1,6} $/.test(tag) ||
|
||||
tag === "- [ ] "
|
||||
) {
|
||||
// Для списка, цитаты, заголовка и чекбокса добавляем тег перед выделенным текстом
|
||||
@ -792,6 +808,20 @@ colorBtn.addEventListener("click", function () {
|
||||
// Обработчик кнопки заголовка - открываем выпадающее меню
|
||||
headerBtn.addEventListener("click", function (event) {
|
||||
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");
|
||||
});
|
||||
|
||||
@ -1380,9 +1410,9 @@ async function renderNotes(notes) {
|
||||
const created = parseSQLiteUtc(note.created_at);
|
||||
if (note.updated_at && note.created_at !== note.updated_at) {
|
||||
const updated = parseSQLiteUtc(note.updated_at);
|
||||
dateDisplay = `Создано: ${formatLocalDateTime(
|
||||
dateDisplay = `${formatLocalDateTime(
|
||||
created
|
||||
)} • Изменено: ${formatLocalDateTime(updated)}`;
|
||||
)} <span class="iconify" data-icon="mdi:pencil" style="font-size: 12px; margin-left: 8px;"></span> ${formatLocalDateTime(updated)}`;
|
||||
} else {
|
||||
dateDisplay = formatLocalDateTime(created);
|
||||
}
|
||||
@ -1575,7 +1605,7 @@ function addNoteEventListeners() {
|
||||
// Обработчик редактирования
|
||||
document.querySelectorAll("#editBtn").forEach((btn) => {
|
||||
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 noteContent = noteContainer.querySelector(".textNote");
|
||||
|
||||
@ -1596,8 +1626,9 @@ function addNoteEventListeners() {
|
||||
const markdownButtons = [
|
||||
{ id: "editBoldBtn", icon: "mdi:format-bold", tag: "**" },
|
||||
{ id: "editItalicBtn", icon: "mdi:format-italic", tag: "*" },
|
||||
{ id: "editStrikethroughBtn", icon: "mdi:format-strikethrough", tag: "~~" },
|
||||
{ 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: "editNumberedListBtn",
|
||||
@ -1617,11 +1648,66 @@ function addNoteEventListeners() {
|
||||
];
|
||||
|
||||
markdownButtons.forEach((button) => {
|
||||
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);
|
||||
if (button.tag === "header") {
|
||||
// Создаем контейнер для кнопки заголовка с dropdown
|
||||
const headerContainer = document.createElement("div");
|
||||
headerContainer.classList.add("header-dropdown");
|
||||
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
|
||||
@ -2016,6 +2102,10 @@ function addNoteEventListeners() {
|
||||
|
||||
// Добавляем обработчики для markdown кнопок редактирования
|
||||
markdownButtons.forEach((button) => {
|
||||
if (button.tag === "header") {
|
||||
// Header имеет собственный обработчик в dropdown
|
||||
return;
|
||||
}
|
||||
const btn = document.getElementById(button.id);
|
||||
btn.addEventListener("click", function () {
|
||||
if (button.tag === "image") {
|
||||
@ -2098,6 +2188,7 @@ function addTagClickListeners() {
|
||||
// Перерисовываем заметки и теги
|
||||
await renderNotes(allNotes);
|
||||
renderTags();
|
||||
renderTagsMobile();
|
||||
updateFilterIndicator();
|
||||
});
|
||||
});
|
||||
@ -2743,7 +2834,7 @@ function renderCalendar() {
|
||||
const firstDay = new Date(year, month, 1);
|
||||
// Получаем последний день месяца
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
// Получаем день недели первого дня (0 - воскресенье, 1 - понедельник и т.д.)
|
||||
// Получаем день недели первого дня (0 - воскресенье, 1 - воскресенье, 1 - понедельник и т.д.)
|
||||
let firstDayOfWeek = firstDay.getDay();
|
||||
// Преобразуем так, чтобы понедельник был первым днем (0)
|
||||
firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;
|
||||
@ -2751,11 +2842,17 @@ function renderCalendar() {
|
||||
// Очищаем календарь
|
||||
calendarDays.innerHTML = "";
|
||||
|
||||
// Создаём Set дат, когда были созданы заметки (используем created_at)
|
||||
const noteDates = new Set();
|
||||
// Создаём Set дат, когда были созданы заметки (зеленые кружки)
|
||||
const createdNoteDates = new Set();
|
||||
// Создаём Set дат, когда были отредактированы заметки (желтые кружки)
|
||||
const editedNoteDates = new Set();
|
||||
|
||||
allNotes.forEach((note) => {
|
||||
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;
|
||||
|
||||
// Проверяем, есть ли заметки на этот день
|
||||
if (noteDates.has(dateStr)) {
|
||||
if (createdNoteDates.has(dateStr)) {
|
||||
dayDiv.classList.add("has-notes");
|
||||
}
|
||||
if (editedNoteDates.has(dateStr)) {
|
||||
dayDiv.classList.add("has-edited-notes");
|
||||
}
|
||||
|
||||
// Проверяем, выбран ли этот день
|
||||
if (selectedDateFilter === dateStr) {
|
||||
@ -2817,9 +2917,12 @@ function renderCalendar() {
|
||||
}
|
||||
|
||||
// Проверяем, есть ли заметки на этот день
|
||||
if (noteDates.has(dateStr)) {
|
||||
if (createdNoteDates.has(dateStr)) {
|
||||
dayDiv.classList.add("has-notes");
|
||||
}
|
||||
if (editedNoteDates.has(dateStr)) {
|
||||
dayDiv.classList.add("has-edited-notes");
|
||||
}
|
||||
|
||||
// Проверяем, выбран ли этот день
|
||||
if (selectedDateFilter === dateStr) {
|
||||
@ -2852,9 +2955,12 @@ function renderCalendar() {
|
||||
dayDiv.dataset.date = dateStr;
|
||||
|
||||
// Проверяем, есть ли заметки на этот день
|
||||
if (noteDates.has(dateStr)) {
|
||||
if (createdNoteDates.has(dateStr)) {
|
||||
dayDiv.classList.add("has-notes");
|
||||
}
|
||||
if (editedNoteDates.has(dateStr)) {
|
||||
dayDiv.classList.add("has-edited-notes");
|
||||
}
|
||||
|
||||
// Проверяем, выбран ли этот день
|
||||
if (selectedDateFilter === dateStr) {
|
||||
@ -2945,7 +3051,9 @@ window.clearFilter = async function () {
|
||||
|
||||
await renderNotes(allNotes);
|
||||
renderCalendar();
|
||||
renderCalendarMobile();
|
||||
renderTags();
|
||||
renderTagsMobile();
|
||||
updateFilterIndicator();
|
||||
};
|
||||
|
||||
@ -3072,11 +3180,19 @@ function renderCalendarMobile() {
|
||||
// Очищаем календарь
|
||||
calendarDays.innerHTML = "";
|
||||
|
||||
// Создаём Set дат, когда были созданы заметки (используем created_at)
|
||||
const noteDates = new Set();
|
||||
// Создаём Set дат, когда были созданы заметки (зеленые кружки)
|
||||
const createdNoteDates = new Set();
|
||||
allNotes.forEach((note) => {
|
||||
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;
|
||||
|
||||
// Проверяем, есть ли заметки на этот день
|
||||
if (noteDates.has(dateStr)) {
|
||||
if (createdNoteDates.has(dateStr)) {
|
||||
dayDiv.classList.add("has-notes");
|
||||
}
|
||||
if (editedNoteDates.has(dateStr)) {
|
||||
dayDiv.classList.add("has-edited-notes");
|
||||
}
|
||||
|
||||
// Проверяем, выбран ли этот день
|
||||
if (selectedDateFilter === dateStr) {
|
||||
@ -3138,9 +3257,12 @@ function renderCalendarMobile() {
|
||||
}
|
||||
|
||||
// Проверяем, есть ли заметки на этот день
|
||||
if (noteDates.has(dateStr)) {
|
||||
if (createdNoteDates.has(dateStr)) {
|
||||
dayDiv.classList.add("has-notes");
|
||||
}
|
||||
if (editedNoteDates.has(dateStr)) {
|
||||
dayDiv.classList.add("has-edited-notes");
|
||||
}
|
||||
|
||||
// Проверяем, выбран ли этот день
|
||||
if (selectedDateFilter === dateStr) {
|
||||
@ -3173,9 +3295,12 @@ function renderCalendarMobile() {
|
||||
dayDiv.dataset.date = dateStr;
|
||||
|
||||
// Проверяем, есть ли заметки на этот день
|
||||
if (noteDates.has(dateStr)) {
|
||||
if (createdNoteDates.has(dateStr)) {
|
||||
dayDiv.classList.add("has-notes");
|
||||
}
|
||||
if (editedNoteDates.has(dateStr)) {
|
||||
dayDiv.classList.add("has-edited-notes");
|
||||
}
|
||||
|
||||
// Проверяем, выбран ли этот день
|
||||
if (selectedDateFilter === dateStr) {
|
||||
|
||||
@ -419,7 +419,7 @@ header {
|
||||
box-sizing: border-box;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
@ -808,6 +808,8 @@ textarea:focus {
|
||||
.markdown-buttons {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.markdown-buttons .btnMarkdown {
|
||||
@ -837,20 +839,22 @@ textarea:focus {
|
||||
.header-dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.header-dropdown-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
z-index: 100;
|
||||
z-index: 1000;
|
||||
margin-top: 2px;
|
||||
min-width: 80px;
|
||||
min-width: 60px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.header-dropdown-menu.show {
|
||||
@ -883,6 +887,14 @@ textarea:focus {
|
||||
border-radius: 0 0 5px 5px;
|
||||
}
|
||||
|
||||
/* Адаптивность для выпадающего меню заголовков */
|
||||
@media (max-width: 768px) {
|
||||
.header-dropdown-menu {
|
||||
min-width: 50px;
|
||||
right: -10px; /* Смещаем чуть левее для лучшего позиционирования на мобильных */
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
@ -1168,7 +1180,7 @@ textarea:focus {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Индикатор для дней с заметками */
|
||||
/* Индикатор для дней с заметками (зеленый кружок) */
|
||||
.calendar-day.has-notes::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@ -1181,14 +1193,68 @@ textarea:focus {
|
||||
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 {
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
|
||||
/* Стили для секции тегов */
|
||||
|
||||
52
server.js
52
server.js
@ -14,6 +14,9 @@ require("dotenv").config();
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Настройка trust proxy для правильного получения IP адресов через прокси
|
||||
app.set('trust proxy', true);
|
||||
|
||||
// Создаем директорию для аватарок, если её нет
|
||||
const uploadsDir = path.join(__dirname, "public", "uploads");
|
||||
if (!fs.existsSync(uploadsDir)) {
|
||||
@ -293,28 +296,51 @@ function logAction(userId, actionType, details, ipAddress) {
|
||||
// Функция для получения IP-адреса клиента
|
||||
function getClientIP(req) {
|
||||
// Проверяем различные заголовки, которые могут содержать внешний IP-адрес
|
||||
// Приоритет: x-forwarded-for, x-real-ip, cf-connecting-ip (Cloudflare)
|
||||
let ip =
|
||||
// Приоритет: x-forwarded-for, x-real-ip, x-client-ip, cf-connecting-ip (Cloudflare)
|
||||
let ip =
|
||||
req.headers["x-forwarded-for"]?.split(",")[0].trim() ||
|
||||
req.headers["x-real-ip"] ||
|
||||
req.headers["x-client-ip"] ||
|
||||
req.headers["cf-connecting-ip"] || // Для Cloudflare
|
||||
req.headers["x-forwarded-for"]?.split(",")[0].trim() || // Повтор для надежности
|
||||
req.headers["x-cluster-client-ip"] || // Для кластеров
|
||||
req.connection?.remoteAddress ||
|
||||
req.socket?.remoteAddress ||
|
||||
req.connection?.socket?.remoteAddress ||
|
||||
"unknown";
|
||||
|
||||
// Убираем порт из IPv6 адреса, если есть
|
||||
if (ip.includes(":") && ip.split(":").length > 2) {
|
||||
// Это IPv6 адрес, убираем порт
|
||||
ip = ip.split(":").slice(0, -1).join(":").replace(/[[\]]/g, "");
|
||||
} else if (ip.includes(":")) {
|
||||
// Это IPv4 адрес с портом, убираем порт
|
||||
ip = ip.split(":")[0];
|
||||
|
||||
// Очищаем IP от скобок IPv6 и портов
|
||||
if (ip && ip !== "unknown") {
|
||||
// Убираем скобки IPv6
|
||||
ip = ip.replace(/[[\]]/g, "");
|
||||
|
||||
// Проверяем, является ли это IPv6 адресом
|
||||
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";
|
||||
}
|
||||
|
||||
// Миграции базы данных
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user