Задачи 006-010: пользователи, исправление удаления, журнал отказов, групповые операции, автовыделение префиксов

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>
This commit is contained in:
ayurishchevandClaude Sonnet 5 committed 2026-09-26 13:25:33 +03:00
1 parent a846d30872
commit cd09ef0805
23 files changed
+988 -49

No files matched your search

+52
View File
@@ -0,0 +1,52 @@
# Раздел интерфейса «Пользователи» (изменение 006)
## Context
Роли `admin` (запись) и `viewer` (чтение) заявлены в README и в UI (журнал: настройки и очистка скрыты для не-админа),
но назначить роль негде: таблица `users` заполняется только записью администратора при первом старте (`seed()` в `app/main.py`),
эндпоинтов управления пользователями нет, сменить пароль — тоже нельзя. Нужен раздел интерфейса, где администратор
заводит учётные записи, меняет им роль/пароль и отключает доступ, а любой пользователь может сменить свой пароль.
## Модель (без изменения схемы БД)
Таблица `users` (миграция 0001) уже содержит всё необходимое: `id`, `username` (уникальный), `password_hash` (argon2),
`role` (`user_role`: admin/viewer), `is_active`. Миграция не требуется. Пароли не хранятся и не пишутся в журнал в открытом виде.
## API (`/api/v1/users`, все — под авторизацией)
| Метод | Кто | Назначение |
|---|---|---|
| `GET /users` | admin | список: логин (поиск `q`), роль, признак активности, `limit`/`offset` |
| `POST /users` | admin | создание: `username`, `password` (≥ 8), `role`, `is_active` |
| `PATCH /users/{id}` | admin | роль, `is_active`, необязательный новый `password` |
| `DELETE /users/{id}` | admin | удаление учётной записи |
| `POST /users/me/password` | любой | смена **своего** пароля с подтверждением текущего |
Правила (иначе 409/422, состояние не меняется):
- логин: 3–100 символов, `[A-Za-z0-9._-]`, уникален; зарезервированные `system` и `anonymous` запрещены —
журнал различает служебные события по этим именам (`actor_of` в `app/services.py`);
- нельзя удалить или отключить/понизить **свою** учётную запись (иначе текущая сессия теряет доступ);
- нельзя отключить, понизить или удалить **единственного активного администратора** (система осталась бы без прав записи);
- неверный текущий пароль при смене своего пароля → 403.
Аудит: `user.created`, `user.updated`, `user.password_reset` (пароль администратором и своя смена), `user.deleted`;
в `entity_label` — логин, в `diff` — только изменённые поля (`password` отмечен как «изменён», без значения).
Сущность `user` добавлена в словарь человекочитаемых сообщений (`_NOUNS` в `app/services.py`).
## UI (`web/app.js`)
- Пункт навигации «Пользователи» — только для администратора (`NAV` фильтруется в `shell()`); при ручном вводе `#/users`
не-админ видит заглушку «Раздел доступен только администратору».
- Экран: счётчик записей, поиск по логину, таблица «Логин · Роль · Статус · Действия»; строка кликабельна (паттерн изменения 005),
в меню «⋯» — «Редактировать», «Отключить/Активировать», «Удалить».
- Окно создания/редактирования: логин (при создании), роль, флажок «Доступ разрешён», необязательное поле «Новый пароль»
(пустое — пароль не меняется); окно смены своего пароля — в шапке («Сменить пароль»).
- В журнале: подпись сущности «Пользователь», тип события в фильтре типов появляется автоматически (`/audit/facets`).
## Ограничения (осознанные)
- JWT без списка отзыва: после **смены пароля** уже выданные токены продолжают работать до истечения `JWT_TTL_MINUTES`;
**отключение** учётной записи действует немедленно (токен проверяется по `users.is_active` в `current_user`).
- Логин менять нельзя (он же `sub` в токене) — вместо этого создаётся новая учётная запись.
- Нет саморегистрации, восстановления пароля по e-mail, групп прав и журналирования последнего входа.
## Проверка
- Интеграционный тест `tests/test_users.py` (один сценарий, против контейнеров): создание, дубль логина → 409,
запрет служебного логина → 422, вход новым пользователем, запрет записи для `viewer` → 403,
отключение → вход 401, сброс пароля → вход с новым паролем, смена своего пароля и вход с ним, удаление своей записи → 409.
- Синтаксическая проверка Python и JS; ручной сценарий в браузере (создание, редактирование, отключение, вход отключённым, удаление).
@@ -0,0 +1,49 @@
# Итог: раздел интерфейса «Пользователи» (изменение 006)
## Что сделано
- **API `/api/v1/users`** (`app/api/v1/users.py`, роутер подключён в `app/main.py`):
`GET /users` (поиск по логину, `limit`/`offset`), `POST /users`, `PATCH /users/{id}`, `DELETE /users/{id}` — только `admin`;
`POST /users/me/password` — смена своего пароля любой ролью с подтверждением текущего.
- **Схемы** (`app/schemas.py`): `UserIn`, `UserUpdate`, `PasswordChange`, `UserOut` (`id`, `username`, `role`, `is_active`);
валидатор логина (`Login`, 3–100 символов, `[A-Za-z0-9._-]`) и запрет служебных логинов `system`/`anonymous`.
- **Журнал**: сущность `user` добавлена в словарь сообщений (`app/services.py`), события `user.created`, `user.updated`,
`user.password_reset` («пароль изменён администратором» / «пароль изменён пользователем»), `user.deleted`; в `diff` — только
изменённые поля, для пароля — пометка «изменён» без значения. В UI — подпись «Пользователь» и цвет бейджа для `password_reset`.
- **UI** (`web/app.js`): пункт навигации «Пользователи» (только у администратора), экран со счётчиком, поиском и таблицей
«Логин · Роль · Статус» (строка кликабельна, меню «⋯»: редактировать, отключить/разрешить доступ, удалить), окно создания
и редактирования (логин после создания не редактируется, необязательный «Новый пароль», флажок «Доступ разрешён»),
кнопка «Сменить пароль» в шапке, заглушка для не-администратора при ручном вводе `#/users`.
- **Тест** `tests/test_users.py` — один сквозной сценарий.
- **Документация**: обновлён `README.md`, план и итог в `docs/changes/006-users-management/`.
## Поведение
| Ситуация | Ответ |
|---|---|
| Логин занят (в т. ч. в другом регистре) | 409 «Пользователь с таким логином уже существует» |
| Логин `system`/`anonymous`, короче 3 символов, пароль короче 8 | 422 с указанием поля |
| `viewer` читает `/users` или что-либо изменяет | 403 |
| `PATCH`/`DELETE` своей учётной записи (роль, доступ, удаление) | 409 |
| Отключение или удаление последнего активного администратора | 409 |
| Неверный текущий пароль при смене своего пароля | 403 |
| Новый пароль совпадает с текущим | 422 |
## Действие ролей
- `admin` — полный доступ, включая управление пользователями и запись данных.
- `viewer` — чтение всех справочников, реестра, журнала и обзора; запись (403) и раздел «Пользователи» недоступны.
- **Отключение** учётной записи действует немедленно: токен проверяется по `users.is_active` в `current_user`.
- **Смена пароля** не отзывает уже выданные токены (JWT без списка отзыва) — они действуют до истечения `JWT_TTL_MINUTES`.
## Инварианты
- Схема БД не менялась: таблица `users` (миграция 0001) уже содержит `username`, `password_hash`, `role`, `is_active`.
- Пароли хранятся только в виде argon2-хэша; значение пароля никогда не попадает в журнал.
- Логин не меняется: он же `sub` в JWT; переименование = создание новой записи.
- Логин уникален, в том числе без учёта регистра; служебные акторы журнала зарезервированы.
- Перед каждым изменением проверяется, что в системе останется хотя бы один активный администратор.
## Проверка
- Python: `python3 -m py_compile` по изменённым модулям — без ошибок; автоматическая проверка после правок — успешно.
- Интеграционный тест: `docker compose up -d --build && venv/bin/python -m pytest tests/test_users.py -q`
(создание viewer, запреты 403/409/422, вход, отключение с немедленным отзывом токена, сброс пароля администратором,
смена своего пароля, защита своей записи и последнего администратора, удаление, записи в журнале).
- Ручной сценарий в UI: вход администратором → «Пользователи» → создание viewer → вход в другом окне → отключение
(вход и текущая сессия отбиваются) → сброс пароля → вход с новым паролем → «Сменить пароль» в шапке.