feat(users): add transactional employee deletion and token revocation
All checks were successful
State Corp Backend CI/CD / Quality gate (push) Successful in 2m42s
State Corp Backend CI/CD / Build linux/amd64 images once (push) Successful in 5m29s
State Corp Backend CI/CD / Refresh and release internal main (push) Has been skipped
State Corp Backend CI/CD / Release customer main (push) Has been skipped
State Corp Backend CI/CD / Release dev (push) Successful in 36s

This commit is contained in:
Aleksandr Meshchryakov
2026-09-13 22:55:31 +02:00
parent ef087d5559
commit e99616ed6d
9 changed files with 971 additions and 69 deletions

View File

@@ -0,0 +1,98 @@
# Полное удаление сотрудника
Администратор может полностью удалить аккаунт сотрудника через существующий
маршрут управления пользователями:
```http
DELETE /api/v1/users/admin/users/{user_id}/
Authorization: Bearer <access-token>
```
Тело запроса не требуется. Успех — `204 No Content` без JSON-тела. Сотрудник
исчезает из списка, поиска и карточки; повторный DELETE возвращает 404.
Деактивация и обратная активация остаются отдельными операциями.
## Права и ошибки
Право удаления имеет существующий активный пользователь с `is_staff=True`,
как и для остальных административных endpoint'ов. Проверка повторяется внутри
транзакции после получения блокировок: ранее аутентифицированный, но уже удалённый
или лишённый прав администратор не может завершить изменение.
| HTTP | Код ошибки | Значение |
|---|---|---|
| 400 | `self_delete_forbidden` | Нельзя удалить самого себя |
| 401 | Стандартная ошибка аутентификации | Нет действующего access token |
| 403 | Стандартная ошибка прав / `permission_denied` | Требуется активный администратор |
| 404 | `not_found` | Сотрудник отсутствует, в том числе после удаления |
| 409 | `last_active_admin` | Нельзя оставить систему без активного администратора |
| 409 | `user_delete_conflict` | Защищённая связь препятствует удалению |
Ошибки используют стандартный envelope проекта: `success: false`, `data: null`,
`errors: [{code, message}]`, `meta`. Запрет удаления себя имеет приоритет над
проверкой последнего администратора. Существующие связи не блокируют удаление;
409 для защищённой связи сохраняет безопасное поведение при расширении модели.
DELETE, создание аккаунта администратором, смена роли/активности и соответствующие
операции Django admin используют одни блокировки пользователей. Пакетное действие
Django admin атомарно: если
выбран сам исполнитель, вся операция отклоняется.
Сохранение собственных учётных данных и смена пароля также блокируют актуальную
строку User, чтобы начавшийся до DELETE запрос не создал аккаунт заново.
## Данные после удаления
- Физически удаляются User, Profile, связи с группами/permissions, записи
OutstandingToken и связанные BlacklistedToken. Сами справочники прав остаются.
- Отчёты ReportUpload, загрузки реестров RegisterUpload, ExchangePackageImport
сохраняются; их ссылки на автора обнуляются штатным SET_NULL. Сохраняются
загруженные файлы, предметные данные и цепочки импортов.
- BackgroundJob сохраняются, `user_id` очищается явно, поскольку это integer,
а не внешний ключ. Выполняющиеся задачи не отменяются.
- Аватар удаляется из storage после commit, если его не использует другой
профиль. При ошибке storage удаление аккаунта остаётся успешным, ошибка
регистрируется в серверном журнале для последующей очистки файла.
- Технический Django admin LogEntry сохраняет штатный CASCADE для записей,
исполнителем которых был удалённый пользователь; предметные журналы выше
не удаляются. Содержимое исторических документов не переписывается.
- Старый access перестаёт проходить JWTAuthentication. Refresh endpoint
дополнительно проверяет существование и активность пользователя и возвращает
401 для удалённого/неактивного аккаунта. Повторный refresh активного пользователя
поддерживается как раньше. Записей восстановления пароля в текущей модели нет.
Изменений схемы БД и миграций не требуется. Откат версии приложения не
восстанавливает уже удалённые аккаунты.
## Передача frontend-разработчику (Глебу)
1. Добавить отдельное действие «Удалить» в меню сотрудника; для текущего
пользователя скрыть или отключить его.
2. Перед запросом показать имя выбранного сотрудника и подтверждение полного
необратимого удаления аккаунта. Сообщить, что отчёты и история импортов останутся.
3. Вызвать DELETE; на время запроса блокировать повторное действие. После 204
закрыть диалог и обновить список/счётчик; если последняя строка страницы удалена,
перейти на предыдущую существующую страницу.
4. Обработать стандартный error envelope, включая 400/403/404/409. При 404 обновить
список. Не пытаться читать JSON из успешного ответа 204.
5. Обновить клиент из OpenAPI и проверить удаление обычного, неактивного и другого
административного аккаунта, запрет удаления себя и потерю прав во время запроса.
## Проверка backend
```bash
PYTHONPATH=src uv run --no-sync pytest tests/apps/user
```
Восемь тестов `test_management_concurrency.py` пропускаются SQLite и требуют
PostgreSQL. Для них использовать отдельную пустую тестовую БД и явно задавать все
`TEST_POSTGRES_HOST`, `TEST_POSTGRES_PORT`, `TEST_POSTGRES_USER`,
`TEST_POSTGRES_PASSWORD`, `TEST_POSTGRES_DB`, чтобы исключить fallback к рабочей БД:
```bash
scripts/run-tests-prod.sh ../tests/apps/user
```
Регрессии обмена и подготовленных выгрузок находятся в
`tests/apps/exchange/test_api.py`, `tests/apps/external_data/test_source_record_export.py`
и `tests/apps/external_data/test_export_tasks.py`. Изменений upload/export API эта
доработка не вносит.