diff --git a/package.json b/package.json index 5309b12..d5699d2 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/v2/src/admin/index.js b/v2/src/admin/index.js index 284e381..6f26e07 100644 --- a/v2/src/admin/index.js +++ b/v2/src/admin/index.js @@ -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('

Ошибка

' + e.message + '
'); } }); - // ── 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('

Ошибка

' + e.message + '
'); } }); - // ── 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('

Ошибка

' + e.message + '
'); } @@ -90,126 +75,4 @@ function createAdminRouter() { return router; } -// ═══════════════════════════════════════════════════════════════════════════ -// HTML-рендеринг (временный, будет заменён на EJS) -// ═══════════════════════════════════════════════════════════════════════════ - -function renderDashboard({ companies, user, version }) { - const rows = companies.map(c => ` - - ${c.name || c.client_id} - ${c.client_id} - ${c.active_count} / ${c.custom_limit != null ? c.custom_limit : 15} - ${c.custom_limit != null ? c.custom_limit : '—'} - - аудит | - записи - - -
- - - -
- - `).join(''); - - return ` -Admin — IP WhiteList v${version} - -
- 🛡️ ADMIN v${version} - ${user.email || '—'} | ← Пользовательский | Выйти -
-${msgHtml()} -

Компании (${companies.length})

- - - ${rows || ''} -
КомпанияClient IDЗаписейЛимитНовый лимит
Нет компаний
-`; -} - -function renderAudit({ audit, company, version }) { - const rows = audit.map(a => ` - - ${new Date(a.created_at).toLocaleString('ru')} - ${a.action} - ${a.user_email} - ${a.impersonated_by || '—'} - ${a.company_name || '—'} - ${a.old_value || '—'} → ${a.new_value || '—'} - `).join(''); - - return ` -Аудит — ${company ? company.client_id : 'Все'} — v${version} - -
- 📋 Аудит ${company ? company.client_id : 'Все компании'} - ← Админка | Пользовательский -
- - - ${rows || ''} -
ДатаДействиеКтоОт имениКомпанияЗначения
Нет записей
-

Показано: ${audit.length} (макс 500)

-`; -} - -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 `${e.value_cidr}${e.comment || ''}${e.created_by}${new Date(e.created_at).toLocaleString('ru')}${del ? 'удалено' : 'активна'}`; - }).join(''); - - return ` -Записи — ${company.client_id} — v${version} - -
- 📋 Записи ${company.client_id} — ${company.name || company.client_id} - ← Админка -
-

Записей: ${used} из ${limit} ${includeDeleted ? '(с удалёнными: ' + entries.length + ')' : ''}

- - - ${rows || ''} -
CIDRКомментарийКтоКогдаСтатус
Нет записей
-

${includeDeleted ? 'Скрыть удалённые' : 'Показать удалённые'}

-`; -} - -// ── msgHtml() — скрипт для показа зелёных/красных плашек ────────────── -function msgHtml() { - return ``; -} - module.exports = { createAdminRouter }; diff --git a/v2/src/config/index.js b/v2/src/config/index.js index a6b320d..8d3e552 100644 --- a/v2/src/config/index.js +++ b/v2/src/config/index.js @@ -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', diff --git a/v2/src/user/index.js b/v2/src/user/index.js index 86c59f3..676c283 100644 --- a/v2/src/user/index.js +++ b/v2/src/user/index.js @@ -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('

Ошибка

' + e.message + '
Назад'); } }); - // ── 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 => - `` - ).join(''); - - const rows = entries.map(e => { - const del = e.deleted_at; - const style = del ? 'text-decoration:line-through;opacity:0.5' : ''; - const actions = del ? 'удалено' : ` -
- - - -
-
- -
`; - return `${e.value_cidr}${e.comment || ''}${e.created_by}${new Date(e.created_at).toLocaleString('ru')}${actions}`; - }).join(''); - - return ` -V2 IP WhiteList - -
- V2 IP WhiteList v${require('../config').version} - ${user.email} ${user.isAdmin ? '| ADMIN' : ''} - -
- -
- Выйти -
-
- -

Записей: ${used} из ${limit}

-
-
- - - -
-
- - - ${rows || ''} -
CIDRКомментарийКтоКогда
Нет записей
-

${includeDeleted ? 'Скрыть удалённые' : 'Показать удалённые'}

-`; -} - module.exports = { createUserRouter }; diff --git a/views/v2/admin-audit.ejs b/views/v2/admin-audit.ejs new file mode 100644 index 0000000..547cdbf --- /dev/null +++ b/views/v2/admin-audit.ejs @@ -0,0 +1,30 @@ + +Аудит — <%= company ? company.client_id : 'Все' %> — v<%= version %> + +
+ 📋 Аудит <%= company ? company.client_id : 'Все компании' %> + ← Админка | Пользовательский +
+ + + <% if (audit.length === 0) { %><% } %> + <% audit.forEach(function(a) { %> + + + + + + + + + <% }) %> +
ДатаДействиеКтоОт имениКомпанияЗначения
Нет записей
<%= new Date(a.created_at).toLocaleString('ru') %><%= a.action %><%= a.user_email %><%= a.impersonated_by || '—' %><%= a.company_name || '—' %><%= a.old_value || '—' %> → <%= a.new_value || '—' %>
+

Показано: <%= audit.length %> (макс 500)

+ diff --git a/views/v2/admin-entries.ejs b/views/v2/admin-entries.ejs new file mode 100644 index 0000000..e277802 --- /dev/null +++ b/views/v2/admin-entries.ejs @@ -0,0 +1,30 @@ + +Записи — <%= company.client_id %> — v<%= version %> + +
+ 📋 Записи <%= company.client_id %> — <%= company.name || company.client_id %> + ← Админка +
+

Записей: <%= used %> из <%= limit %> <%= includeDeleted ? '(с удалёнными: ' + entries.length + ')' : '' %>

+ + + <% if (entries.length === 0) { %><% } %> + <% entries.forEach(function(e) { %> + + + + + + + + <% }) %> +
CIDRКомментарийКтоКогдаСтатус
Нет записей
<%= e.value_cidr %><%= e.comment || '' %><%= e.created_by %><%= new Date(e.created_at).toLocaleString('ru') %><%= e.deleted_at ? 'удалено' : 'активна' %>
+

<%= includeDeleted ? 'Скрыть удалённые' : 'Показать удалённые' %>

+ diff --git a/views/v2/admin.ejs b/views/v2/admin.ejs new file mode 100644 index 0000000..3afdb8a --- /dev/null +++ b/views/v2/admin.ejs @@ -0,0 +1,47 @@ + +Admin — IP WhiteList v<%= version %> + +
+ 🛡️ ADMIN v<%= version %> + <%= user.email || '—' %> | ← Пользовательский | Выйти +
+ +

Компании (<%= companies.length %>)

+ + + <% if (companies.length === 0) { %><% } %> + <% companies.forEach(function(c) { %> + + + + + + + + + <% }) %> +
КомпанияClient IDЗаписейЛимитНовый лимит
Нет компаний
<%= c.name || c.client_id %><%= c.client_id %><%= c.active_count %> / <%= c.custom_limit != null ? c.custom_limit : 15 %><%= c.custom_limit != null ? c.custom_limit : '—' %> + аудит | + записи + +
+ + + +
+
+ diff --git a/views/v2/user.ejs b/views/v2/user.ejs new file mode 100644 index 0000000..67e74ac --- /dev/null +++ b/views/v2/user.ejs @@ -0,0 +1,68 @@ + +V2 IP WhiteList + +
+ V2 IP WhiteList v<%= version %> + <%= user.email %> <% if (user.isAdmin) { %>| ADMIN<% } %> + +
+ +
+ Выйти +
+
+ +

Записей: <%= used %> из <%= limit %>

+
+
+ + + +
+
+ + + <% if (entries.length === 0) { %> + + <% } %> + <% entries.forEach(function(e) { %> + + + + + + + + <% }) %> +
CIDRКомментарийКтоКогда
Нет записей
<%= e.value_cidr %><%= e.comment || '' %><%= e.created_by %><%= new Date(e.created_at).toLocaleString('ru') %> + <% if (e.deleted_at) { %> + удалено + <% } else { %> +
+ + + +
+
+ +
+ <% } %> +
+

<%= includeDeleted ? 'Скрыть удалённые' : 'Показать удалённые' %>

+