325 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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,
)