feat: UI — edit modal, admin-панель выбора компании, кнопки Аудит/Лимиты; views/audit.ejs, views/admin.ejs

This commit is contained in:
2026-05-30 09:16:08 +03:00
parent 8c1bafeb97
commit c319e76c74
3 changed files with 535 additions and 4 deletions
+158 -4
View File
@@ -157,6 +157,14 @@
</svg>
<span>|</span>
<span style="font-weight:500;color:var(--text);">Белые списки IP</span>
<!-- Кнопки правой части header: admin-ссылки + выход -->
<span style="margin-left:auto;display:flex;gap:.5rem;align-items:center;">
<% if (isAdmin) { %>
<a href="/audit" style="font-size:.8rem;color:var(--muted);text-decoration:none;padding:.2rem .6rem;border:1px solid var(--border);border-radius:5px;background:#fff;">Аудит</a>
<a href="/admin" style="font-size:.8rem;color:var(--muted);text-decoration:none;padding:.2rem .6rem;border:1px solid var(--border);border-radius:5px;background:#fff;">Лимиты</a>
<% } %>
<a href="/logout" style="font-size:.8rem;color:var(--muted);text-decoration:none;padding:.2rem .6rem;border:1px solid var(--border);border-radius:5px;background:#fff;">Выйти (<%= user.email %>)</a>
</span>
</header>
<div class="page">
@@ -164,6 +172,40 @@
<% if (message) { %><div class="alert <%= wasNormalized ? 'alert-warn' : 'alert-ok' %>"><%= message %></div><% } %>
<% if (error) { %><div class="alert alert-err"><%= error %></div><% } %>
<%
// Для admin: selectedCompany — выбранная компания (объект из getAllCompanies).
// Для обычного пользователя: selectedCompany — их компания (из getOrCreateCompany).
// company_id — числовой PK для передачи в скрытые поля форм.
const companyPk = selectedCompany ? selectedCompany.id : null;
%>
<!-- Переключатель компании — только для администратора -->
<% if (isAdmin && companies) { %>
<div class="card">
<div class="card-body" style="padding:.75rem 1rem;">
<form method="GET" action="/" style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap;">
<span style="font-size:.85rem;color:var(--muted);font-weight:500;">Компания:</span>
<select name="company" onchange="this.form.submit()"
style="padding:.4rem .75rem;border:1px solid var(--border);border-radius:6px;font-size:.85rem;background:#fff;">
<% companies.forEach(c => { %>
<option value="<%= c.id %>" <%= selectedCompany && c.id === selectedCompany.id ? 'selected' : '' %>>
<%= c.name %> (<%= c.client_id %>) — <%= c.active_count %> зап.
</option>
<% }) %>
</select>
<!-- Быстрый доступ к admin-страницам прямо из главной -->
<a href="/audit<%= selectedCompany ? '?company=' + selectedCompany.id : '' %>"
style="font-size:.82rem;color:var(--muted);text-decoration:none;">→ Аудит этой компании</a>
<a href="/admin"
style="font-size:.82rem;color:var(--muted);text-decoration:none;">→ Лимиты</a>
<span style="margin-left:auto;font-size:.8rem;color:var(--muted);">
Режим: <strong style="color:var(--amber);">Администратор</strong>
</span>
</form>
</div>
</div>
<% } %>
<!-- Статистика -->
<div class="card">
<div class="card-body">
@@ -173,7 +215,8 @@
<div class="lbl">записей</div>
</div>
<div class="stat">
<div class="num"><%= user.clientId %></div>
<!-- Для admin показываем clientId выбранной компании, для user — их собственный -->
<div class="num"><%= selectedCompany ? selectedCompany.client_id : user.clientId %></div>
<div class="lbl">компания</div>
</div>
<div class="stat">
@@ -189,6 +232,13 @@
<div class="card-header">Добавить адрес</div>
<div class="card-body">
<form method="POST" action="/add">
<%
// Передаём числовой PK компании в скрытом поле.
// Для admin: server.js возьмёт его из req.body.company_id.
// Для обычного пользователя: server.js игнорирует это поле
// и использует clientId из токена — защита от подмены.
%>
<input type="hidden" name="company_id" value="<%= companyPk || '' %>">
<div class="form-grid">
<div class="field">
<label>IPv4 адрес или подсеть CIDR</label>
@@ -220,7 +270,8 @@
<th>Комментарий</th>
<th>Добавил</th>
<th>Дата</th>
<th class="text-center">Действие</th>
<th>Изменён</th>
<th class="text-center">Действия</th>
</tr>
</thead>
<tbody>
@@ -230,8 +281,33 @@
<td><%= e.comment || '—' %></td>
<td><%= e.created_by %></td>
<td><%= new Date(e.created_at).toLocaleDateString('ru', {day:'numeric',month:'short',year:'numeric',hour:'2-digit',minute:'2-digit'}) %></td>
<td class="text-center">
<form method="POST" action="/delete/<%= e.id %>" style="display:inline" onsubmit="return confirm('Удалить запись?')">
<!-- Дата последнего редактирования (null если не менялась) -->
<td style="color:var(--muted);font-size:.8rem;">
<%= e.updated_at
? new Date(e.updated_at).toLocaleDateString('ru', {day:'numeric',month:'short',hour:'2-digit',minute:'2-digit'})
: '—' %>
</td>
<td class="text-center" style="white-space:nowrap;">
<%
// data-* атрибуты для JS-заполнения modal без inline-onclick с параметрами.
// Так как комментарий может содержать кавычки — EJS экранирует их в HTML.
// Но в data-атрибуте JSON::stringify безопаснее.
%>
<!-- Кнопка редактирования: открывает modal и заполняет его данными из data-* -->
<button class="btn"
data-id="<%= e.id %>"
data-value="<%= e.value_cidr %>"
data-comment="<%= e.comment || '' %>"
data-company="<%= companyPk || '' %>"
onclick="openEditModal(this)"
style="margin-right:.25rem;">
Изменить
</button>
<!-- Форма удаления: company_id нужен для admin-ветки в server.js -->
<form method="POST" action="/delete/<%= e.id %>"
style="display:inline"
onsubmit="return confirm('Удалить запись <%= e.value_cidr %>?')">
<input type="hidden" name="company_id" value="<%= companyPk || '' %>">
<button class="btn btn-danger">Удалить</button>
</form>
</td>
@@ -244,5 +320,83 @@
</div>
</div>
<!-- ══ Modal редактирования записи ══════════════════════════════════════════ -->
<!--
Модальное окно — фиксированный overlay поверх страницы.
Открывается кнопкой "Изменить" в таблице (через JS openEditModal).
Закрывается кнопкой "Отмена", кликом по backdrop или Escape.
Данные заполняются из data-* атрибутов кнопки — никаких лишних fetch-запросов.
-->
<div id="edit-modal"
style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;
background:rgba(0,0,0,.4);z-index:200;align-items:center;justify-content:center;">
<div style="background:#fff;border-radius:12px;padding:1.5rem;width:500px;
max-width:92vw;box-shadow:0 8px 32px rgba(0,0,0,.18);">
<h3 style="font-size:1rem;font-weight:600;margin-bottom:1.25rem;">Редактировать запись</h3>
<form method="POST" id="edit-form">
<!-- Скрытые поля: action (action='/edit/N') и company_id заполняет openEditModal() -->
<input type="hidden" id="edit-company-id" name="company_id" value="">
<div class="field" style="margin-bottom:.75rem;">
<label>IPv4 адрес или подсеть CIDR</label>
<input id="edit-value" name="value"
pattern="^(\d{1,3}\.){3}\d{1,3}(/\d{1,2})?$"
maxlength="18"
title="IPv4 адрес или CIDR /22/32"
required
style="padding:.5rem .75rem;border:1px solid var(--border);border-radius:6px;font-size:.9rem;">
</div>
<div class="field" style="margin-bottom:1.25rem;">
<label>Комментарий</label>
<input id="edit-comment" name="comment"
maxlength="255"
placeholder="Необязательно"
style="padding:.5rem .75rem;border:1px solid var(--border);border-radius:6px;font-size:.9rem;">
</div>
<div style="display:flex;gap:.75rem;justify-content:flex-end;">
<button type="button" class="btn" onclick="closeEditModal()">Отмена</button>
<button type="submit" class="btn btn-primary">Сохранить</button>
</div>
</form>
</div>
</div>
<script>
/**
* Открыть modal редактирования.
* Вызывается из onclick кнопки "Изменить" в таблице.
* @param {HTMLElement} btn — кнопка с data-id, data-value, data-comment, data-company
*/
function openEditModal(btn) {
// Заполняем поля из data-атрибутов (EJS уже HTML-экранировал значения)
document.getElementById('edit-value').value = btn.dataset.value;
document.getElementById('edit-comment').value = btn.dataset.comment;
document.getElementById('edit-company-id').value = btn.dataset.company;
// Устанавливаем action формы на конкретный /edit/:id
document.getElementById('edit-form').action = '/edit/' + btn.dataset.id;
// Показываем modal (display:flex для центрирования)
const modal = document.getElementById('edit-modal');
modal.style.display = 'flex';
// Фокус на поле ввода для удобства клавиатурных пользователей
setTimeout(() => document.getElementById('edit-value').focus(), 50);
}
/** Закрыть modal без сохранения */
function closeEditModal() {
document.getElementById('edit-modal').style.display = 'none';
}
// Закрытие кликом по backdrop (за пределами карточки)
document.getElementById('edit-modal').addEventListener('click', function(e) {
if (e.target === this) closeEditModal();
});
// Закрытие клавишей Escape
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeEditModal();
});
</script>
</body>
</html>