006 Пользователи: API /users (CRUD, смена своего пароля), раздел UI «Пользователи»,
события журнала user.*, защита от отключения/удаления себя и последнего админа.
007 Исправление удаления организации: VRF удаляются явным DELETE до организации
(без relationship() порядок DELETE не гарантирован → ложный 409).
008 Журнал фиксирует отказы в удалении (<entity>.delete_blocked) со списком
мешающих объектов в «Данных»: организация, VRF, тип устройства, префикс, пользователь.
009 Выбор строк чекбоксами и групповые операции в UI (удаление, смена типа устройств,
статус префиксов и адресов, доступ пользователей); цикл запросов из UI, итог и список отказов.
010 Автовыделение следующего вложенного префикса: POST/GET /prefixes/{id}/subnets/next,
первый свободный выровненный блок; пункт «Добавить вложенный (авто)» в меню префикса.
Тесты: 14 (добавлены сценарии для 006, 007/008, 010); исправлена нестабильность
тестов журнала (IPv6-группы с ведущими нулями нормализуются PostgreSQL).
Документация: README.md, docs/changes/006-010 (планы и итоги).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
49 lines
5.8 KiB
Markdown
49 lines
5.8 KiB
Markdown
# Журнал: события отклонённого удаления (изменение 008)
|
||
|
||
## Context
|
||
Когда удаление запрещено бизнес-правилом (например, у организации есть префиксы/устройства/операторы), API отвечает 409 через `raise HTTPException`,
|
||
а запись в журнал не пишется: `audit()` вызывается только на успешном пути, сессия закрывается без commit (`app/db.py:get_db`).
|
||
Нужно фиксировать такие попытки как предупреждения и показывать в блоке «Данные» окна записи журнала, какие именно связанные сущности мешают удалению.
|
||
|
||
## Решения
|
||
- **Тип события:** `<entity>.delete_blocked` (например `organization.delete_blocked`) — вписывается в существующую схему `entity_type` + `action`,
|
||
фильтры и facets журнала подхватывают его без изменений (`app/api/v1/journal.py` разбирает `event_type` по точке).
|
||
- **Охват:** все отказы в удалении по бизнес-правилам (409): организация, VRF, тип устройства (в т.ч. «по умолчанию»), префикс с адресами,
|
||
пользователь (свой аккаунт / последний администратор). Прочие 409/422 (дубли, валидация) не журналируются.
|
||
- **Содержимое «Данных»** — уже существующее поле `audit_log.diff` (JSONB), миграция БД не нужна:
|
||
```json
|
||
{"reason": "у организации есть префиксы, устройства или операторы",
|
||
"blocked_by": {"prefixes": {"total": 13, "items": ["10.0.0.0/8 (default)", "…"]},
|
||
"devices": {"total": 2, "items": ["db-master.internal", "…"]},
|
||
"isps": {"total": 1, "items": ["Ростелеком"]}}}
|
||
```
|
||
В списке не более 20 элементов на группу, `total` — полное число. Пустые группы не выводятся. Для отказов без связанных объектов
|
||
(свой аккаунт, тип по умолчанию, последний админ) — только `reason`.
|
||
- **Сообщение:** «Организация ООО «X»: удаление отклонено — есть связанные объекты (префиксы: 13, устройства: 2, операторы: 1)».
|
||
|
||
## Реализация
|
||
1. `app/services.py`: хелпер `refuse_delete(db, user, entity_type, entity, label, reason, blocked_by=None)` — пишет `audit(..., "delete_blocked", label, diff, message=...)`,
|
||
делает `commit(db)` (в транзакции только запись журнала, других изменений к этому моменту нет) и поднимает `HTTPException(409, reason_text)`.
|
||
Текст ответа API остаётся прежним. Хелпер `_blockers(db, stmt, label_col, limit=20)` — `{"total", "items"}` по запросу (переиспользуем `count()`).
|
||
2. Вызовы вместо прямых `raise HTTPException(409, …)` в:
|
||
- `app/api/v1/refs.py`: `delete_org` (префиксы: `prefix (vrf)`, устройства: `name`, операторы: `name`), `delete_vrf` (префиксы), `delete_type` (устройства + случай `is_default`);
|
||
- `app/api/v1/prefixes.py`: `delete_prefix` (адреса, до 20 шт.);
|
||
- `app/api/v1/users.py`: `delete_user` (два отказа: свой аккаунт, единственный активный админ).
|
||
Для организации проверка «занято» переписывается с `or`-цепочки на три подсчёта, чтобы собрать все группы сразу.
|
||
3. `web/app.js`: `eventBadge` — цвет `amber` для `delete_blocked`; в `actionBadge` обзора — `delete_blocked: ["amber", "отклонено"]`.
|
||
Блок «Данные» (`entryDialog`) уже выводит `diff` как JSON — отдельная вёрстка не нужна.
|
||
4. `_VERBS` в `make_message` не трогаем: сообщение передаётся явно.
|
||
|
||
## Артефакты (правила проекта)
|
||
`docs/changes/008-blocked-delete-audit/PLAN.md` (копия этого плана) и `SUMMARY.md`; `README.md` — в раздел «Журнал»: событие `*.delete_blocked`, состав «Данных».
|
||
|
||
## Тест (минимум)
|
||
Расширить `test_delete_organization` в `tests/test_api.py`: после 409 на организации с префиксом найти в `/audit?event_type=organization.delete_blocked&q=<имя>`
|
||
запись, проверить `diff.blocked_by.prefixes.total == 1` и наличие CIDR в `items`.
|
||
|
||
## Проверка
|
||
1. `docker compose -p ipam_control_006 up -d --build app` (БД и прежняя поставка не затрагиваются); `venv/bin/python -m pytest -q`.
|
||
2. В UI (http://192.168.5.9:8088): «Организации» → «Удалить» у «ООО «Технологии связи»» → 409; в «Журнале» появилась запись `organization.delete_blocked`
|
||
(жёлтый бейдж), в окне записи блок «Данные» показывает списки префиксов/устройств/операторов; фильтр по типу события находит её.
|
||
3. Отказ на удаление VRF с префиксами и своего аккаунта — аналогичные записи.
|