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:
@@ -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
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ipwhitelist",
|
"name": "ipwhitelist",
|
||||||
"version": "0.5.40",
|
"version": "0.5.41",
|
||||||
"description": "IP WhiteList microservice for cloud provider",
|
"description": "IP WhiteList microservice for cloud provider",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+6
-2
@@ -58,8 +58,11 @@ function createUiRouter({ auth, MOCK_USERS, authLimiter }) {
|
|||||||
? sessionActive
|
? sessionActive
|
||||||
: (allClientIds[0] || rawClientId);
|
: (allClientIds[0] || rawClientId);
|
||||||
|
|
||||||
// 🔮 isAdmin через IAM, временно: || clientId === WZ01112
|
// 🔮 isAdmin через IAM
|
||||||
const iamAdmin = req.session && req.session.user && req.session.user.isAdmin;
|
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 = {
|
req.user = {
|
||||||
clientId: rawClientId,
|
clientId: rawClientId,
|
||||||
allClientIds,
|
allClientIds,
|
||||||
@@ -67,7 +70,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: iamAdmin || activeClientId === ADMIN_CLIENT_ID,
|
isAdmin: iamAdmin,
|
||||||
|
canAdminMode,
|
||||||
adminMode: !!(req.session && req.session.adminMode),
|
adminMode: !!(req.session && req.session.adminMode),
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
+6
-18
@@ -83,30 +83,18 @@ function createRouter() {
|
|||||||
|
|
||||||
const companyId = req.query.company ? parseInt(req.query.company, 10) : null;
|
const companyId = req.query.company ? parseInt(req.query.company, 10) : null;
|
||||||
|
|
||||||
if (!companyId) {
|
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 {
|
|
||||||
selectedCompany = companies.find(c => c.id === companyId) || null;
|
selectedCompany = companies.find(c => c.id === companyId) || null;
|
||||||
|
}
|
||||||
|
// Если компания не выбрана — показываем список компаний, без записей
|
||||||
|
// (админ выбирает компанию из выпадающего списка)
|
||||||
|
|
||||||
if (selectedCompany) {
|
if (selectedCompany) {
|
||||||
const includeDeleted = req.query.includeDeleted === 'true' ? '&includeDeleted=true' : '';
|
const includeDeleted = req.query.includeDeleted === 'true' ? '&includeDeleted=true' : '';
|
||||||
const er = await api.get('/api/v1/entries?company=' + companyId + includeDeleted, token);
|
const er = await api.get('/api/v1/entries?company=' + selectedCompany.id + includeDeleted, token);
|
||||||
entries = er.data.entries || [];
|
entries = er.data.entries || [];
|
||||||
limit = er.data.limit || 15;
|
limit = er.data.limit || 15;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Обычный пользователь: запрашиваем записи для активной компании
|
// Обычный пользователь: запрашиваем записи для активной компании
|
||||||
const activeClientId = req.user.activeClientId || req.user.clientId;
|
const activeClientId = req.user.activeClientId || req.user.clientId;
|
||||||
|
|||||||
+9
-7
@@ -159,13 +159,15 @@
|
|||||||
<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 (typeof isAdmin !== 'undefined' && isAdmin) { %>
|
<% if (typeof canAdminMode !== 'undefined' && canAdminMode) { %>
|
||||||
<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;">
|
<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;"
|
||||||
<label>
|
onclick="const t=this.querySelector('input');t.checked=!t.checked;fetch('/toggle-admin',{method:'POST'}).then(()=>location.reload())">
|
||||||
<input type="checkbox" onchange="this.form.submit()" <%= adminMode ? 'checked' : '' %>>
|
<label style="cursor:pointer;">Администратор</label>
|
||||||
Администратор
|
<input type="checkbox" <%= adminMode ? 'checked' : '' %> style="display:none;">
|
||||||
</label>
|
<span style="position:relative;width:36px;height:20px;background:<%= adminMode ? 'var(--blue)' : 'var(--border)' %>;border-radius:10px;transition:background .2s;">
|
||||||
</form>
|
<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) { %>
|
<% 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user