# Итог: раздел интерфейса «Пользователи» (изменение 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 → вход в другом окне → отключение (вход и текущая сессия отбиваются) → сброс пароля → вход с новым паролем → «Сменить пароль» в шапке.