v2: разделение слоёв — crud/index.js API-слой, user/ и test/ через crud
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ipwhitelist",
|
"name": "ipwhitelist",
|
||||||
"version": "0.5.89",
|
"version": "0.5.90",
|
||||||
"description": "IP WhiteList microservice for cloud provider",
|
"description": "IP WhiteList microservice for cloud provider",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -159,3 +159,213 @@ req.v2_profiles → req.profiles
|
|||||||
req.v2_companyName → req.companyName
|
req.v2_companyName → req.companyName
|
||||||
```
|
```
|
||||||
Причина: в основном коде нет префиксов.
|
Причина: в основном коде нет префиксов.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Текущее состояние (0.5.89)
|
||||||
|
|
||||||
|
### Структура v2/
|
||||||
|
|
||||||
|
```
|
||||||
|
v2/
|
||||||
|
├── server.js # createV2Router() — монтируется в server.js как /v2
|
||||||
|
├── history/
|
||||||
|
│ └── 2026-06-12.md # этот файл
|
||||||
|
└── src/
|
||||||
|
├── auth/index.js # fetchIamUser(), login(), exchangeCode(), buildAuthUrl()
|
||||||
|
├── config/index.js # V2_* env, version, умолчания
|
||||||
|
├── router/index.js # resolveContext middleware (email, clientId, isAdmin, impersonation)
|
||||||
|
├── user/index.js # createUserRouter() — UI CRUD с выбором компании
|
||||||
|
├── admin/index.js # createAdminRouter() — ПУСТЫШКА (email, clientId)
|
||||||
|
├── crud/index.js # createCrudRouter() — старый, НЕ монтирован
|
||||||
|
├── db/
|
||||||
|
│ ├── index.js # pg pool
|
||||||
|
│ ├── queries.js # CRUD: getOrCreateCompany, listEntries, createEntry, updateEntry, deleteEntry, getAudit, getLimit, setLimit, getExportCIDRs, getAllCompanies
|
||||||
|
│ └── schema.js # ensureSchema(pool) — автосоздание v2_companies, v2_entries, v2_audit
|
||||||
|
├── test/
|
||||||
|
│ ├── index.js # createTestRouter() — /v2/test?action=... и GET /chaos
|
||||||
|
│ └── test.sh # 54 curl-теста
|
||||||
|
└── validators/index.js # validate(), overlaps(), cidrToRange(), aggregateCIDRs(), BLOCKED_RANGES
|
||||||
|
```
|
||||||
|
|
||||||
|
### Маршруты /v2
|
||||||
|
|
||||||
|
| Маршрут | Что | Авторизация |
|
||||||
|
|---------|-----|------------|
|
||||||
|
| `/v2/login` | Редирект на `/login?returnTo=/v2/app` | Нет |
|
||||||
|
| `/v2/iam` | IAM-данные из сессии | Да (сессия) |
|
||||||
|
| `/v2/app` | UI CRUD (createUserRouter) | resolveContext |
|
||||||
|
| `/v2/admin` | Админка (пустышка) | resolveContext |
|
||||||
|
| `/v2/test` | Тестовый API (`?action=...`) | Нет |
|
||||||
|
| `/v2/test/chaos` | Параллельный хаос-тест | Нет |
|
||||||
|
| `/v2/logout` | Редирект на `/logout` | Нет |
|
||||||
|
|
||||||
|
### Деплой
|
||||||
|
|
||||||
|
git push → Gitea → Nubes UI redeploy → `whitelist.nodejsk8s.services.ngcloud.ru`
|
||||||
|
|
||||||
|
VM (italo.kube5s.ru) — НЕ используется для деплоя v2.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Тесты — полный список
|
||||||
|
|
||||||
|
### 1. test.sh — 54 curl-теста (✅ все пройдены)
|
||||||
|
|
||||||
|
**Запрещённые диапазоны (14 тестов):**
|
||||||
|
10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10, 127.0.0.0/8,
|
||||||
|
169.254.0.0/16, 192.0.0.0/24, 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24,
|
||||||
|
198.18.0.0/15, 224.0.0.0/4, 240.0.0.0/4, 255.255.255.255/32
|
||||||
|
|
||||||
|
**Нормализация и маски (5 тестов):**
|
||||||
|
- /22 работает, /21 отклонена, /33 отклонена, IPv6 отклонён, домен отклонён
|
||||||
|
- Нормализация хостовой части (13.0.0.5/24 → 13.0.0.0/24)
|
||||||
|
- wasNormalized=true
|
||||||
|
|
||||||
|
**Лимиты (2 теста):**
|
||||||
|
- 16-я запись отклонена (лимит 15)
|
||||||
|
- Сообщение о лимите
|
||||||
|
|
||||||
|
**CRUD полный цикл (10 тестов):**
|
||||||
|
- Добавление, добавление 2, добавление /32
|
||||||
|
- Список — 3 записи
|
||||||
|
- Изменение CIDR, проверка в БД
|
||||||
|
- Soft delete, список после удаления, с удалёнными
|
||||||
|
|
||||||
|
**Изоляция компаний (3 теста):**
|
||||||
|
- Добавление в компанию A/B, пересечение разрешено
|
||||||
|
|
||||||
|
**Переключение компаний (5 тестов):**
|
||||||
|
- Добавление в активную, переключение, список, добавление в другую, обратно
|
||||||
|
|
||||||
|
**Имперсонация — двойной аудит (6 тестов):**
|
||||||
|
- created_by = originalUserEmail
|
||||||
|
- audit: impersonated_by, user_email
|
||||||
|
- UPDATE: old→new записаны
|
||||||
|
- DELETE записан
|
||||||
|
|
||||||
|
**Нагрузка (2 теста):**
|
||||||
|
- 50 добавлений (первые 15 ок, остальные лимит)
|
||||||
|
- БД: ровно 15 записей
|
||||||
|
|
||||||
|
**Экспорт (1 тест):**
|
||||||
|
- Эндпоинт доступен
|
||||||
|
|
||||||
|
### 2. Chaos-тест — параллельные пользователи (✅)
|
||||||
|
|
||||||
|
`GET /v2/test/chaos` — 4 компании × 10 записей = 40 параллельных операций.
|
||||||
|
|
||||||
|
Проверки:
|
||||||
|
- ok: 40, errors: 0
|
||||||
|
- totalEntries: 40, totalAudit: 40
|
||||||
|
- 4 компании по 10 записей, без дубликатов, в лимите
|
||||||
|
|
||||||
|
Префиксы CIDR: 11, 12, 13, 14 (10.x заблокирован 10.0.0.0/8).
|
||||||
|
|
||||||
|
### 3. Router — resolveContext (✅ 9 тестов)
|
||||||
|
|
||||||
|
Файл: `v2/src/router/test.js` (или внутренние).
|
||||||
|
|
||||||
|
1. нет сессии → /v2/login
|
||||||
|
2. нет user → /v2/login
|
||||||
|
3. обычный юзер 1 компания
|
||||||
|
4. юзер 2 компании → activeClientId
|
||||||
|
5. админ adminMode=true
|
||||||
|
6. админ adminMode=false
|
||||||
|
7. имперсонация с originalUserEmail
|
||||||
|
8. имперсонация без originalUserEmail
|
||||||
|
9. без activeClientId → fallback на clientId
|
||||||
|
|
||||||
|
### Сводка
|
||||||
|
|
||||||
|
| Группа | Кол-во | Статус |
|
||||||
|
|--------|--------|--------|
|
||||||
|
| test.sh | 54 | ✅ |
|
||||||
|
| Chaos | 1 | ✅ |
|
||||||
|
| Router | 9 | ✅ |
|
||||||
|
| **Всего** | **64** | **✅** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## НЕ протестировано
|
||||||
|
|
||||||
|
| Модуль | Причина |
|
||||||
|
|--------|---------|
|
||||||
|
| **auth/** (login, exchangeCode, fetchIamUser) | Нужен `/v2/callback` в redirect URIs production KC |
|
||||||
|
| **admin/** | Пустышка — только email/clientId |
|
||||||
|
| **user/** UI (EJS) | Не тестировался UI-интерфейс |
|
||||||
|
| Интеграция через основную сессию | Не проверялся полный путь KC → IAM → CRUD |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что дальше (план)
|
||||||
|
|
||||||
|
1. Реализовать **admin/** — аудит, лимиты, все компании
|
||||||
|
2. Реализовать **export/** — выгрузка CIDR
|
||||||
|
3. Добавить `/v2/callback` в production KC → протестировать auth/
|
||||||
|
4. EJS-шаблоны вместо inline HTML
|
||||||
|
5. Интеграция v2 модулей в основной код (убрать префиксы v2_)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Известные ошибки (исправлены)
|
||||||
|
|
||||||
|
| Баг | Причина | Фикс |
|
||||||
|
|-----|---------|------|
|
||||||
|
| `10.x` CIDR → 45 ошибок в chaos | `10.0.0.0/8` в BLOCKED_RANGES | Префиксы 11–14 |
|
||||||
|
| auditCount=0, wZ10001Entries=0 | Проверяли только WZ10001 | Проверка всех 4 компаний |
|
||||||
|
| 105 ошибок из 150 | 3 задачи × 15 > лимит 15 | 1 задача × 10 на компанию |
|
||||||
|
| `/v2/test/chaos` → 302 /login | Роутер на `/test/api`, а не `/test` | Сменили mount на `/test` |
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Разделение слоёв (0.5.90+) — ПЛАН
|
||||||
|
|
||||||
|
### Проблема
|
||||||
|
|
||||||
|
Сейчас `user/index.js` знает про `db/queries.js` и `getOrCreateCompany`/`companyId`:
|
||||||
|
```
|
||||||
|
user/index.js → db/queries.js → SQL
|
||||||
|
```
|
||||||
|
|
||||||
|
Фронтенд смешан с логикой БД.
|
||||||
|
|
||||||
|
### Решение: три слоя
|
||||||
|
|
||||||
|
```
|
||||||
|
user/index.js (фронтенд, Express) → crud/index.js (API БД) → db/queries.js (SQL)
|
||||||
|
```
|
||||||
|
|
||||||
|
**crud/index.js** — чистые функции-агностики:
|
||||||
|
- Не знает про Express, req, res, сессии
|
||||||
|
- Принимает `clientId` (W-номер), сам резолвит `companyId` через `getOrCreateCompany`
|
||||||
|
- Вход: plain values, Выход: plain object или throw
|
||||||
|
|
||||||
|
```js
|
||||||
|
list(clientId) → { entries, used, limit }
|
||||||
|
add(clientId, cidr, comment, email, impBy) → { entry, wasNormalized }
|
||||||
|
edit(entryId, clientId, cidr, comment, email, impBy) → { entry, wasNormalized }
|
||||||
|
remove(entryId, clientId, email, impBy) → void
|
||||||
|
```
|
||||||
|
|
||||||
|
**user/index.js** — только Express:
|
||||||
|
- Дёргает crud.*, рендерит HTML
|
||||||
|
- Не знает про companyId, getOrCreateCompany, SQL
|
||||||
|
|
||||||
|
**db/queries.js** — без изменений (чистый SQL)
|
||||||
|
|
||||||
|
### Что изменится
|
||||||
|
|
||||||
|
| Файл | Было | Стало |
|
||||||
|
|------|------|-------|
|
||||||
|
| crud/index.js | старый, не рабочий | новый API-слой |
|
||||||
|
| user/index.js | require('../db/queries') | require('../crud') |
|
||||||
|
| test/index.js | require('../db/queries') | require('../crud') |
|
||||||
|
|
||||||
|
### Преимущества
|
||||||
|
|
||||||
|
1. **Тестирование**: crud тестируется без Express, user тестируется с моком crud
|
||||||
|
2. **Переиспользование**: admin, test, export — все через crud
|
||||||
|
3. **Независимость**: фронтенд не знает про deviceId, БД-схему, транзакции
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
version: '0.5.89',
|
version: '0.5.90',
|
||||||
iamUrl: process.env.V2_IAM_API_URL || 'https://auth-api.ngcloud.ru',
|
iamUrl: process.env.V2_IAM_API_URL || 'https://auth-api.ngcloud.ru',
|
||||||
appUrl: process.env.V2_APP_URL || 'https://whitelist.nodejsk8s.services.ngcloud.ru',
|
appUrl: process.env.V2_APP_URL || 'https://whitelist.nodejsk8s.services.ngcloud.ru',
|
||||||
};
|
};
|
||||||
|
|||||||
+31
-205
@@ -1,217 +1,43 @@
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
// V2 — CRUD-роутер (только для авторизованных)
|
// V2 — CRUD API-слой (чистые функции, без Express)
|
||||||
//
|
//
|
||||||
// Вход: sessionUser из v2_ сессии (email, clientId, allClientIds, profiles, ...)
|
// Принимает clientId (W-номер), сам резолвит companyId через getOrCreateCompany.
|
||||||
// Всегда показывает выбор компании (даже если одна).
|
// Фронтенд (user/) и тесты (test/) дергают эти функции.
|
||||||
// Переключение — смена activeClientId в сессии (без IAM).
|
//
|
||||||
|
// Сигнатуры:
|
||||||
|
// list(clientId) → { entries, used, limit }
|
||||||
|
// add(clientId, cidr, comment, email, impBy) → { entry, wasNormalized }
|
||||||
|
// edit(entryId, clientId, cidr, comment, email, impBy) → { entry, wasNormalized }
|
||||||
|
// remove(entryId, clientId, email, impBy) → void
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const express = require('express');
|
|
||||||
const q = require('../db/queries');
|
const q = require('../db/queries');
|
||||||
const config = require('../config');
|
|
||||||
const { validate } = require('../validators');
|
|
||||||
|
|
||||||
function createCrudRouter() {
|
async function resolve(clientId) {
|
||||||
const router = express.Router();
|
return q.getOrCreateCompany(clientId, clientId);
|
||||||
|
}
|
||||||
|
|
||||||
// ── middleware: только для залогиненных ─────────────────────────────────
|
async function list(clientId, includeDeleted = false) {
|
||||||
router.use((req, res, next) => {
|
const co = await resolve(clientId);
|
||||||
if (!req.session.user) return res.redirect('/v2/login');
|
const entries = await q.listEntries(co.id, includeDeleted);
|
||||||
next();
|
const limit = await q.getLimit(co);
|
||||||
});
|
|
||||||
|
|
||||||
// ── helpers ──────────────────────────────────────────────────────────────
|
|
||||||
function user(req) { return req.session.user; }
|
|
||||||
function token(req) { return req.session.token; }
|
|
||||||
|
|
||||||
// activeClientId — текущая выбранная компания
|
|
||||||
function activeClientId(req) {
|
|
||||||
return req.session.user.activeClientId || req.session.user.clientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Список компаний для выпадающего списка
|
|
||||||
function companies(req) {
|
|
||||||
const u = user(req);
|
|
||||||
if (u.profiles && u.profiles.length > 0) return u.profiles;
|
|
||||||
// fallback: одна компания из clientId
|
|
||||||
return [{ client_id: u.clientId, company_name: u.companyName || u.clientId, is_active_profile: true }];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── GET /v2/app — главная страница ──────────────────────────────────────
|
|
||||||
router.get('/', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const clId = activeClientId(req);
|
|
||||||
const allCompanies = companies(req);
|
|
||||||
const company = await q.getOrCreateCompany(clId, clId);
|
|
||||||
|
|
||||||
const includeDeleted = req.query.deleted === '1';
|
|
||||||
const entries = await q.listEntries(company.id, includeDeleted);
|
|
||||||
const limit = await q.getLimit(company);
|
|
||||||
const used = entries.filter(e => !e.deleted_at).length;
|
const used = entries.filter(e => !e.deleted_at).length;
|
||||||
|
return { entries, used, limit };
|
||||||
const switchTo = req.query.switchTo;
|
|
||||||
if (switchTo && allCompanies.find(c => c.client_id === switchTo)) {
|
|
||||||
req.session.user.activeClientId = switchTo;
|
|
||||||
return res.redirect('/v2/app');
|
|
||||||
}
|
|
||||||
|
|
||||||
res.send(renderApp({ entries, company, limit, used, allCompanies, clId, user: user(req), includeDeleted }));
|
|
||||||
} catch (e) {
|
|
||||||
res.status(500).send('<h2>Ошибка</h2><pre>' + e.message + '</pre><a href="/v2/app">Назад</a>');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── POST /v2/app/add ────────────────────────────────────────────────────
|
|
||||||
router.post('/add', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const clId = activeClientId(req);
|
|
||||||
const company = await q.getOrCreateCompany(clId, clId);
|
|
||||||
const u = user(req);
|
|
||||||
|
|
||||||
const result = await q.createEntry(
|
|
||||||
company.id,
|
|
||||||
req.body.cidr || '',
|
|
||||||
req.body.comment || '',
|
|
||||||
u.email,
|
|
||||||
u.originalUserEmail || null // impersonated_by
|
|
||||||
);
|
|
||||||
|
|
||||||
const msg = result.wasNormalized ? 'Добавлено (адрес нормализован)' : 'Добавлено';
|
|
||||||
res.redirect('/v2/app?msg=' + encodeURIComponent(msg));
|
|
||||||
} catch (e) {
|
|
||||||
res.redirect('/v2/app?error=' + encodeURIComponent(e.message));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── POST /v2/app/edit/:id ───────────────────────────────────────────────
|
|
||||||
router.post('/edit/:id', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const clId = activeClientId(req);
|
|
||||||
const company = await q.getOrCreateCompany(clId, clId);
|
|
||||||
const u = user(req);
|
|
||||||
|
|
||||||
const result = await q.updateEntry(
|
|
||||||
parseInt(req.params.id),
|
|
||||||
company.id,
|
|
||||||
req.body.cidr || '',
|
|
||||||
req.body.comment || '',
|
|
||||||
u.email,
|
|
||||||
u.originalUserEmail || null
|
|
||||||
);
|
|
||||||
|
|
||||||
const msg = result.wasNormalized ? 'Изменено (адрес нормализован)' : 'Изменено';
|
|
||||||
res.redirect('/v2/app?msg=' + encodeURIComponent(msg));
|
|
||||||
} catch (e) {
|
|
||||||
res.redirect('/v2/app?error=' + encodeURIComponent(e.message));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── POST /v2/app/delete/:id ─────────────────────────────────────────────
|
|
||||||
router.post('/delete/:id', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const clId = activeClientId(req);
|
|
||||||
const company = await q.getOrCreateCompany(clId, clId);
|
|
||||||
const u = user(req);
|
|
||||||
|
|
||||||
await q.deleteEntry(
|
|
||||||
parseInt(req.params.id),
|
|
||||||
company.id,
|
|
||||||
u.email,
|
|
||||||
u.originalUserEmail || null
|
|
||||||
);
|
|
||||||
|
|
||||||
res.redirect('/v2/app?msg=' + encodeURIComponent('Удалено'));
|
|
||||||
} catch (e) {
|
|
||||||
res.redirect('/v2/app?error=' + encodeURIComponent(e.message));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return router;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── HTML-рендеринг (временный, без EJS) ─────────────────────────────────────
|
async function add(clientId, cidr, comment, email, impBy) {
|
||||||
|
const co = await resolve(clientId);
|
||||||
function renderApp({ entries, company, limit, used, allCompanies, clId, user, includeDeleted }) {
|
return q.createEntry(co.id, cidr, comment, email, impBy);
|
||||||
const msg = (s) => s ? `<div style="background:#4caf50;color:#fff;padding:8px;margin:8px 0">${s}</div>` : '';
|
|
||||||
const err = (s) => s ? `<div style="background:#f44336;color:#fff;padding:8px;margin:8px 0">${s}</div>` : '';
|
|
||||||
|
|
||||||
const companyOptions = allCompanies.map(c =>
|
|
||||||
`<option value="${c.client_id}" ${c.client_id === clId ? 'selected' : ''}>${c.company_name || c.client_id} (${c.client_id})</option>`
|
|
||||||
).join('');
|
|
||||||
|
|
||||||
const rows = entries.map(e => {
|
|
||||||
const del = e.deleted_at;
|
|
||||||
const style = del ? 'text-decoration:line-through;opacity:0.5' : '';
|
|
||||||
const actions = del ? '<span style="color:#888">удалено</span>' : `
|
|
||||||
<form method="POST" action="/v2/app/edit/${e.id}" style="display:inline">
|
|
||||||
<input name="cidr" value="${e.value_cidr}" size="18">
|
|
||||||
<input name="comment" value="${e.comment || ''}" size="20">
|
|
||||||
<button>изменить</button>
|
|
||||||
</form>
|
|
||||||
<form method="POST" action="/v2/app/delete/${e.id}" style="display:inline">
|
|
||||||
<button onclick="return confirm('Удалить?')">удалить</button>
|
|
||||||
</form>`;
|
|
||||||
return `<tr style="${style}">
|
|
||||||
<td>${e.value_cidr}</td><td>${e.comment || ''}</td><td>${e.created_by}</td>
|
|
||||||
<td>${new Date(e.created_at).toLocaleString('ru')}</td>
|
|
||||||
<td>${actions}</td>
|
|
||||||
</tr>`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
return `<!DOCTYPE html>
|
|
||||||
<html><head><meta charset="utf-8"><title>V2 — IP WhiteList</title>
|
|
||||||
<style>
|
|
||||||
body { font-family: sans-serif; max-width:960px; margin:20px auto; color:#222; }
|
|
||||||
table { width:100%; border-collapse:collapse; margin:10px 0; }
|
|
||||||
th,td { border:1px solid #ccc; padding:6px; text-align:left; }
|
|
||||||
th { background:#f5f5f5; }
|
|
||||||
.bar { display:flex; justify-content:space-between; align-items:center; background:#eee; padding:10px; margin-bottom:10px; }
|
|
||||||
button,input,select { padding:4px 8px; }
|
|
||||||
.add-form { background:#f0f8ff; padding:10px; margin:10px 0; }
|
|
||||||
a { color:#1976d2; }
|
|
||||||
</style></head><body>
|
|
||||||
|
|
||||||
<div class="bar">
|
|
||||||
<span><strong>V2 IP WhiteList</strong> <small>v${config.version}</small></span>
|
|
||||||
<span>${user.email} | ${user.fio || ''} ${user.isAdmin ? '| ADMIN' : ''}</span>
|
|
||||||
<span>
|
|
||||||
<form method="GET" action="/v2/app" style="display:inline">
|
|
||||||
<select name="switchTo" onchange="this.form.submit()">
|
|
||||||
${companyOptions}
|
|
||||||
</select>
|
|
||||||
</form>
|
|
||||||
<a href="/v2/logout">Выйти</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${msg('')}${err('')}
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const params = new URLSearchParams(location.search);
|
|
||||||
const m = params.get('msg'), e = params.get('error');
|
|
||||||
if (m) document.querySelector('body').insertAdjacentHTML('afterbegin', '<div style="background:#4caf50;color:#fff;padding:8px">'+m+'</div>');
|
|
||||||
if (e) document.querySelector('body').insertAdjacentHTML('afterbegin', '<div style="background:#f44336;color:#fff;padding:8px">'+e+'</div>');
|
|
||||||
if (m||e) history.replaceState(null,'','/v2/app');
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<p>Записей: <strong>${used}</strong> из ${limit}</p>
|
|
||||||
|
|
||||||
<div class="add-form">
|
|
||||||
<form method="POST" action="/v2/app/add">
|
|
||||||
<input name="cidr" placeholder="x.x.x.x/xx" size="18" required>
|
|
||||||
<input name="comment" placeholder="комментарий" size="30">
|
|
||||||
<button>Добавить</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr><th>CIDR</th><th>Комментарий</th><th>Кто</th><th>Когда</th><th></th></tr>
|
|
||||||
${rows || '<tr><td colspan="5">Нет записей</td></tr>'}
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<p><a href="/v2/app?deleted=${includeDeleted ? '0' : '1'}">${includeDeleted ? 'Скрыть удалённые' : 'Показать удалённые'}</a></p>
|
|
||||||
|
|
||||||
</body></html>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { createCrudRouter };
|
async function edit(entryId, clientId, cidr, comment, email, impBy) {
|
||||||
|
const co = await resolve(clientId);
|
||||||
|
return q.updateEntry(entryId, co.id, cidr, comment, email, impBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(entryId, clientId, email, impBy) {
|
||||||
|
const co = await resolve(clientId);
|
||||||
|
return q.deleteEntry(entryId, co.id, email, impBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { list, add, edit, remove };
|
||||||
|
|||||||
+10
-12
@@ -4,6 +4,7 @@
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const crud = require('../crud');
|
||||||
const q = require('../db/queries');
|
const q = require('../db/queries');
|
||||||
const { pool } = require('../db');
|
const { pool } = require('../db');
|
||||||
|
|
||||||
@@ -30,25 +31,25 @@ function createTestRouter() {
|
|||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
// ── LIST ──
|
// ── LIST ──
|
||||||
case 'list':
|
case 'list': {
|
||||||
const entries = await q.listEntries(company.id, req.query.deleted === '1');
|
const { entries, used, limit } = await crud.list(clientId, req.query.deleted === '1');
|
||||||
return res.json({ ok: true, entries, count: entries.length, company: { id: company.id, client_id: company.client_id } });
|
return res.json({ ok: true, entries, count: entries.length, used, limit }); }
|
||||||
|
|
||||||
// ── ADD ──
|
// ── ADD ──
|
||||||
case 'add': {
|
case 'add': {
|
||||||
const r = await q.createEntry(company.id, req.query.cidr || '', req.query.comment || '', email, impBy);
|
const r = await crud.add(clientId, req.query.cidr || '', req.query.comment || '', email, impBy);
|
||||||
return res.json({ ok: true, entry: r.entry, wasNormalized: r.wasNormalized });
|
return res.json({ ok: true, entry: r.entry, wasNormalized: r.wasNormalized });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── EDIT ──
|
// ── EDIT ──
|
||||||
case 'edit': {
|
case 'edit': {
|
||||||
const r = await q.updateEntry(parseInt(req.query.id), company.id, req.query.cidr || '', req.query.comment || '', email, impBy);
|
const r = await crud.edit(parseInt(req.query.id), clientId, req.query.cidr || '', req.query.comment || '', email, impBy);
|
||||||
return res.json({ ok: true, entry: r.entry, wasNormalized: r.wasNormalized });
|
return res.json({ ok: true, entry: r.entry, wasNormalized: r.wasNormalized });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── DELETE ──
|
// ── DELETE ──
|
||||||
case 'delete':
|
case 'delete':
|
||||||
await q.deleteEntry(parseInt(req.query.id), company.id, email, impBy);
|
await crud.remove(parseInt(req.query.id), clientId, email, impBy);
|
||||||
return res.json({ ok: true });
|
return res.json({ ok: true });
|
||||||
|
|
||||||
// ── AUDIT ──
|
// ── AUDIT ──
|
||||||
@@ -112,10 +113,9 @@ function createTestRouter() {
|
|||||||
const impBy = idx === 3 ? 'real@nubes.ru' : null;
|
const impBy = idx === 3 ? 'real@nubes.ru' : null;
|
||||||
const prefix = cidPrefix[idx];
|
const prefix = cidPrefix[idx];
|
||||||
tasks.push((async () => {
|
tasks.push((async () => {
|
||||||
const co = await q.getOrCreateCompany(clientId, clientId);
|
|
||||||
for (let j = 0; j < perCompany; j++) {
|
for (let j = 0; j < perCompany; j++) {
|
||||||
try {
|
try {
|
||||||
await q.createEntry(co.id, prefix + '.0.' + j + '.0/24', 'chaos', email, impBy);
|
await crud.add(clientId, prefix + '.0.' + j + '.0/24', 'chaos', email, impBy);
|
||||||
ok++;
|
ok++;
|
||||||
} catch (e) { errors++; }
|
} catch (e) { errors++; }
|
||||||
}
|
}
|
||||||
@@ -127,12 +127,10 @@ function createTestRouter() {
|
|||||||
const checks = [];
|
const checks = [];
|
||||||
let totalEntries = 0, totalAudit = 0, anyDupes = false, anyOverLimit = false;
|
let totalEntries = 0, totalAudit = 0, anyDupes = false, anyOverLimit = false;
|
||||||
for (const clientId of cids) {
|
for (const clientId of cids) {
|
||||||
const co = await q.getOrCreateCompany(clientId, clientId);
|
const { entries, limit } = await crud.list(clientId);
|
||||||
const entries = await q.listEntries(co.id);
|
|
||||||
const cidrs = entries.map(e => e.value_cidr);
|
const cidrs = entries.map(e => e.value_cidr);
|
||||||
const dupes = cidrs.length !== new Set(cidrs).size;
|
const dupes = cidrs.length !== new Set(cidrs).size;
|
||||||
const limit = await q.getLimit(co);
|
const audit = await q.getAudit((await q.getOrCreateCompany(clientId, clientId)).id);
|
||||||
const audit = await q.getAudit(co.id);
|
|
||||||
totalEntries += entries.length;
|
totalEntries += entries.length;
|
||||||
totalAudit += audit.length;
|
totalAudit += audit.length;
|
||||||
if (dupes) anyDupes = true;
|
if (dupes) anyDupes = true;
|
||||||
|
|||||||
+11
-18
@@ -1,11 +1,13 @@
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
// V2 — пользовательский CRUD
|
// V2 — пользовательский CRUD (фронтенд, только Express)
|
||||||
// Вход: req.v2_* из resolveContext (email, clientId, allClientIds, profiles, ...)
|
//
|
||||||
|
// Вход: req.clientId, req.email, req.impersonatedBy из resolveContext.
|
||||||
// Всегда показывает выбор компании (даже если одна).
|
// Всегда показывает выбор компании (даже если одна).
|
||||||
|
// НЕ работает с БД напрямую — только через crud API-слой.
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const q = require('../db/queries');
|
const crud = require('../crud');
|
||||||
|
|
||||||
function createUserRouter() {
|
function createUserRouter() {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -18,11 +20,8 @@ function createUserRouter() {
|
|||||||
? req.profiles
|
? req.profiles
|
||||||
: [{ client_id: clId, company_name: req.companyName || clId }];
|
: [{ client_id: clId, company_name: req.companyName || clId }];
|
||||||
|
|
||||||
const company = await q.getOrCreateCompany(clId, allCompanies.find(c => c.client_id === clId)?.company_name || clId);
|
|
||||||
const includeDeleted = req.query.deleted === '1';
|
const includeDeleted = req.query.deleted === '1';
|
||||||
const entries = await q.listEntries(company.id, includeDeleted);
|
const { entries, used, limit } = await crud.list(clId, includeDeleted);
|
||||||
const limit = await q.getLimit(company);
|
|
||||||
const used = entries.filter(e => !e.deleted_at).length;
|
|
||||||
|
|
||||||
// Переключение компании
|
// Переключение компании
|
||||||
if (req.query.switchTo && allCompanies.find(c => c.client_id === req.query.switchTo)) {
|
if (req.query.switchTo && allCompanies.find(c => c.client_id === req.query.switchTo)) {
|
||||||
@@ -30,7 +29,7 @@ function createUserRouter() {
|
|||||||
return res.redirect('/v2/app');
|
return res.redirect('/v2/app');
|
||||||
}
|
}
|
||||||
|
|
||||||
res.send(renderPage({ entries, company, limit, used, allCompanies, clId, user: req.user, includeDeleted }));
|
res.send(renderPage({ entries, limit, used, allCompanies, clId, user: req.user, includeDeleted }));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send('<h2>Ошибка</h2><pre>' + e.message + '</pre><a href="/v2/app">Назад</a>');
|
res.status(500).send('<h2>Ошибка</h2><pre>' + e.message + '</pre><a href="/v2/app">Назад</a>');
|
||||||
}
|
}
|
||||||
@@ -39,9 +38,7 @@ function createUserRouter() {
|
|||||||
// ── POST /add ───────────────────────────────────────────────────────────
|
// ── POST /add ───────────────────────────────────────────────────────────
|
||||||
router.post('/add', async (req, res) => {
|
router.post('/add', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const clId = req.clientId;
|
const result = await crud.add(req.clientId, req.body.cidr || '', req.body.comment || '', req.email, req.impersonatedBy);
|
||||||
const company = await q.getOrCreateCompany(clId, clId);
|
|
||||||
const result = await q.createEntry(company.id, req.body.cidr || '', req.body.comment || '', req.email, req.impersonatedBy);
|
|
||||||
const msg = result.wasNormalized ? 'Добавлено (адрес нормализован)' : 'Добавлено';
|
const msg = result.wasNormalized ? 'Добавлено (адрес нормализован)' : 'Добавлено';
|
||||||
res.redirect('/v2/app?msg=' + encodeURIComponent(msg));
|
res.redirect('/v2/app?msg=' + encodeURIComponent(msg));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -52,9 +49,7 @@ function createUserRouter() {
|
|||||||
// ── POST /edit/:id ──────────────────────────────────────────────────────
|
// ── POST /edit/:id ──────────────────────────────────────────────────────
|
||||||
router.post('/edit/:id', async (req, res) => {
|
router.post('/edit/:id', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const clId = req.clientId;
|
const result = await crud.edit(parseInt(req.params.id), req.clientId, req.body.cidr || '', req.body.comment || '', req.email, req.impersonatedBy);
|
||||||
const company = await q.getOrCreateCompany(clId, clId);
|
|
||||||
const result = await q.updateEntry(parseInt(req.params.id), company.id, req.body.cidr || '', req.body.comment || '', req.email, req.impersonatedBy);
|
|
||||||
const msg = result.wasNormalized ? 'Изменено (адрес нормализован)' : 'Изменено';
|
const msg = result.wasNormalized ? 'Изменено (адрес нормализован)' : 'Изменено';
|
||||||
res.redirect('/v2/app?msg=' + encodeURIComponent(msg));
|
res.redirect('/v2/app?msg=' + encodeURIComponent(msg));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -65,9 +60,7 @@ function createUserRouter() {
|
|||||||
// ── POST /delete/:id ────────────────────────────────────────────────────
|
// ── POST /delete/:id ────────────────────────────────────────────────────
|
||||||
router.post('/delete/:id', async (req, res) => {
|
router.post('/delete/:id', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const clId = req.clientId;
|
await crud.remove(parseInt(req.params.id), req.clientId, req.email, req.impersonatedBy);
|
||||||
const company = await q.getOrCreateCompany(clId, clId);
|
|
||||||
await q.deleteEntry(parseInt(req.params.id), company.id, req.email, req.impersonatedBy);
|
|
||||||
res.redirect('/v2/app?msg=Удалено');
|
res.redirect('/v2/app?msg=Удалено');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.redirect('/v2/app?error=' + encodeURIComponent(e.message));
|
res.redirect('/v2/app?error=' + encodeURIComponent(e.message));
|
||||||
@@ -79,7 +72,7 @@ function createUserRouter() {
|
|||||||
|
|
||||||
// ── HTML-рендеринг (временный, будет заменён на EJS) ─────────────────────
|
// ── HTML-рендеринг (временный, будет заменён на EJS) ─────────────────────
|
||||||
|
|
||||||
function renderPage({ entries, company, limit, used, allCompanies, clId, user, includeDeleted }) {
|
function renderPage({ entries, limit, used, allCompanies, clId, user, includeDeleted }) {
|
||||||
const companyOptions = allCompanies.map(c =>
|
const companyOptions = allCompanies.map(c =>
|
||||||
`<option value="${c.client_id}" ${c.client_id === clId ? 'selected' : ''}>${c.company_name || c.client_id} (${c.client_id})</option>`
|
`<option value="${c.client_id}" ${c.client_id === clId ? 'selected' : ''}>${c.company_name || c.client_id} (${c.client_id})</option>`
|
||||||
).join('');
|
).join('');
|
||||||
|
|||||||
Reference in New Issue
Block a user