From a31b3e61f26415709e5fafa8f2fc3234e1926474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Mon, 15 Jun 2026 13:30:12 +0400 Subject: [PATCH] =?UTF-8?q?v0.5.102:=20=D0=B5=D0=B4=D0=B8=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81?= =?UTF-8?q?=20/v2/app=20=D0=B4=D0=BB=D1=8F=20=D1=8E=D0=B7=D0=B5=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=20=D0=B8=20=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- v2/history/2026-06-15-unified-interface.md | 30 ++ v2/server.js | 8 +- v2/src/config/index.js | 2 +- v2/src/user/index.js | 81 ++++- views/v2/user.ejs | 341 ++++++++++++++++----- 6 files changed, 371 insertions(+), 93 deletions(-) create mode 100644 v2/history/2026-06-15-unified-interface.md diff --git a/package.json b/package.json index c76d5b8..0c6f1d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ipwhitelist", - "version": "0.5.101", + "version": "0.5.102", "description": "IP WhiteList microservice for cloud provider", "main": "server.js", "scripts": { diff --git a/v2/history/2026-06-15-unified-interface.md b/v2/history/2026-06-15-unified-interface.md new file mode 100644 index 0000000..e914fe5 --- /dev/null +++ b/v2/history/2026-06-15-unified-interface.md @@ -0,0 +1,30 @@ +# V2 Unified Interface — v0.5.102 (2026-06-15) + +**Что сделано:** объединение юзерского и админского интерфейсов в один `/v2/app`. + +## Изменённые файлы + +### `v2/src/user/index.js` — единый роутер +- GET `/` — для юзеров и админов +- Админ: dropdown = все компании из БД (`getAllCompanies()`), по умолчанию своя +- Юзер: dropdown = свои профили из IAM, по умолчанию активный +- `?switchTo=W***` — смена компании: админ → любая, юзер → только свои +- CRUD без изменений (add/edit/delete через `req.clientId`) + +### `views/v2/user.ejs` — единый шаблон +- Дизайн из оригинала: лого Nubes, CSS-переменные, card-раскладка +- Хедер: лого | "IP WhiteList v{VERSION}" | email (админ) | Выйти (реальный юзер) +- Dropdown компаний + таблица IP с inline CRUD +- Статистика, форма добавления, поддержка удалённых записей + +### `v2/server.js` +- `/v2/admin` → 302 на `/v2/app` +- `toggle-admin` → всегда редирект на `/v2/app` +- Убран импорт `createAdminRouter` + +### Версии +- `v2/src/config/index.js`: 0.5.98 → 0.5.99 +- `package.json`: 0.5.101 → 0.5.102 + +## Синтаксис +`node -c` — OK на всех изменённых файлах. diff --git a/v2/server.js b/v2/server.js index 3049b67..3b1569e 100644 --- a/v2/server.js +++ b/v2/server.js @@ -28,7 +28,6 @@ const auth = require('./src/auth'); const { resolveContext } = require('./src/router'); const { enhanceImpersonation } = require('./src/impersonation'); const { createUserRouter } = require('./src/user'); -const { createAdminRouter } = require('./src/admin'); const { pool } = require('./src/db'); const { ensureSchema } = require('./src/db/schema'); @@ -53,8 +52,8 @@ function createV2Router() { // ── /v2/app — пользовательский CRUD (только с сессией) ────────────── router.use('/app', enhanceImpersonation, resolveContext, createUserRouter()); - // ── /v2/admin — админка (только для isAdmin=true) ──────────────────── - router.use('/admin', enhanceImpersonation, resolveContext, createAdminRouter()); + // ── /v2/admin — редирект на единый интерфейс ─────────────────── + router.get('/admin', (req, res) => res.redirect('/v2/app')); // ── POST /v2/toggle-admin — переключить режим администратора ──── router.post('/toggle-admin', (req, res) => { @@ -63,10 +62,9 @@ function createV2Router() { const canAdmin = (u.allClientIds || []).includes('WZ01112') || u.clientId === 'WZ01112' || !!u.isAdmin; if (!canAdmin) return res.redirect('/v2/app'); u.adminMode = !u.adminMode; - const target = u.adminMode ? '/v2/admin' : '/v2/app'; req.session.save(err => { if (err) console.error('[toggle-admin] session save error:', err.message); - res.redirect(target); + res.redirect('/v2/app'); }); }); diff --git a/v2/src/config/index.js b/v2/src/config/index.js index 4559e21..985c632 100644 --- a/v2/src/config/index.js +++ b/v2/src/config/index.js @@ -9,7 +9,7 @@ // ═══════════════════════════════════════════════════════════════════════════════ module.exports = { - version: '0.5.98', + version: '0.5.99', // ── 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 9b9903e..212712f 100644 --- a/v2/src/user/index.js +++ b/v2/src/user/index.js @@ -1,15 +1,22 @@ // ═══════════════════════════════════════════════════════════════════════════════ -// V2 — Фронтенд пользователя (Express + EJS) +// V2 — Единый фронтенд (Express + EJS) — юзеры и админы // -// ЭТО: Express-роутер, рендерит EJS-шаблоны. +// ЭТО: Express-роутер, рендерит EJS-шаблон views/v2/user.ejs. // НЕ: работа с БД (только через crud/). // -// ШАБЛОН: views/v2/user.ejs — данные: entries, limit, used, companies, -// activeClientId, user, includeDeleted, version. +// ОДИН ИНТЕРФЕЙС для юзеров и админов: +// - Юзер: dropdown = свои профили из IAM, CRUD внутри выбранной компании. +// - Админ: dropdown = все компании из БД, CRUD внутри выбранной. +// - По умолчанию: активный профиль (is_active_profile=true). +// +// ПЕРЕКЛЮЧЕНИЕ КОМПАНИИ: ?switchTo=W*** → обновляет activeClientId в сессии. +// - Юзер: только свои профили. +// - Админ: любая компания. // ═══════════════════════════════════════════════════════════════════════════════ const express = require('express'); const crud = require('../crud'); +const q = require('../db/queries'); const config = require('../config'); function createUserRouter() { @@ -18,29 +25,71 @@ function createUserRouter() { // ── GET / — список записей + форма ────────────────────────────────── router.get('/', async (req, res) => { try { + const isAdmin = req.isAdmin; const clId = req.clientId; - const allCompanies = req.profiles.length > 0 - ? req.profiles - : [{ client_id: clId, company_name: req.companyName || clId }]; + // ── Переключение компании ────────────────────────────────────── + if (req.query.switchTo) { + const targetId = req.query.switchTo; + if (isAdmin) { + // Админ может переключиться на любую компанию + req.session.user.activeClientId = targetId; + req.session.save(() => res.redirect('/v2/app')); + return; + } else { + // Юзер — только свои профили + const allowed = (req.profiles || []).find(p => p.client_id === targetId); + if (allowed) { + req.session.user.activeClientId = targetId; + req.session.save(() => res.redirect('/v2/app')); + return; + } + } + } + + // ── Компании для dropdown ─────────────────────────────────────── + let companies; + if (isAdmin) { + const all = await q.getAllCompanies(); + companies = all.map(c => ({ + client_id: c.client_id, + name: c.name || c.client_id, + active_count: c.active_count, + })); + } else { + companies = (req.profiles && req.profiles.length > 0) + ? req.profiles.map(p => ({ + client_id: p.client_id, + name: p.company_name || p.client_id, + is_active: p.is_active_profile, + })) + : [{ client_id: clId, name: req.companyName || clId, is_active: true }]; + } + + // ── Записи ────────────────────────────────────────────────────── 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'); - } + // ── Данные пользователя для шаблона ──────────────────────────── + const sessionUser = req.session && req.session.user ? req.session.user : {}; + const templateUser = { + email: req.email, + fio: sessionUser.fio || null, + isAdmin: req.canAdmin || false, + adminMode: isAdmin, + isImpersonated: req.isImpersonated || false, + originalUserEmail: sessionUser.originalUserEmail || '', + originalUserFullName: sessionUser.originalUserFullName || '', + originalUserCompany: sessionUser.originalUserCompany || '', + }; res.render('v2/user', { entries, limit, used, - companies: allCompanies, + companies, activeClientId: clId, - user: req.user || {}, + user: templateUser, includeDeleted, version: config.version, - canAdmin: req.canAdmin || false, - adminMode: req.adminMode || false, }); } catch (e) { res.status(500).send('

