v2: EJS-шаблоны — user, admin, admin-audit, admin-entries
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ipwhitelist",
|
||||
"version": "0.5.96",
|
||||
"version": "0.5.97",
|
||||
"description": "IP WhiteList microservice for cloud provider",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
|
||||
+19
-156
@@ -1,65 +1,51 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// V2 — Админка
|
||||
// V2 — Админка (Express + EJS)
|
||||
//
|
||||
// ЭТО: Express-роутер для администратора (isAdmin=true).
|
||||
// НЕ: доступно обычным юзерам (middleware проверяет req.isAdmin).
|
||||
// ЭТО: Express-роутер для админа, рендерит EJS-шаблоны.
|
||||
// НЕ: доступно обычным юзерам (middleware: req.isAdmin).
|
||||
//
|
||||
// ЗАЧЕМ:
|
||||
// 1. Видеть ВСЕ компании и их статистику (дашборд).
|
||||
// 2. Смотреть аудит любой компании.
|
||||
// 3. Устанавливать персональные лимиты.
|
||||
// 4. Просматривать записи любой компании.
|
||||
//
|
||||
// ДАННЫЕ:
|
||||
// crud.list(clientId) — записи компании (через API-слой)
|
||||
// q.getAllCompanies() — все компании со счётчиками
|
||||
// q.getAudit(companyId) — аудит (null = все компании)
|
||||
// q.setLimit(id, limit) — персональный лимит
|
||||
//
|
||||
// МАРШРУТЫ:
|
||||
// GET /admin — дашборд: таблица всех компаний
|
||||
// GET /admin/audit — аудит (?companyId=X — фильтр)
|
||||
// POST /admin/limit — установить лимит (body: companyId, limit)
|
||||
// GET /admin/entries — записи компании (?companyId=X)
|
||||
// ШАБЛОНЫ:
|
||||
// views/v2/admin.ejs — дашборд (companies, user, version)
|
||||
// views/v2/admin-audit.ejs — аудит (audit, company, version)
|
||||
// views/v2/admin-entries.ejs — записи (entries, company, used, limit, ...)
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const express = require('express');
|
||||
const q = require('../db/queries'); // admin-specific: аудит, лимиты, все компании
|
||||
const crud = require('../crud'); // CRUD для просмотра записей
|
||||
const q = require('../db/queries');
|
||||
const crud = require('../crud');
|
||||
const config = require('../config');
|
||||
|
||||
function createAdminRouter() {
|
||||
const router = express.Router();
|
||||
|
||||
// ── middleware: только админы ───────────────────────────────────────
|
||||
router.use((req, res, next) => {
|
||||
if (!req.isAdmin) return res.redirect('/v2/app');
|
||||
next();
|
||||
});
|
||||
|
||||
// ── GET / — дашборд: все компании ──────────────────────────────────
|
||||
// ── GET / — дашборд ─────────────────────────────────────────────────
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const companies = await q.getAllCompanies();
|
||||
const version = require('../config').version;
|
||||
res.send(renderDashboard({ companies, user: req.user || {}, version }));
|
||||
res.render('v2/admin', { companies, user: req.user || {}, version: config.version });
|
||||
} catch (e) {
|
||||
res.status(500).send('<h2>Ошибка</h2><pre>' + e.message + '</pre>');
|
||||
}
|
||||
});
|
||||
|
||||
// ── GET /audit?companyId=X — журнал действий ───────────────────────
|
||||
// ── GET /audit ──────────────────────────────────────────────────────
|
||||
router.get('/audit', async (req, res) => {
|
||||
try {
|
||||
const companyId = req.query.companyId ? parseInt(req.query.companyId) : null;
|
||||
const audit = await q.getAudit(companyId);
|
||||
const company = companyId ? await q.getCompanyById(companyId) : null;
|
||||
res.send(renderAudit({ audit, company, version: require('../config').version }));
|
||||
res.render('v2/admin-audit', { audit, company, version: config.version });
|
||||
} catch (e) {
|
||||
res.status(500).send('<h2>Ошибка</h2><pre>' + e.message + '</pre>');
|
||||
}
|
||||
});
|
||||
|
||||
// ── POST /limit — установить персональный лимит ────────────────────
|
||||
// ── POST /limit ─────────────────────────────────────────────────────
|
||||
router.post('/limit', async (req, res) => {
|
||||
try {
|
||||
const companyId = parseInt(req.body.companyId);
|
||||
@@ -72,16 +58,15 @@ function createAdminRouter() {
|
||||
}
|
||||
});
|
||||
|
||||
// ── GET /entries?companyId=X — записи любой компании ───────────────
|
||||
// ── GET /entries ────────────────────────────────────────────────────
|
||||
router.get('/entries', async (req, res) => {
|
||||
try {
|
||||
const companyId = parseInt(req.query.companyId);
|
||||
const company = companyId ? await q.getCompanyById(companyId) : null;
|
||||
if (!company) return res.redirect('/v2/admin');
|
||||
// crud.list принимает clientId (W-номер), не внутренний id
|
||||
const { entries, used, limit } = await crud.list(company.client_id, req.query.deleted === '1');
|
||||
const version = require('../config').version;
|
||||
res.send(renderEntries({ entries, company, used, limit, includeDeleted: req.query.deleted === '1', version }));
|
||||
const includeDeleted = req.query.deleted === '1';
|
||||
const { entries, used, limit } = await crud.list(company.client_id, includeDeleted);
|
||||
res.render('v2/admin-entries', { entries, company, used, limit, includeDeleted, version: config.version });
|
||||
} catch (e) {
|
||||
res.status(500).send('<h2>Ошибка</h2><pre>' + e.message + '</pre>');
|
||||
}
|
||||
@@ -90,126 +75,4 @@ function createAdminRouter() {
|
||||
return router;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// HTML-рендеринг (временный, будет заменён на EJS)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function renderDashboard({ companies, user, version }) {
|
||||
const rows = companies.map(c => `
|
||||
<tr>
|
||||
<td>${c.name || c.client_id}</td>
|
||||
<td><code>${c.client_id}</code></td>
|
||||
<td><strong>${c.active_count}</strong> / ${c.custom_limit != null ? c.custom_limit : 15}</td>
|
||||
<td>${c.custom_limit != null ? c.custom_limit : '—'}</td>
|
||||
<td>
|
||||
<a href="/v2/admin/audit?companyId=${c.id}">аудит</a> |
|
||||
<a href="/v2/admin/entries?companyId=${c.id}">записи</a>
|
||||
</td>
|
||||
<td>
|
||||
<form method="POST" action="/v2/admin/limit" style="display:inline">
|
||||
<input type="hidden" name="companyId" value="${c.id}">
|
||||
<input name="limit" value="${c.custom_limit || 15}" size="3">
|
||||
<button>установить</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>`).join('');
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html><head><meta charset="utf-8"><title>Admin — IP WhiteList v${version}</title>
|
||||
<style>
|
||||
body{font-family:sans-serif;max-width:1100px;margin:20px auto;color:#222}
|
||||
table{width:100%;border-collapse:collapse;margin:10px 0}
|
||||
th,td{border:1px solid #ccc;padding:6px;text-align:left}
|
||||
th{background:#263238;color:#fff}
|
||||
.bar{display:flex;justify-content:space-between;align-items:center;background:#263238;color:#fff;padding:10px;margin-bottom:15px}
|
||||
.bar a{color:#80cbc4}
|
||||
button,input,select{padding:4px 8px}
|
||||
.msg{background:#4caf50;color:#fff;padding:8px;margin-bottom:10px}
|
||||
.err{background:#f44336;color:#fff;padding:8px;margin-bottom:10px}
|
||||
</style></head><body>
|
||||
<div class="bar">
|
||||
<span><strong>🛡️ ADMIN</strong> v${version}</span>
|
||||
<span>${user.email || '—'} | <a href="/v2/app">← Пользовательский</a> | <a href="/v2/logout">Выйти</a></span>
|
||||
</div>
|
||||
${msgHtml()}
|
||||
<h2>Компании (${companies.length})</h2>
|
||||
<table>
|
||||
<tr><th>Компания</th><th>Client ID</th><th>Записей</th><th>Лимит</th><th></th><th>Новый лимит</th></tr>
|
||||
${rows || '<tr><td colspan="6">Нет компаний</td></tr>'}
|
||||
</table>
|
||||
</body></html>`;
|
||||
}
|
||||
|
||||
function renderAudit({ audit, company, version }) {
|
||||
const rows = audit.map(a => `
|
||||
<tr>
|
||||
<td>${new Date(a.created_at).toLocaleString('ru')}</td>
|
||||
<td><strong>${a.action}</strong></td>
|
||||
<td>${a.user_email}</td>
|
||||
<td>${a.impersonated_by || '—'}</td>
|
||||
<td>${a.company_name || '—'}</td>
|
||||
<td>${a.old_value || '—'} → ${a.new_value || '—'}</td>
|
||||
</tr>`).join('');
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html><head><meta charset="utf-8"><title>Аудит — ${company ? company.client_id : 'Все'} — v${version}</title>
|
||||
<style>
|
||||
body{font-family:sans-serif;max-width:1200px;margin:20px auto;color:#222}
|
||||
table{width:100%;border-collapse:collapse;margin:10px 0}
|
||||
th,td{border:1px solid #ccc;padding:6px;text-align:left;font-size:13px}
|
||||
th{background:#263238;color:#fff}
|
||||
.bar{display:flex;justify-content:space-between;align-items:center;background:#263238;color:#fff;padding:10px;margin-bottom:15px}
|
||||
.bar a{color:#80cbc4}
|
||||
</style></head><body>
|
||||
<div class="bar">
|
||||
<span><strong>📋 Аудит</strong> ${company ? company.client_id : 'Все компании'}</span>
|
||||
<span><a href="/v2/admin">← Админка</a> | <a href="/v2/app">Пользовательский</a></span>
|
||||
</div>
|
||||
<table>
|
||||
<tr><th>Дата</th><th>Действие</th><th>Кто</th><th>От имени</th><th>Компания</th><th>Значения</th></tr>
|
||||
${rows || '<tr><td colspan="6">Нет записей</td></tr>'}
|
||||
</table>
|
||||
<p>Показано: ${audit.length} (макс 500)</p>
|
||||
</body></html>`;
|
||||
}
|
||||
|
||||
function renderEntries({ entries, company, used, limit, includeDeleted, version }) {
|
||||
const rows = entries.map(e => {
|
||||
const del = e.deleted_at;
|
||||
const style = del ? 'text-decoration:line-through;opacity:0.5' : '';
|
||||
return `<tr style="${style}"><td>${e.value_cidr}</td><td>${e.comment || ''}</td><td>${e.created_by}</td><td>${new Date(e.created_at).toLocaleString('ru')}</td><td>${del ? 'удалено' : 'активна'}</td></tr>`;
|
||||
}).join('');
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html><head><meta charset="utf-8"><title>Записи — ${company.client_id} — v${version}</title>
|
||||
<style>
|
||||
body{font-family:sans-serif;max-width:1000px;margin:20px auto;color:#222}
|
||||
table{width:100%;border-collapse:collapse;margin:10px 0}
|
||||
th,td{border:1px solid #ccc;padding:6px;text-align:left}
|
||||
th{background:#263238;color:#fff}
|
||||
.bar{display:flex;justify-content:space-between;align-items:center;background:#263238;color:#fff;padding:10px;margin-bottom:15px}
|
||||
.bar a{color:#80cbc4}
|
||||
</style></head><body>
|
||||
<div class="bar">
|
||||
<span><strong>📋 Записи</strong> ${company.client_id} — ${company.name || company.client_id}</span>
|
||||
<span><a href="/v2/admin">← Админка</a></span>
|
||||
</div>
|
||||
<p>Записей: <strong>${used}</strong> из ${limit} ${includeDeleted ? '(с удалёнными: ' + entries.length + ')' : ''}</p>
|
||||
<table>
|
||||
<tr><th>CIDR</th><th>Комментарий</th><th>Кто</th><th>Когда</th><th>Статус</th></tr>
|
||||
${rows || '<tr><td colspan="5">Нет записей</td></tr>'}
|
||||
</table>
|
||||
<p><a href="?companyId=${company.id}&deleted=${includeDeleted ? '0' : '1'}">${includeDeleted ? 'Скрыть удалённые' : 'Показать удалённые'}</a></p>
|
||||
</body></html>`;
|
||||
}
|
||||
|
||||
// ── msgHtml() — скрипт для показа зелёных/красных плашек ──────────────
|
||||
function msgHtml() {
|
||||
return `<script>
|
||||
const p=new URLSearchParams(location.search);
|
||||
if(p.get('msg')){document.write('<div class="msg">'+p.get('msg')+'</div>');history.replaceState(null,'','/v2/admin');}
|
||||
if(p.get('error')){document.write('<div class="err">'+p.get('error')+'</div>');history.replaceState(null,'','/v2/admin');}
|
||||
</script>`;
|
||||
}
|
||||
|
||||
module.exports = { createAdminRouter };
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
module.exports = {
|
||||
version: '0.5.96',
|
||||
version: '0.5.97',
|
||||
|
||||
// ── IAM ──────────────────────────────────────────────────────────────────
|
||||
iamUrl: process.env.V2_IAM_URL || 'https://auth-api.ngcloud.ru/api/v1/auth/user',
|
||||
|
||||
+20
-96
@@ -1,41 +1,24 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// V2 — Фронтенд обычного пользователя
|
||||
// V2 — Фронтенд пользователя (Express + EJS)
|
||||
//
|
||||
// ЭТО: Express-роутер, который рендерит HTML и принимает POST-формы.
|
||||
// НЕ: работа с БД (только через crud/), валидация (в crud/).
|
||||
// ЭТО: Express-роутер, рендерит EJS-шаблоны.
|
||||
// НЕ: работа с БД (только через crud/).
|
||||
//
|
||||
// ЗАЧЕМ:
|
||||
// 1. Единственное место где генерится HTML для юзера.
|
||||
// 2. Все действия юзера (add/edit/delete) — через POST-формы.
|
||||
// 3. Выбор компании — всегда выпадающий список (даже если одна компания).
|
||||
// 4. Сообщения об ошибках/успехе — через query-параметры (?msg=, ?error=).
|
||||
//
|
||||
// КОНТЕКСТ (из resolveContext middleware):
|
||||
// req.clientId — W-номер текущей компании
|
||||
// req.email — почта юзера
|
||||
// req.impersonatedBy — null или почта админа
|
||||
// req.profiles — список компаний для выпадающего списка
|
||||
//
|
||||
// МАРШРУТЫ:
|
||||
// GET / — список записей + форма добавления
|
||||
// POST /add — добавить CIDR (→ crud.add → 302 с ?msg=)
|
||||
// POST /edit/:id — изменить запись
|
||||
// POST /delete/:id — удалить запись (soft delete)
|
||||
// ШАБЛОН: views/v2/user.ejs — данные: entries, limit, used, companies,
|
||||
// activeClientId, user, includeDeleted, version.
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const express = require('express');
|
||||
const crud = require('../crud'); // ← единственная зависимость от данных
|
||||
const crud = require('../crud');
|
||||
const config = require('../config');
|
||||
|
||||
function createUserRouter() {
|
||||
const router = express.Router();
|
||||
|
||||
// ── GET / — главная страница ──────────────────────────────────────────
|
||||
// ── GET / — список записей + форма ──────────────────────────────────
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const clId = req.clientId;
|
||||
|
||||
// Список компаний для выпадающего списка
|
||||
// Если profiles пуст — создаём fallback из clientId (одна компания)
|
||||
const allCompanies = req.profiles.length > 0
|
||||
? req.profiles
|
||||
: [{ client_id: clId, company_name: req.companyName || clId }];
|
||||
@@ -43,31 +26,37 @@ function createUserRouter() {
|
||||
const includeDeleted = req.query.deleted === '1';
|
||||
const { entries, used, limit } = await crud.list(clId, includeDeleted);
|
||||
|
||||
// Переключение компании: запоминаем в сессии и редиректим
|
||||
// Переключение компании
|
||||
if (req.query.switchTo && allCompanies.find(c => c.client_id === req.query.switchTo)) {
|
||||
req.session.user.activeClientId = req.query.switchTo;
|
||||
return res.redirect('/v2/app');
|
||||
}
|
||||
|
||||
res.send(renderPage({ entries, limit, used, allCompanies, clId, user: req.user, includeDeleted }));
|
||||
res.render('v2/user', {
|
||||
entries, limit, used,
|
||||
companies: allCompanies,
|
||||
activeClientId: clId,
|
||||
user: req.user || {},
|
||||
includeDeleted,
|
||||
version: config.version,
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).send('<h2>Ошибка</h2><pre>' + e.message + '</pre><a href="/v2/app">Назад</a>');
|
||||
}
|
||||
});
|
||||
|
||||
// ── POST /add — добавить запись ───────────────────────────────────────
|
||||
// ── POST /add ───────────────────────────────────────────────────────
|
||||
router.post('/add', async (req, res) => {
|
||||
try {
|
||||
const result = await crud.add(req.clientId, req.body.cidr || '', req.body.comment || '', req.email, req.impersonatedBy);
|
||||
const msg = result.wasNormalized ? 'Добавлено (адрес нормализован)' : 'Добавлено';
|
||||
res.redirect('/v2/app?msg=' + encodeURIComponent(msg));
|
||||
} catch (e) {
|
||||
// Ошибка валидации/лимита/дубликата — показываем юзеру
|
||||
res.redirect('/v2/app?error=' + encodeURIComponent(e.message));
|
||||
}
|
||||
});
|
||||
|
||||
// ── POST /edit/:id — изменить запись ──────────────────────────────────
|
||||
// ── POST /edit/:id ──────────────────────────────────────────────────
|
||||
router.post('/edit/:id', async (req, res) => {
|
||||
try {
|
||||
const result = await crud.edit(parseInt(req.params.id), req.clientId, req.body.cidr || '', req.body.comment || '', req.email, req.impersonatedBy);
|
||||
@@ -78,7 +67,7 @@ function createUserRouter() {
|
||||
}
|
||||
});
|
||||
|
||||
// ── POST /delete/:id — удалить запись (soft delete) ───────────────────
|
||||
// ── POST /delete/:id ────────────────────────────────────────────────
|
||||
router.post('/delete/:id', async (req, res) => {
|
||||
try {
|
||||
await crud.remove(parseInt(req.params.id), req.clientId, req.email, req.impersonatedBy);
|
||||
@@ -91,69 +80,4 @@ function createUserRouter() {
|
||||
return router;
|
||||
}
|
||||
|
||||
// ── renderPage() → HTML ─────────────────────────────────────────────────────
|
||||
// Временный inline HTML (будет заменён на EJS-шаблоны).
|
||||
// Данные: entries, limit, used, allCompanies, clId, user, includeDeleted.
|
||||
function renderPage({ entries, limit, used, allCompanies, clId, user, includeDeleted }) {
|
||||
const companyOptions = allCompanies.map(c =>
|
||||
`<option value="${c.client_id}" ${c.client_id === clId ? 'selected' : ''}>${c.company_name || c.client_id} (${c.client_id})</option>`
|
||||
).join('');
|
||||
|
||||
const rows = entries.map(e => {
|
||||
const del = e.deleted_at;
|
||||
const style = del ? 'text-decoration:line-through;opacity:0.5' : '';
|
||||
const actions = del ? '<span style="color:#888">удалено</span>' : `
|
||||
<form method="POST" action="/v2/app/edit/${e.id}" style="display:inline">
|
||||
<input name="cidr" value="${e.value_cidr}" size="18">
|
||||
<input name="comment" value="${e.comment || ''}" size="20">
|
||||
<button>изменить</button>
|
||||
</form>
|
||||
<form method="POST" action="/v2/app/delete/${e.id}" style="display:inline">
|
||||
<button onclick="return confirm('Удалить?')">удалить</button>
|
||||
</form>`;
|
||||
return `<tr style="${style}"><td>${e.value_cidr}</td><td>${e.comment || ''}</td><td>${e.created_by}</td><td>${new Date(e.created_at).toLocaleString('ru')}</td><td>${actions}</td></tr>`;
|
||||
}).join('');
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html><head><meta charset="utf-8"><title>V2 IP WhiteList</title>
|
||||
<style>
|
||||
body{font-family:sans-serif;max-width:960px;margin:20px auto;color:#222}
|
||||
table{width:100%;border-collapse:collapse;margin:10px 0}
|
||||
th,td{border:1px solid #ccc;padding:6px;text-align:left}
|
||||
th{background:#f5f5f5}
|
||||
.bar{display:flex;justify-content:space-between;align-items:center;background:#eee;padding:10px;margin-bottom:10px}
|
||||
button,input,select{padding:4px 8px}
|
||||
.add-form{background:#f0f8ff;padding:10px;margin:10px 0}
|
||||
</style></head><body>
|
||||
<div class="bar">
|
||||
<span><strong>V2 IP WhiteList</strong> <small>v${require('../config').version}</small></span>
|
||||
<span>${user.email} ${user.isAdmin ? '| <a href="/v2/admin">ADMIN</a>' : ''}</span>
|
||||
<span>
|
||||
<form method="GET" action="/v2/app" style="display:inline">
|
||||
<select name="switchTo" onchange="this.form.submit()">${companyOptions}</select>
|
||||
</form>
|
||||
<a href="/v2/logout">Выйти</a>
|
||||
</span>
|
||||
</div>
|
||||
<script>
|
||||
const p=new URLSearchParams(location.search);
|
||||
if(p.get('msg')){document.body.insertAdjacentHTML('afterbegin','<div style="background:#4caf50;color:#fff;padding:8px">'+p.get('msg')+'</div>');history.replaceState(null,'','/v2/app')}
|
||||
if(p.get('error')){document.body.insertAdjacentHTML('afterbegin','<div style="background:#f44336;color:#fff;padding:8px">'+p.get('error')+'</div>');history.replaceState(null,'','/v2/app')}
|
||||
</script>
|
||||
<p>Записей: <strong>${used}</strong> из ${limit}</p>
|
||||
<div class="add-form">
|
||||
<form method="POST" action="/v2/app/add">
|
||||
<input name="cidr" placeholder="x.x.x.x/xx" size="18" required>
|
||||
<input name="comment" placeholder="комментарий" size="30">
|
||||
<button>Добавить</button>
|
||||
</form>
|
||||
</div>
|
||||
<table>
|
||||
<tr><th>CIDR</th><th>Комментарий</th><th>Кто</th><th>Когда</th><th></th></tr>
|
||||
${rows || '<tr><td colspan="5">Нет записей</td></tr>'}
|
||||
</table>
|
||||
<p><a href="/v2/app?deleted=${includeDeleted ? '0' : '1'}">${includeDeleted ? 'Скрыть удалённые' : 'Показать удалённые'}</a></p>
|
||||
</body></html>`;
|
||||
}
|
||||
|
||||
module.exports = { createUserRouter };
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html><head><meta charset="utf-8"><title>Аудит — <%= company ? company.client_id : 'Все' %> — v<%= version %></title>
|
||||
<style>
|
||||
body{font-family:sans-serif;max-width:1200px;margin:20px auto;color:#222}
|
||||
table{width:100%;border-collapse:collapse;margin:10px 0}
|
||||
th,td{border:1px solid #ccc;padding:6px;text-align:left;font-size:13px}
|
||||
th{background:#263238;color:#fff}
|
||||
.bar{display:flex;justify-content:space-between;align-items:center;background:#263238;color:#fff;padding:10px;margin-bottom:15px}
|
||||
.bar a{color:#80cbc4}
|
||||
</style></head><body>
|
||||
<div class="bar">
|
||||
<span><strong>📋 Аудит</strong> <%= company ? company.client_id : 'Все компании' %></span>
|
||||
<span><a href="/v2/admin">← Админка</a> | <a href="/v2/app">Пользовательский</a></span>
|
||||
</div>
|
||||
<table>
|
||||
<tr><th>Дата</th><th>Действие</th><th>Кто</th><th>От имени</th><th>Компания</th><th>Значения</th></tr>
|
||||
<% if (audit.length === 0) { %><tr><td colspan="6">Нет записей</td></tr><% } %>
|
||||
<% audit.forEach(function(a) { %>
|
||||
<tr>
|
||||
<td><%= new Date(a.created_at).toLocaleString('ru') %></td>
|
||||
<td><strong><%= a.action %></strong></td>
|
||||
<td><%= a.user_email %></td>
|
||||
<td><%= a.impersonated_by || '—' %></td>
|
||||
<td><%= a.company_name || '—' %></td>
|
||||
<td><%= a.old_value || '—' %> → <%= a.new_value || '—' %></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</table>
|
||||
<p>Показано: <%= audit.length %> (макс 500)</p>
|
||||
</body></html>
|
||||
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html><head><meta charset="utf-8"><title>Записи — <%= company.client_id %> — v<%= version %></title>
|
||||
<style>
|
||||
body{font-family:sans-serif;max-width:1000px;margin:20px auto;color:#222}
|
||||
table{width:100%;border-collapse:collapse;margin:10px 0}
|
||||
th,td{border:1px solid #ccc;padding:6px;text-align:left}
|
||||
th{background:#263238;color:#fff}
|
||||
.bar{display:flex;justify-content:space-between;align-items:center;background:#263238;color:#fff;padding:10px;margin-bottom:15px}
|
||||
.bar a{color:#80cbc4}
|
||||
</style></head><body>
|
||||
<div class="bar">
|
||||
<span><strong>📋 Записи</strong> <%= company.client_id %> — <%= company.name || company.client_id %></span>
|
||||
<span><a href="/v2/admin">← Админка</a></span>
|
||||
</div>
|
||||
<p>Записей: <strong><%= used %></strong> из <%= limit %> <%= includeDeleted ? '(с удалёнными: ' + entries.length + ')' : '' %></p>
|
||||
<table>
|
||||
<tr><th>CIDR</th><th>Комментарий</th><th>Кто</th><th>Когда</th><th>Статус</th></tr>
|
||||
<% if (entries.length === 0) { %><tr><td colspan="5">Нет записей</td></tr><% } %>
|
||||
<% entries.forEach(function(e) { %>
|
||||
<tr style="<%= e.deleted_at ? 'text-decoration:line-through;opacity:0.5' : '' %>">
|
||||
<td><%= e.value_cidr %></td>
|
||||
<td><%= e.comment || '' %></td>
|
||||
<td><%= e.created_by %></td>
|
||||
<td><%= new Date(e.created_at).toLocaleString('ru') %></td>
|
||||
<td><%= e.deleted_at ? 'удалено' : 'активна' %></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</table>
|
||||
<p><a href="?companyId=<%= company.id %>&deleted=<%= includeDeleted ? '0' : '1' %>"><%= includeDeleted ? 'Скрыть удалённые' : 'Показать удалённые' %></a></p>
|
||||
</body></html>
|
||||
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html><head><meta charset="utf-8"><title>Admin — IP WhiteList v<%= version %></title>
|
||||
<style>
|
||||
body{font-family:sans-serif;max-width:1100px;margin:20px auto;color:#222}
|
||||
table{width:100%;border-collapse:collapse;margin:10px 0}
|
||||
th,td{border:1px solid #ccc;padding:6px;text-align:left}
|
||||
th{background:#263238;color:#fff}
|
||||
.bar{display:flex;justify-content:space-between;align-items:center;background:#263238;color:#fff;padding:10px;margin-bottom:15px}
|
||||
.bar a{color:#80cbc4}
|
||||
button,input,select{padding:4px 8px}
|
||||
.msg{background:#4caf50;color:#fff;padding:8px;margin-bottom:10px}
|
||||
.err{background:#f44336;color:#fff;padding:8px;margin-bottom:10px}
|
||||
</style></head><body>
|
||||
<div class="bar">
|
||||
<span><strong>🛡️ ADMIN</strong> v<%= version %></span>
|
||||
<span><%= user.email || '—' %> | <a href="/v2/app">← Пользовательский</a> | <a href="/v2/logout">Выйти</a></span>
|
||||
</div>
|
||||
<script>
|
||||
const p=new URLSearchParams(location.search);
|
||||
if(p.get('msg')){document.write('<div class="msg">'+p.get('msg')+'</div>');history.replaceState(null,'','/v2/admin');}
|
||||
if(p.get('error')){document.write('<div class="err">'+p.get('error')+'</div>');history.replaceState(null,'','/v2/admin');}
|
||||
</script>
|
||||
<h2>Компании (<%= companies.length %>)</h2>
|
||||
<table>
|
||||
<tr><th>Компания</th><th>Client ID</th><th>Записей</th><th>Лимит</th><th></th><th>Новый лимит</th></tr>
|
||||
<% if (companies.length === 0) { %><tr><td colspan="6">Нет компаний</td></tr><% } %>
|
||||
<% companies.forEach(function(c) { %>
|
||||
<tr>
|
||||
<td><%= c.name || c.client_id %></td>
|
||||
<td><code><%= c.client_id %></code></td>
|
||||
<td><strong><%= c.active_count %></strong> / <%= c.custom_limit != null ? c.custom_limit : 15 %></td>
|
||||
<td><%= c.custom_limit != null ? c.custom_limit : '—' %></td>
|
||||
<td>
|
||||
<a href="/v2/admin/audit?companyId=<%= c.id %>">аудит</a> |
|
||||
<a href="/v2/admin/entries?companyId=<%= c.id %>">записи</a>
|
||||
</td>
|
||||
<td>
|
||||
<form method="POST" action="/v2/admin/limit" style="display:inline">
|
||||
<input type="hidden" name="companyId" value="<%= c.id %>">
|
||||
<input name="limit" value="<%= c.custom_limit || 15 %>" size="3">
|
||||
<button>установить</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</table>
|
||||
</body></html>
|
||||
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html><head><meta charset="utf-8"><title>V2 IP WhiteList</title>
|
||||
<style>
|
||||
body{font-family:sans-serif;max-width:960px;margin:20px auto;color:#222}
|
||||
table{width:100%;border-collapse:collapse;margin:10px 0}
|
||||
th,td{border:1px solid #ccc;padding:6px;text-align:left}
|
||||
th{background:#f5f5f5}
|
||||
.bar{display:flex;justify-content:space-between;align-items:center;background:#eee;padding:10px;margin-bottom:10px}
|
||||
button,input,select{padding:4px 8px}
|
||||
.add-form{background:#f0f8ff;padding:10px;margin:10px 0}
|
||||
</style></head><body>
|
||||
<div class="bar">
|
||||
<span><strong>V2 IP WhiteList</strong> <small>v<%= version %></small></span>
|
||||
<span><%= user.email %> <% if (user.isAdmin) { %>| <a href="/v2/admin">ADMIN</a><% } %></span>
|
||||
<span>
|
||||
<form method="GET" action="/v2/app" style="display:inline">
|
||||
<select name="switchTo" onchange="this.form.submit()">
|
||||
<% companies.forEach(function(c) { %>
|
||||
<option value="<%= c.client_id %>" <%= c.client_id === activeClientId ? 'selected' : '' %>><%= c.company_name || c.client_id %> (<%= c.client_id %>)</option>
|
||||
<% }) %>
|
||||
</select>
|
||||
</form>
|
||||
<a href="/v2/logout">Выйти</a>
|
||||
</span>
|
||||
</div>
|
||||
<script>
|
||||
const p=new URLSearchParams(location.search);
|
||||
if(p.get('msg')){document.body.insertAdjacentHTML('afterbegin','<div style="background:#4caf50;color:#fff;padding:8px">'+p.get('msg')+'</div>');history.replaceState(null,'','/v2/app')}
|
||||
if(p.get('error')){document.body.insertAdjacentHTML('afterbegin','<div style="background:#f44336;color:#fff;padding:8px">'+p.get('error')+'</div>');history.replaceState(null,'','/v2/app')}
|
||||
</script>
|
||||
<p>Записей: <strong><%= used %></strong> из <%= limit %></p>
|
||||
<div class="add-form">
|
||||
<form method="POST" action="/v2/app/add">
|
||||
<input name="cidr" placeholder="x.x.x.x/xx" size="18" required>
|
||||
<input name="comment" placeholder="комментарий" size="30">
|
||||
<button>Добавить</button>
|
||||
</form>
|
||||
</div>
|
||||
<table>
|
||||
<tr><th>CIDR</th><th>Комментарий</th><th>Кто</th><th>Когда</th><th></th></tr>
|
||||
<% if (entries.length === 0) { %>
|
||||
<tr><td colspan="5">Нет записей</td></tr>
|
||||
<% } %>
|
||||
<% entries.forEach(function(e) { %>
|
||||
<tr style="<%= e.deleted_at ? 'text-decoration:line-through;opacity:0.5' : '' %>">
|
||||
<td><%= e.value_cidr %></td>
|
||||
<td><%= e.comment || '' %></td>
|
||||
<td><%= e.created_by %></td>
|
||||
<td><%= new Date(e.created_at).toLocaleString('ru') %></td>
|
||||
<td>
|
||||
<% if (e.deleted_at) { %>
|
||||
<span style="color:#888">удалено</span>
|
||||
<% } else { %>
|
||||
<form method="POST" action="/v2/app/edit/<%= e.id %>" style="display:inline">
|
||||
<input name="cidr" value="<%= e.value_cidr %>" size="18">
|
||||
<input name="comment" value="<%= e.comment || '' %>" size="20">
|
||||
<button>изменить</button>
|
||||
</form>
|
||||
<form method="POST" action="/v2/app/delete/<%= e.id %>" style="display:inline">
|
||||
<button onclick="return confirm('Удалить?')">удалить</button>
|
||||
</form>
|
||||
<% } %>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</table>
|
||||
<p><a href="/v2/app?deleted=<%= includeDeleted ? '0' : '1' %>"><%= includeDeleted ? 'Скрыть удалённые' : 'Показать удалённые' %></a></p>
|
||||
</body></html>
|
||||
Reference in New Issue
Block a user