v2: admin — дашборд компаний, аудит, лимиты, просмотр записей
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ipwhitelist",
|
"name": "ipwhitelist",
|
||||||
"version": "0.5.92",
|
"version": "0.5.93",
|
||||||
"description": "IP WhiteList microservice for cloud provider",
|
"description": "IP WhiteList microservice for cloud provider",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+180
-9
@@ -1,23 +1,194 @@
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
// V2 — админка (пустышка)
|
// V2 — Админка (Dashboard, аудит, лимиты, все компании)
|
||||||
// Будет: аудит, лимиты, список всех компаний, toggle «Администратор»
|
//
|
||||||
|
// Вход: req.isAdmin, req.email, req.clientId из resolveContext.
|
||||||
|
// Только для админов (isAdmin=true).
|
||||||
|
// Через crud/ для CRUD, через db/queries для admin-specific (аудит, лимиты).
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const q = require('../db/queries');
|
||||||
|
const crud = require('../crud');
|
||||||
|
|
||||||
function createAdminRouter() {
|
function createAdminRouter() {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.get('/', (req, res) => {
|
// ── middleware: только админы ───────────────────────────────────────────
|
||||||
res.send(`<h2>Admin panel</h2>
|
router.use((req, res, next) => {
|
||||||
<p>email: ${req.email}</p>
|
if (!req.isAdmin) return res.redirect('/v2/app');
|
||||||
<p>clientId: ${req.clientId}</p>
|
next();
|
||||||
<p>Все компании: ${(req.allClientIds || []).join(', ') || '—'}</p>
|
});
|
||||||
<p><a href="/v2/app">Пользовательский CRUD</a> | <a href="/v2/logout">Выйти</a></p>
|
|
||||||
`);
|
// ── 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 }));
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send('<h2>Ошибка</h2><pre>' + e.message + '</pre>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── GET /audit?companyId=X ─────────────────────────────────────────────
|
||||||
|
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 }));
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send('<h2>Ошибка</h2><pre>' + e.message + '</pre>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── POST /limit ────────────────────────────────────────────────────────
|
||||||
|
router.post('/limit', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const companyId = parseInt(req.body.companyId);
|
||||||
|
const newLimit = parseInt(req.body.limit);
|
||||||
|
if (!companyId || !newLimit || newLimit < 1) throw new Error('Неверные параметры');
|
||||||
|
await q.setLimit(companyId, newLimit);
|
||||||
|
res.redirect('/v2/admin?msg=' + encodeURIComponent('Лимит установлен: ' + newLimit));
|
||||||
|
} catch (e) {
|
||||||
|
res.redirect('/v2/admin?error=' + encodeURIComponent(e.message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── GET /entries?companyId=X — записи любой компании ───────────────────
|
||||||
|
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');
|
||||||
|
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 }));
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send('<h2>Ошибка</h2><pre>' + e.message + '</pre>');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── HTML-рендеринг ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
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>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
module.exports = { createAdminRouter };
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
version: '0.5.92',
|
version: '0.5.93',
|
||||||
iamUrl: process.env.V2_IAM_API_URL || 'https://auth-api.ngcloud.ru',
|
iamUrl: process.env.V2_IAM_API_URL || 'https://auth-api.ngcloud.ru',
|
||||||
appUrl: process.env.V2_APP_URL || 'https://whitelist.nodejsk8s.services.ngcloud.ru',
|
appUrl: process.env.V2_APP_URL || 'https://whitelist.nodejsk8s.services.ngcloud.ru',
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user