Ошибка

' + e.message + '
Назад'); diff --git a/views/v2/user.ejs b/views/v2/user.ejs index fe89354..c171da0 100644 --- a/views/v2/user.ejs +++ b/views/v2/user.ejs @@ -1,77 +1,278 @@ -V2 IP WhiteList - -
- V2 IP WhiteList v<%= version %> - - <% if (typeof canAdmin !== 'undefined' && canAdmin) { %> -
-
<% } %> - <%= user.email %> + <%= user.email %><% if (user.isAdmin && user.adminMode) { %> (админ)<% } %> + Выйти<% if (user.isImpersonated && user.originalUserEmail) { %> (<%= user.originalUserEmail %>)<% } %>
- -
- -
- Выйти -
-
- -

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

-
-
- - - -
-
- - - <% if (entries.length === 0) { %> - + + +
+ + + + <% if (user.isImpersonated) { %> +
+ ⚠️ Режим имперсонации — данные сохраняются от имени <%= activeClientId %> + <% if (user.originalUserEmail) { %> (вы: <%= user.originalUserEmail %>)<% } %> +
<% } %> - <% 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 { %> -
- - - -
-
- -
+ + +
+
+
+ Компания: + + + <% if (user.adminMode) { %> + Режим: Администратор <% } %> -
-

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

- + + + + + +
+
+
+
+
<%= used %> / <%= limit %>
+
записей
+
+
+
<%= activeClientId %>
+
компания
+
+
+
<%= user.email %>
+
пользователь
+
+
+
+
+ + +
+
Добавить адрес
+
+
+
+
+ + = limit ? 'disabled' : '' %>> +
+
+ + +
+ +
+
+
+
+ + +
+
Доверенные адреса
+ <% if (entries.length === 0) { %> +
Нет добавленных адресов
+ <% } else { %> +
+ + + + + + + + + + + + <% entries.forEach(function(e) { %> + + + + + + + + <% }) %> + +
Адрес / ПодсетьКомментарийДобавилДобавленаДействия
<%= e.value_cidr %><% if (e.deleted_at) { %> удалена<% } %><%= e.comment || '—' %><%= e.created_by %><%= new Date(e.created_at).toLocaleString('ru') %> + <% if (!e.deleted_at) { %> +
+ + + +
+
+ +
+ <% } %> +
+
+ <% } %> +
+ + + + + +