refactor(api): вынос LDAP логики из RegisterAPIView в миксин
ПРОБЛЕМА: - RegisterAPIView содержал ~30 строк LDAP-специфичной логики - DirectoryService, DirectoryUserDTO, обработка исключений - всё в основном методе - Нарушение принципа единственной ответственности - Сложность тестирования и переиспользования РЕШЕНИЕ: Создан api/v1/employees/views/mixins.py с миксинами: 1. LdapUserCreationMixin: - create_ldap_user() - создание пользователя в LDAP через DirectoryService - create_db_user() - создание пользователя напрямую в БД (fallback без LDAP) - Централизованная обработка DirectoryLdapError/DirectoryDbError - Возвращает (Employee, None) при успехе или (None, Response) при ошибке 2. LdapPasswordMixin: - update_ldap_password() - обновление пароля в LDAP - Подготовка для использования в PasswordChangeView и других endpoints ИЗМЕНЕНИЯ: - auth.py: RegisterAPIView использует LdapUserCreationMixin - Блок if ldap_enabled заменён на вызовы self.create_ldap_user() / self.create_db_user() - _helpers.py: удалён _is_ldap_enabled (перемещён в mixins.py) - Уменьшено количество строк в RegisterAPIView: ~190 → ~170 ПРЕИМУЩЕСТВА: - Чёткое разделение: общая логика регистрации vs LDAP-специфика - Переиспользование: миксины можно применить в других views - Тестируемость: можно мокать методы миксина - Читаемость: основной метод фокусируется на бизнес-логике Django check: успешно
This commit is contained in:
parent
ee920dd365
commit
aa70c23f0f
@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.models import Q
|
||||
from employees.utils import _detect_phone_field
|
||||
@ -19,14 +18,6 @@ Employee = get_user_model()
|
||||
PHONE_FIELD = _detect_phone_field()
|
||||
|
||||
|
||||
def _is_ldap_enabled() -> bool:
|
||||
"""Проверяет, включена ли интеграция с LDAP.
|
||||
|
||||
Используется только в RegisterAPIView для создания LDAP пользователей.
|
||||
"""
|
||||
return getattr(settings, "LDAP_ENABLED", False)
|
||||
|
||||
|
||||
class HistoryActionMixin:
|
||||
"""
|
||||
Добавляет GET /{basename}/{pk}/history/
|
||||
|
||||
@ -10,8 +10,6 @@ from django.core.files.base import ContentFile
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from django.utils.crypto import get_random_string
|
||||
from employees.ldap.directory_service import DirectoryService, DirectoryUserDTO
|
||||
from employees.ldap.errors import DirectoryDbError, DirectoryLdapError
|
||||
from employees.models import Position, Skill
|
||||
from employees.utils import _normalize_phone
|
||||
from rest_framework import status
|
||||
@ -22,7 +20,8 @@ from rest_framework.views import APIView
|
||||
|
||||
from ..serializers import (EmailSerializer, EmailVerifySerializer,
|
||||
RegisterSerializer)
|
||||
from ._helpers import Employee, _is_ldap_enabled
|
||||
from ._helpers import Employee
|
||||
from .mixins import LdapUserCreationMixin, _is_ldap_enabled
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -95,8 +94,11 @@ class VerifyEmailAPIView(APIView):
|
||||
return Response({"ok": True, "user_id": user.id}, status=200)
|
||||
|
||||
|
||||
class RegisterAPIView(APIView):
|
||||
"""Регистрация: создаём учётку в LDAP (disabled) с паролем, в БД — set_unusable_password."""
|
||||
class RegisterAPIView(LdapUserCreationMixin, APIView):
|
||||
"""Регистрация: создаём учётку в LDAP (disabled) с паролем, в БД — set_unusable_password.
|
||||
|
||||
Использует LdapUserCreationMixin для вынесения LDAP-специфичной логики.
|
||||
"""
|
||||
|
||||
throttle_scope = "anon"
|
||||
permission_classes = [AllowAny]
|
||||
@ -188,40 +190,27 @@ class RegisterAPIView(APIView):
|
||||
|
||||
if ldap_enabled:
|
||||
# Режим с LDAP: создаём disabled учётку в LDAP + пароль
|
||||
svc = DirectoryService()
|
||||
dto = DirectoryUserDTO(
|
||||
emp, error_response = self.create_ldap_user(
|
||||
first_name=v["first_name"],
|
||||
last_name=v["last_name"],
|
||||
email=email,
|
||||
phone_e164=phone_norm,
|
||||
department_dn=None,
|
||||
group_cns=[],
|
||||
initial_password=password, # пароль идёт только в LDAP
|
||||
password=password,
|
||||
avatar_bytes=avatar_bytes,
|
||||
is_active=False, # disabled до верификации
|
||||
is_active=False, # disabled до верификации email
|
||||
)
|
||||
try:
|
||||
emp = svc.create_user(dto)
|
||||
except DirectoryLdapError as e:
|
||||
return Response(
|
||||
{"ok": False, "error": "ldap_error", "detail": str(e)}, status=502
|
||||
)
|
||||
except DirectoryDbError as e:
|
||||
return Response(
|
||||
{"ok": False, "error": "db_error", "detail": str(e)}, status=500
|
||||
)
|
||||
if error_response:
|
||||
return error_response
|
||||
else:
|
||||
# Режим без LDAP: создаём пользователя напрямую в БД
|
||||
emp = Employee.objects.create(
|
||||
emp = self.create_db_user(
|
||||
first_name=v["first_name"],
|
||||
last_name=v["last_name"],
|
||||
email=email,
|
||||
phone_number=phone_norm,
|
||||
password=password,
|
||||
is_active=False, # не активен до верификации email
|
||||
is_ldap_managed=False,
|
||||
)
|
||||
# Устанавливаем пароль в БД
|
||||
emp.set_password(password)
|
||||
|
||||
# 2) Заполняем доп.поля БД
|
||||
if avatar_bytes:
|
||||
|
||||
184
backend/api/v1/employees/views/mixins.py
Normal file
184
backend/api/v1/employees/views/mixins.py
Normal file
@ -0,0 +1,184 @@
|
||||
"""Миксины для API views с LDAP-специфичной логикой."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from django.conf import settings
|
||||
from employees.ldap.directory_service import DirectoryService, DirectoryUserDTO
|
||||
from employees.ldap.errors import DirectoryDbError, DirectoryLdapError
|
||||
from rest_framework.response import Response
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from employees.models import Employee
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _is_ldap_enabled() -> bool:
|
||||
"""Проверяет, включена ли интеграция с LDAP."""
|
||||
return getattr(settings, "LDAP_ENABLED", False)
|
||||
|
||||
|
||||
class LdapUserCreationMixin:
|
||||
"""Миксин для создания пользователей в LDAP при регистрации.
|
||||
|
||||
Используется в RegisterAPIView для вынесения LDAP-специфичной логики
|
||||
из основного метода регистрации.
|
||||
|
||||
Методы:
|
||||
create_ldap_user(): Создаёт пользователя в LDAP через DirectoryService
|
||||
create_db_user(): Создаёт пользователя напрямую в БД (fallback без LDAP)
|
||||
"""
|
||||
|
||||
def create_ldap_user(
|
||||
self,
|
||||
*,
|
||||
first_name: str,
|
||||
last_name: str,
|
||||
email: str,
|
||||
phone_e164: str,
|
||||
password: str,
|
||||
avatar_bytes: Optional[bytes] = None,
|
||||
is_active: bool = False,
|
||||
) -> tuple[Optional['Employee'], Optional[Response]]:
|
||||
"""Создаёт disabled пользователя в LDAP с паролем.
|
||||
|
||||
Args:
|
||||
first_name: Имя
|
||||
last_name: Фамилия
|
||||
email: Email (нормализованный, lowercase)
|
||||
phone_e164: Телефон в E.164 формате
|
||||
password: Пароль (идёт только в LDAP, в БД будет unusable)
|
||||
avatar_bytes: Байты аватара (опционально)
|
||||
is_active: False до верификации email
|
||||
|
||||
Returns:
|
||||
tuple[Employee | None, Response | None]:
|
||||
- (Employee, None) при успехе
|
||||
- (None, Response) при ошибке (Response для возврата клиенту)
|
||||
"""
|
||||
svc = DirectoryService()
|
||||
dto = DirectoryUserDTO(
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
email=email,
|
||||
phone_e164=phone_e164,
|
||||
department_dn=None,
|
||||
group_cns=[],
|
||||
initial_password=password, # пароль идёт только в LDAP
|
||||
avatar_bytes=avatar_bytes,
|
||||
is_active=is_active, # disabled до верификации email
|
||||
)
|
||||
|
||||
try:
|
||||
emp = svc.create_user(dto)
|
||||
return emp, None
|
||||
except DirectoryLdapError as e:
|
||||
logger.error(f"LDAP user creation failed: {e}", exc_info=True)
|
||||
return None, Response(
|
||||
{"ok": False, "error": "ldap_error", "detail": str(e)},
|
||||
status=502
|
||||
)
|
||||
except DirectoryDbError as e:
|
||||
logger.error(f"DB error during LDAP user creation: {e}", exc_info=True)
|
||||
return None, Response(
|
||||
{"ok": False, "error": "db_error", "detail": str(e)},
|
||||
status=500
|
||||
)
|
||||
|
||||
def create_db_user(
|
||||
self,
|
||||
*,
|
||||
first_name: str,
|
||||
last_name: str,
|
||||
email: str,
|
||||
phone_number: str,
|
||||
password: str,
|
||||
is_active: bool = False,
|
||||
) -> 'Employee':
|
||||
"""Создаёт пользователя напрямую в БД (режим без LDAP).
|
||||
|
||||
Args:
|
||||
first_name: Имя
|
||||
last_name: Фамилия
|
||||
email: Email (нормализованный, lowercase)
|
||||
phone_number: Телефон в E.164 формате
|
||||
password: Пароль (устанавливается через set_password)
|
||||
is_active: False до верификации email
|
||||
|
||||
Returns:
|
||||
Employee: Созданный пользователь
|
||||
"""
|
||||
from employees.models import Employee
|
||||
|
||||
emp = Employee.objects.create(
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
email=email,
|
||||
phone_number=phone_number,
|
||||
is_active=is_active,
|
||||
is_ldap_managed=False,
|
||||
)
|
||||
# Устанавливаем пароль в БД
|
||||
emp.set_password(password)
|
||||
emp.save(update_fields=['password'])
|
||||
|
||||
return emp
|
||||
|
||||
|
||||
class LdapPasswordMixin:
|
||||
"""Миксин для endpoints, работающих с паролями LDAP пользователей.
|
||||
|
||||
Используется в PasswordChangeView, PasswordResetView и других
|
||||
endpoints, которым нужно обновлять пароли в LDAP.
|
||||
|
||||
Методы:
|
||||
update_ldap_password(): Обновляет пароль пользователя в LDAP
|
||||
"""
|
||||
|
||||
def update_ldap_password(
|
||||
self,
|
||||
employee: 'Employee',
|
||||
new_password: str,
|
||||
) -> tuple[bool, Optional[Response]]:
|
||||
"""Обновляет пароль пользователя в LDAP.
|
||||
|
||||
Args:
|
||||
employee: Employee instance
|
||||
new_password: Новый пароль
|
||||
|
||||
Returns:
|
||||
tuple[bool, Response | None]:
|
||||
- (True, None) при успехе
|
||||
- (False, Response) при ошибке (Response для возврата клиенту)
|
||||
"""
|
||||
if not employee.is_ldap_managed:
|
||||
# Не LDAP пользователь - устанавливаем пароль в БД
|
||||
employee.set_password(new_password)
|
||||
employee.save(update_fields=['password'])
|
||||
return True, None
|
||||
|
||||
# LDAP пользователь - обновляем через DirectoryService
|
||||
try:
|
||||
svc = DirectoryService()
|
||||
svc.update_user(
|
||||
emp=employee,
|
||||
changes={'password': new_password},
|
||||
group_cns=None,
|
||||
move_to_department_dn=None,
|
||||
)
|
||||
return True, None
|
||||
except DirectoryLdapError as e:
|
||||
logger.error(f"LDAP password update failed for user {employee.id}: {e}", exc_info=True)
|
||||
return False, Response(
|
||||
{"ok": False, "error": "ldap_error", "detail": str(e)},
|
||||
status=502
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error updating LDAP password for user {employee.id}: {e}", exc_info=True)
|
||||
return False, Response(
|
||||
{"ok": False, "error": "password_update_failed", "detail": str(e)},
|
||||
status=500
|
||||
)
|
||||
Loading…
x
Reference in New Issue
Block a user