"""Auth-related views: регистрация, подтверждение email, повторная отправка кода.""" from __future__ import annotations import logging from datetime import timedelta from common.emails import send_templated_mail 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, DirectoryServiceError) from employees.models import Position, Skill from employees.utils import _normalize_phone from rest_framework import status from rest_framework.parsers import FormParser, JSONParser, MultiPartParser from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.views import APIView from ..serializers import (EmailSerializer, EmailVerifySerializer, RegisterSerializer) from ._helpers import Employee, _is_ldap_enabled logger = logging.getLogger(__name__) class ResendEmailAPIView(APIView): """POST /api/v1/auth/resend-email/ body: {"email": "..."}""" throttle_scope = "anon" permission_classes = [AllowAny] def post(self, request): ser = EmailSerializer(data=request.data) ser.is_valid(raise_exception=True) email = ser.validated_data["email"] user = Employee.objects.filter(email__iexact=email).first() if not user: return Response({"ok": False, "error": "user_not_found"}, status=404) if user.email_verified: return Response({"ok": False, "error": "already_verified"}, status=400) user.email_activation_code = get_random_string(6, "0123456789") user.save(update_fields=["email_activation_code"]) send_templated_mail( subject="Подтверждение регистрации", to=[user.email], template_base="emails/registration_verify_code", context={"code": user.email_activation_code, "user": user}, ) return Response({"ok": True}, status=200) class VerifyEmailAPIView(APIView): throttle_scope = "anon" permission_classes = [AllowAny] def post(self, request): """Подтверждает email и активирует пользователя. В режиме с LDAP активирует запись в LDAP. В режиме без LDAP просто активирует пользователя в БД. """ ser = EmailVerifySerializer(data=request.data) ser.is_valid(raise_exception=True) email = ser.validated_data["email"].strip().lower() code = ser.validated_data["code"].strip() user = Employee.objects.filter(email__iexact=email).first() if not user: return Response({"ok": False, "error": "user_not_found"}, status=404) if not code: return Response({"ok": False, "error": "empty_code"}, status=400) created = getattr(user, "created_at", None) or getattr( user, "date_joined", None ) if created and not user.email_verified: if timezone.now() - created > timedelta(minutes=5): user.delete() return Response({"ok": False, "error": "expired"}, status=400) if not user.verify_email(code): return Response({"ok": False, "error": "invalid_code"}, status=400) ldap_enabled = _is_ldap_enabled() if ldap_enabled: # Режим с LDAP: активируем запись в LDAP try: from employees.models import LdapSyncState svc = DirectoryService() # Проверяем наличие LDAP-идентификаторов в LdapSyncState sync_state = LdapSyncState.objects.filter( model="employee", object_pk=str(user.pk) ).first() has_ldap = sync_state and ( sync_state.ldap_dn or sync_state.ldap_guid) if has_ldap: # Запись существует - активируем через DirectoryService user = svc.update_user(user, {"is_active": True}) else: # LDAP запись не найдена - активируем только в БД logger.warning( f"User {email} has no LDAP sync state, activating in DB only" ) user.is_active = True user.save(update_fields=["is_active"]) # Убеждаемся, что БД тоже активна if not user.is_active: user.is_active = True user.save(update_fields=["is_active"]) except DirectoryLdapError as e: # Если LDAP недоступен, всё равно активируем в БД logger.warning( f"LDAP error during activation for {email}: {e}, activating in DB" ) user.is_active = True user.save(update_fields=["is_active"]) except DirectoryDbError as e: return Response( {"ok": False, "error": "db_error", "detail": str(e)}, status=500 ) except DirectoryServiceError as e: # При ошибке сервиса тоже активируем в БД logger.warning( f"Service error during activation for {email}: {e}, activating in DB" ) user.is_active = True user.save(update_fields=["is_active"]) else: # Режим без LDAP: просто активируем пользователя в БД if not user.is_active: user.is_active = True user.save(update_fields=["is_active"]) return Response({"ok": True, "user_id": user.id}, status=200) class RegisterAPIView(APIView): """Регистрация: создаём учётку в LDAP (disabled) с паролем, в БД — set_unusable_password.""" throttle_scope = "anon" permission_classes = [AllowAny] parser_classes = (JSONParser, FormParser, MultiPartParser) @transaction.atomic def post(self, request): # Логируем входящие данные для диагностики logger.warning(f"[REGISTER] Received data: {request.data}") logger.warning(f"[REGISTER] Content-Type: {request.content_type}") # 0) хотя бы один контакт if not ( request.data.get("telegram") or request.data.get("whatsapp") or request.data.get("wechat") ): logger.warning("[REGISTER] No contact provided") return Response( { "detail": "Заполните хотя бы одно из полей: WhatsApp, WeChat или Telegram" }, status=400, ) ser = RegisterSerializer(data=request.data) if not ser.is_valid(): logger.warning(f"[REGISTER] Validation errors: {ser.errors}") ser.is_valid(raise_exception=True) v = ser.validated_data email = v["email"].strip().lower() password = v["password"] phone_norm = _normalize_phone( v.get("phone_number") or request.data.get("phone") ) if not phone_norm: return Response({"ok": False, "error": "invalid_phone"}, status=400) existing_phone = Employee.objects.filter( phone_number=phone_norm).first() if existing_phone: logger.warning( "[REGISTER] Phone %s already used by user id=%s", phone_norm, existing_phone.id, ) return Response( { "ok": False, "error": "phone_taken", "detail": "Номер телефона уже зарегистрирован." " Войдите в существующий аккаунт или используйте другой номер.", "phone_number": [ "Этот номер телефона уже привязан к другому аккаунту." ], }, status=400, ) user = Employee.objects.filter(email__iexact=email).first() if user: if user.email_verified: # Email уже верифицирован - нельзя регистрироваться return Response({"ok": False, "error": "email_taken"}, status=400) else: # Есть неверифицированный пользователь - повторная отправка кода return Response( {"ok": True, "pending_verification": True, "user_id": user.id}, status=200, ) avatar_file = v.get("avatar") or getattr( request, "FILES", {}).get("avatar") avatar_bytes = None avatar_name = None if avatar_file: try: avatar_bytes = ( avatar_file.read() if hasattr(avatar_file, "read") else None ) avatar_name = getattr( avatar_file, "name", None) or "avatar.jpg" except Exception: avatar_bytes = None avatar_name = None ldap_enabled = _is_ldap_enabled() if ldap_enabled: # Режим с LDAP: создаём disabled учётку в LDAP + пароль svc = DirectoryService() dto = DirectoryUserDTO( 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 avatar_bytes=avatar_bytes, is_active=False, # disabled до верификации ) 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 ) else: # Режим без LDAP: создаём пользователя напрямую в БД emp = Employee.objects.create( first_name=v["first_name"], last_name=v["last_name"], email=email, phone_number=phone_norm, is_active=False, # не активен до верификации email is_ldap_managed=False, ) # Устанавливаем пароль в БД emp.set_password(password) # 2) Заполняем доп.поля БД if avatar_bytes: try: emp.avatar.save(avatar_name, ContentFile( avatar_bytes), save=False) except Exception: pass emp.telegram = v.get("telegram", "") emp.whatsapp = v.get("whatsapp", "") emp.wechat = v.get("wechat", "") emp.birth_date = v["birth_date"] emp.gender = v.get("gender") # опциональное поле if v.get("patronymic"): emp.patronymic = v["patronymic"] pos_id = v.get("position") if pos_id and Position.objects.filter(pk=pos_id).exists(): emp.position_id = pos_id emp.save() skills_ids = v.get("skills") or [] if skills_ids: emp.skills.set(Skill.objects.filter(pk__in=skills_ids)) # 3) Отправляем код верификации emp.email_activation_code = get_random_string(6, "0123456789") emp.save(update_fields=["email_activation_code"]) send_templated_mail( subject="Подтверждение регистрации", to=[emp.email], template_base="emails/registration_verify_code", context={"code": emp.email_activation_code, "user": emp}, ) return Response( { "id": emp.id, "email": emp.email, "email_verified": emp.email_verified, "is_active": emp.is_active, }, status=status.HTTP_201_CREATED, )