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

@@ -1,13 +1,21 @@
"""Admin configuration for user app."""
from contextlib import suppress
from functools import wraps
from apps.core.exceptions import BaseAPIException
from apps.core.models import BackgroundJob
from apps.user.management import lock_managed_users, validate_access_change
from apps.user.models import Profile, User
from django.contrib import admin
from apps.user.services import UserService
from django.contrib import admin, messages
from django.contrib.admin.sites import NotRegistered
from django.contrib.admin.utils import unquote
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import Group
from django.db import transaction
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
@@ -47,6 +55,20 @@ def _unregister(model) -> None:
_unregister(User)
def _management_errors(view):
"""Present guarded account-change failures through Django admin messages."""
@wraps(view)
def wrapped(self, request, *args, **kwargs):
try:
return view(self, request, *args, **kwargs)
except BaseAPIException as exc:
self.message_user(request, exc.message, level=messages.ERROR)
return HttpResponseRedirect(reverse("admin:user_user_changelist"))
return wrapped
class ProfileInline(admin.StackedInline):
"""Inline для профиля пользователя."""
@@ -121,6 +143,50 @@ class UserAdmin(BaseUserAdmin):
readonly_fields = ["created_at", "updated_at", "last_login", "date_joined"]
actions = ["verify_users", "unverify_users", "activate_users", "deactivate_users"]
@_management_errors
def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
return super().changeform_view(request, object_id, form_url, extra_context)
@_management_errors
def delete_view(self, request, object_id, extra_context=None):
return super().delete_view(request, object_id, extra_context)
@_management_errors
def user_change_password(self, request, id, form_url=""):
user = self.get_object(request, unquote(id))
if request.method == "POST" and user is not None:
with lock_managed_users(actor_id=request.user.pk, user_ids=[user.pk]):
return super().user_change_password(request, id, form_url)
return super().user_change_password(request, id, form_url)
@_management_errors
def changelist_view(self, request, extra_context=None):
# delete_selected writes LogEntry rows before delete_queryset. Roll those
# back too if the complete selection fails an account-management guard.
with transaction.atomic():
return super().changelist_view(request, extra_context)
def save_model(self, request, obj, form, change):
with lock_managed_users(
actor_id=request.user.pk, user_ids=[obj.pk] if change else []
) as users:
if change:
validate_access_change(
users[obj.pk],
actor_id=request.user.pk,
is_active=obj.is_active,
is_staff=obj.is_staff,
)
super().save_model(request, obj, form, change)
def delete_model(self, request, obj):
UserService.delete_user(obj.pk, actor_id=request.user.pk)
def delete_queryset(self, request, queryset):
UserService.delete_users(
queryset.values_list("pk", flat=True), actor_id=request.user.pk
)
def is_verified_badge(self, obj):
if obj.is_verified:
return format_html(
@@ -157,12 +223,20 @@ class UserAdmin(BaseUserAdmin):
@admin.action(description="Активировать пользователей")
def activate_users(self, request, queryset):
updated = queryset.update(is_active=True)
updated = UserService.set_users_active(
queryset.values_list("pk", flat=True),
actor_id=request.user.pk,
is_active=True,
)
self.message_user(request, f"Активировано {updated} пользователей")
@admin.action(description="Деактивировать пользователей")
def deactivate_users(self, request, queryset):
updated = queryset.update(is_active=False)
updated = UserService.set_users_active(
queryset.values_list("pk", flat=True),
actor_id=request.user.pk,
is_active=False,
)
self.message_user(request, f"Деактивировано {updated} пользователей")