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