feat: adminMode — чек-бокс переключения админского интерфейса

Шаг 1: /toggle-admin роут в ui/routes/auth.js
  - переключает req.session.adminMode
  - canAdmin = isAdmin || clientId === WZ01112 (временно)

Шаг 2: resolveUser в ui/index.js
  - req.user.adminMode из req.session.adminMode
  - isAdmin = IAM.isAdmin || clientId === WZ01112 (временно)

Шаг 3: adminMode вместо isAdmin в интерфейсе
  - ui/routes/entries.js: adminMode решает админский view
  - ui/routes/admin.js: requireAdmin проверяет adminMode

Шаг 4: views/index.ejs
  - чек-бокс «Администратор» (виден только если isAdmin)
  - ссылки Аудит/Лимиты видны только при adminMode
  - селектор компаний — при adminMode
This commit is contained in:
2026-06-11 11:08:03 +04:00
parent 8c1787df19
commit 4f1f64dd6c
6 changed files with 35 additions and 9 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ipwhitelist", "name": "ipwhitelist",
"version": "0.5.37", "version": "0.5.38",
"description": "IP WhiteList microservice for cloud provider", "description": "IP WhiteList microservice for cloud provider",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
+4 -1
View File
@@ -58,6 +58,8 @@ function createUiRouter({ auth, MOCK_USERS, authLimiter }) {
? sessionActive ? sessionActive
: (allClientIds[0] || rawClientId); : (allClientIds[0] || rawClientId);
// 🔮 isAdmin через IAM, временно: || clientId === WZ01112
const iamAdmin = req.session && req.session.user && req.session.user.isAdmin;
req.user = { req.user = {
clientId: rawClientId, clientId: rawClientId,
allClientIds, allClientIds,
@@ -65,7 +67,8 @@ function createUiRouter({ auth, MOCK_USERS, authLimiter }) {
companyId: payload.company_id || payload.companyId || null, companyId: payload.company_id || payload.companyId || null,
companyName: payload.company_name || payload.companyName || activeClientId, companyName: payload.company_name || payload.companyName || activeClientId,
email: payload.email || activeClientId + '@unknown', email: payload.email || activeClientId + '@unknown',
isAdmin: activeClientId === ADMIN_CLIENT_ID, isAdmin: iamAdmin || activeClientId === ADMIN_CLIENT_ID,
adminMode: !!(req.session && req.session.adminMode),
}; };
} catch (e) { } catch (e) {
req.session.destroy(() => req.session.destroy(() =>
+1 -1
View File
@@ -14,7 +14,7 @@ const { Router } = require('express');
const api = require('../api-client'); const api = require('../api-client');
function requireAdmin(req, res, next) { function requireAdmin(req, res, next) {
if (!req.user || !req.user.isAdmin) { if (!req.user || !req.user.adminMode) {
return res.status(403).render('error', { return res.status(403).render('error', {
status: 403, status: 403,
message: 'Доступ запрещён — требуются права администратора.', message: 'Доступ запрещён — требуются права администратора.',
+14
View File
@@ -166,6 +166,20 @@ function createRouter({ auth, MOCK_USERS, authLimiter }) {
res.redirect(returnTo || '/'); res.redirect(returnTo || '/');
}); });
// ── Переключатель режима администратора ──────────────────────────────
// 🔮 isAdmin будет из IAM, временно: isAdmin || clientId === WZ01112
router.post('/toggle-admin', (req, res) => {
const canAdmin = req.session.user && (
req.session.user.isAdmin ||
req.session.user.clientId === (process.env.ADMIN_CLIENT_ID || 'WZ01112')
);
if (!canAdmin) return res.status(403).send('Forbidden');
req.session.adminMode = !req.session.adminMode;
// Редирект туда же откуда пришли
res.redirect(req.get('Referer') || '/');
});
// GET /logout // GET /logout
router.get('/logout', (req, res) => { router.get('/logout', (req, res) => {
req.session.destroy(() => res.redirect('/login')); req.session.destroy(() => res.redirect('/login'));
+3 -2
View File
@@ -77,7 +77,7 @@ function createRouter() {
try { try {
let entries = [], limit = 15, selectedCompany = null, companies = []; let entries = [], limit = 15, selectedCompany = null, companies = [];
if (req.user.isAdmin) { if (req.user.adminMode) {
const cr = await api.get('/api/v1/companies', token); const cr = await api.get('/api/v1/companies', token);
companies = cr.data.companies || []; companies = cr.data.companies || [];
@@ -120,6 +120,7 @@ function createRouter() {
used: entries.length, used: entries.length,
user: req.user, user: req.user,
isAdmin: req.user.isAdmin, isAdmin: req.user.isAdmin,
adminMode: req.user.adminMode,
companies, companies,
selectedCompany, selectedCompany,
allClientIds: hasMultiple ? req.user.allClientIds : null, allClientIds: hasMultiple ? req.user.allClientIds : null,
@@ -135,7 +136,7 @@ function createRouter() {
} catch (e) { } catch (e) {
res.render('index', { res.render('index', {
entries: [], limit: 15, used: 0, entries: [], limit: 15, used: 0,
user: req.user, isAdmin: req.user.isAdmin, user: req.user, isAdmin: req.user.isAdmin, adminMode: req.user.adminMode,
companies: [], selectedCompany: null, companies: [], selectedCompany: null,
allClientIds: null, activeClientId: null, allClientIds: null, activeClientId: null,
error: 'Ошибка загрузки: ' + e.message, error: 'Ошибка загрузки: ' + e.message,
+12 -4
View File
@@ -157,9 +157,17 @@
</svg> </svg>
<span>|</span> <span>|</span>
<span style="font-weight:500;color:var(--text);">Белые списки IP v<%= appVersion %></span> <span style="font-weight:500;color:var(--text);">Белые списки IP v<%= appVersion %></span>
<!-- Кнопки правой части header: admin-ссылки + выход --> <!-- Кнопки правой части header: admin-ссылки + чекбокс + выход -->
<span style="margin-left:auto;display:flex;gap:.5rem;align-items:center;"> <span style="margin-left:auto;display:flex;gap:.5rem;align-items:center;">
<% if (isAdmin) { %> <% if (typeof isAdmin !== 'undefined' && isAdmin) { %>
<form method="POST" action="/toggle-admin" style="display:inline;font-size:.8rem;color:var(--muted);padding:.2rem .6rem;border:1px solid var(--border);border-radius:5px;background:#fff;">
<label>
<input type="checkbox" onchange="this.form.submit()" <%= adminMode ? 'checked' : '' %>>
Администратор
</label>
</form>
<% } %>
<% if (typeof adminMode !== 'undefined' && adminMode) { %>
<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="/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="/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>
<% } %> <% } %>
@@ -179,8 +187,8 @@
const companyPk = selectedCompany ? selectedCompany.id : null; const companyPk = selectedCompany ? selectedCompany.id : null;
%> %>
<!-- Переключатель компании — для администратора --> <!-- Переключатель компании — для администратора в режиме admin -->
<% if (isAdmin && companies) { %> <% if (adminMode && companies) { %>
<div class="card"> <div class="card">
<div class="card-body" style="padding:.75rem 1rem;"> <div class="card-body" style="padding:.75rem 1rem;">
<form method="GET" action="/" style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap;"> <form method="GET" action="/" style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap;">