simplify: toggle adminMode, canAdminMode flag, remove getOrCreateCompany fallback

- ui/index.js: canAdminMode = isAdmin AND WZ01112 (временно || WZ01112)
- views/index.ejs: checkbox → toggle с JS fetch, условие canAdminMode
- ui/routes/entries.js: убран getOrCreateCompany-фоллбек для админа
- Админ без ?company= видит список компаний, без автозагрузки
- isAdmin теперь чисто из IAM (без || WZ01112)
This commit is contained in:
2026-06-11 13:15:37 +04:00
parent 1794610f77
commit 13466ebd37
5 changed files with 111 additions and 32 deletions
+85
View File
@@ -0,0 +1,85 @@
# План упрощения — 2026-06-11
## Проблема
AI усложнил код `entries.js` фоллбеками `getOrCreateCompany`, запутанной логикой `ownCo`,
ненужными редиректами `?company=`. Пользователь указал что логика простая и не требует усложнений.
## Исходные данные (что юзер знает после входа по Keycloak)
1. **email** — из JWT (Keycloak claims)
2. **session.token** — access_token от Keycloak
3. **IAM API** (`fetchIamUser`) даёт:
- `email`
- `profiles[]` — список компаний юзера: `[{id, client_id, company_name, is_active_profile}, ...]`
- `activeProfile` — текущая активная компания
- `isAdmin` — флаг администратора
## Базовые правила
- **Компания** = общий список IP. Все юзеры компании видят одни и те же записи и могут CRUD.
- **email** пишется в `audit_log.created_by` — кто сделал CRUD.
- **client_id** (WZ01112, WZ03709…) — идентификатор компании в БД (`companies.client_id`).
## Переключение компаний (мульти-компания)
Если у юзера `profiles.length > 1` — он выбирает активную компанию через переключатель.
CRUD идёт в активную компанию.
## Админ-режим (чекбокс → toggle)
### Правило (финальное):
> Toggle «Администратор» показывается **только** когда:
> `isAdmin === true` **И** `activeClientId === 'WZ01112'`
### Временное исключение (тесты):
Пока я (WZ01112 + ntazetdinov@nubes.ru) — тоже вижу toggle, даже без `isAdmin`.
Убрать когда перейдём на чистый IAM `isAdmin`.
### Поведение toggle:
- **ВЫКЛ** — обычный режим: юзер видит свою активную компанию, CRUD только в неё
- **ВКЛ** — admin-режим: выпадающий список ВСЕХ компаний, можно выбрать любую, ссылки «Аудит» и «Лимиты»
### Реализация toggle:
- Не `<input type="checkbox">` с `onchange="this.form.submit()"` (старый вариант, не работал)
- Замена на **toggle-переключатель** (как ползунок ВКЛ/ВЫКЛ)
- `onchange``fetch('/toggle-admin', {method:'POST'})``location.reload()`
- Никакой формы, никакого CSRF — просто JS fetch и перезагрузка страницы
## Что меняем (4 файла)
### 1. `ui/index.js` — resolveUser middleware
- Добавить `canAdminMode`:
```js
canAdminMode: (iamAdmin && activeClientId === 'WZ01112') || activeClientId === 'WZ01112' // временно
```
- Убрать `isAdmin: iamAdmin || activeClientId === ADMIN_CLIENT_ID` — заменить на чистое `isAdmin: iamAdmin`
- `adminMode` остаётся из сессии
### 2. `views/index.ejs` — header
- Условие показа toggle: `<% if (canAdminMode) { %>` вместо `<% if (isAdmin) { %>`
- Заменить checkbox на toggle
- Ссылки «Аудит»/«Лимиты» — по `adminMode` (уже так)
### 3. `ui/routes/entries.js` — GET /
- **Убрать** `getOrCreateCompany`-фоллбек (строки 89-96)
- **Упростить** admin-ветку: если нет `?company=` — показать свою компанию
- Обычный пользователь: без изменений (уже работает)
### 4. `ui/routes/auth.js` — POST /toggle-admin
- Без изменений (уже работает — переключает `req.session.adminMode`)
## Почему именно так
1. **AND а не OR**: админ не должен видеть toggle в чужих компаниях — только в WZ01112
2. **Toggle а не checkbox**: чекбокс = «прочитано/не прочитано», toggle = «режим ВКЛ/ВЫКЛ» — семантически верно
3. **JS fetch а не form submit**: быстрее, без моргания страницы, без CSRF-проблем
4. **Никаких фоллбеков**: компания либо есть в списке (из IAM), либо юзер к ней не относится — не надо «создавать если нет»
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ipwhitelist",
"version": "0.5.40",
"version": "0.5.41",
"description": "IP WhiteList microservice for cloud provider",
"main": "server.js",
"scripts": {
+6 -2
View File
@@ -58,8 +58,11 @@ function createUiRouter({ auth, MOCK_USERS, authLimiter }) {
? sessionActive
: (allClientIds[0] || rawClientId);
// 🔮 isAdmin через IAM, временно: || clientId === WZ01112
// 🔮 isAdmin через IAM
const iamAdmin = req.session && req.session.user && req.session.user.isAdmin;
// ВРЕМЕННО: WZ01112 без isAdmin тоже видит toggle (для тестов)
const canAdminMode = !!(iamAdmin && activeClientId === ADMIN_CLIENT_ID)
|| activeClientId === ADMIN_CLIENT_ID; // ← убрать после тестов
req.user = {
clientId: rawClientId,
allClientIds,
@@ -67,7 +70,8 @@ function createUiRouter({ auth, MOCK_USERS, authLimiter }) {
companyId: payload.company_id || payload.companyId || null,
companyName: payload.company_name || payload.companyName || activeClientId,
email: payload.email || activeClientId + '@unknown',
isAdmin: iamAdmin || activeClientId === ADMIN_CLIENT_ID,
isAdmin: iamAdmin,
canAdminMode,
adminMode: !!(req.session && req.session.adminMode),
};
} catch (e) {
+10 -22
View File
@@ -83,29 +83,17 @@ function createRouter() {
const companyId = req.query.company ? parseInt(req.query.company, 10) : null;
if (!companyId) {
// Админ без ?company= — показываем свою компанию
let ownCo = companies.find(c => c.client_id === req.user.clientId) || null;
if (!ownCo) {
await api.get('/api/v1/entries', token); // создаст компанию через getOrCreateCompany
const cr2 = await api.get('/api/v1/companies', token);
ownCo = (cr2.data.companies || []).find(c => c.client_id === req.user.clientId) || null;
}
if (ownCo) {
const includeDeleted = req.query.includeDeleted === 'true' ? '&includeDeleted=true' : '';
const er = await api.get('/api/v1/entries?company=' + ownCo.id + includeDeleted, token);
entries = er.data.entries || [];
limit = er.data.limit || 15;
selectedCompany = ownCo;
}
} else {
if (companyId) {
selectedCompany = companies.find(c => c.id === companyId) || null;
if (selectedCompany) {
const includeDeleted = req.query.includeDeleted === 'true' ? '&includeDeleted=true' : '';
const er = await api.get('/api/v1/entries?company=' + companyId + includeDeleted, token);
entries = er.data.entries || [];
limit = er.data.limit || 15;
}
}
// Если компания не выбрана — показываем список компаний, без записей
// (админ выбирает компанию из выпадающего списка)
if (selectedCompany) {
const includeDeleted = req.query.includeDeleted === 'true' ? '&includeDeleted=true' : '';
const er = await api.get('/api/v1/entries?company=' + selectedCompany.id + includeDeleted, token);
entries = er.data.entries || [];
limit = er.data.limit || 15;
}
} else {
// Обычный пользователь: запрашиваем записи для активной компании
+9 -7
View File
@@ -159,13 +159,15 @@
<span style="font-weight:500;color:var(--text);">Белые списки IP v<%= appVersion %></span>
<!-- Кнопки правой части header: admin-ссылки + чекбокс + выход -->
<span style="margin-left:auto;display:flex;gap:.5rem;align-items:center;">
<% 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 canAdminMode !== 'undefined' && canAdminMode) { %>
<span style="font-size:.8rem;color:var(--muted);display:flex;align-items:center;gap:.3rem;padding:.2rem .6rem;border:1px solid var(--border);border-radius:5px;background:#fff;cursor:pointer;"
onclick="const t=this.querySelector('input');t.checked=!t.checked;fetch('/toggle-admin',{method:'POST'}).then(()=>location.reload())">
<label style="cursor:pointer;">Администратор</label>
<input type="checkbox" <%= adminMode ? 'checked' : '' %> style="display:none;">
<span style="position:relative;width:36px;height:20px;background:<%= adminMode ? 'var(--blue)' : 'var(--border)' %>;border-radius:10px;transition:background .2s;">
<span style="position:absolute;top:2px;<%= adminMode ? 'right:2px;' : 'left:2px;' %>width:16px;height:16px;background:#fff;border-radius:50%;transition:all .2s;"></span>
</span>
</span>
<% } %>
<% 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>