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",
|
||||
"version": "0.5.40",
|
||||
"version": "0.5.41",
|
||||
"description": "IP WhiteList microservice for cloud provider",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
|
||||
+6
-2
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user