from __future__ import annotations from api.v1.employees.views._helpers import PHONE_FIELD from django.contrib.auth import get_user_model from employees.models import Employee from employees.utils import _normalize_phone from rest_framework import serializers from rest_framework.exceptions import AuthenticationFailed from rest_framework_simplejwt.serializers import ( TokenObtainPairSerializer, TokenRefreshSerializer, ) from .models import UserAuthSession from .services import ( SESSION_ID_CLAIM, create_auth_session, hash_refresh_token, validate_refresh_session, ) User = get_user_model() class TokenPairResponseSerializer(serializers.Serializer): refresh = serializers.CharField(read_only=True) access = serializers.CharField(read_only=True) class TokenRefreshRequestSerializer(serializers.Serializer): refresh = serializers.CharField( help_text="Refresh token, полученный при логине." ) class TokenRefreshResponseSerializer(serializers.Serializer): access = serializers.CharField(read_only=True) class SessionSerializer(serializers.ModelSerializer): session_id = serializers.UUIDField(read_only=True) is_current = serializers.SerializerMethodField() class Meta: model = UserAuthSession fields = ( "session_id", "is_current", "device_name", "ip_address", "created_at", "last_seen_at", "revoked_at", ) def get_is_current(self, obj: UserAuthSession) -> bool: current_session_id = self.context.get("current_session_id") return str(obj.session_id) == str(current_session_id) class SessionBulkActionResponseSerializer(serializers.Serializer): revoked = serializers.IntegerField(read_only=True) class ChangePasswordRequestSerializer(serializers.Serializer): current_password = serializers.CharField( write_only=True, trim_whitespace=False, style={"input_type": "password"}, ) new_password = serializers.CharField( write_only=True, min_length=6, trim_whitespace=False, style={"input_type": "password"}, ) new_password_confirm = serializers.CharField( write_only=True, trim_whitespace=False, style={"input_type": "password"}, ) def validate(self, attrs): if attrs["new_password"] != attrs["new_password_confirm"]: raise serializers.ValidationError( {"new_password_confirm": "Подтверждение пароля не совпадает."} ) if attrs["current_password"] == attrs["new_password"]: raise serializers.ValidationError( {"new_password": "Новый пароль должен отличаться от текущего."} ) return attrs class ChangePasswordResponseSerializer(serializers.Serializer): ok = serializers.BooleanField(read_only=True) class PasswordResetRequestSerializer(serializers.Serializer): login = serializers.CharField( write_only=True, allow_blank=False, trim_whitespace=True, help_text="Email или телефон пользователя.", ) class PasswordResetRequestResponseSerializer(serializers.Serializer): ok = serializers.BooleanField(read_only=True) class PasswordResetConfirmRequestSerializer(serializers.Serializer): uid = serializers.CharField(write_only=True) token = serializers.CharField(write_only=True) new_password = serializers.CharField( write_only=True, min_length=6, trim_whitespace=False, style={"input_type": "password"}, ) new_password_confirm = serializers.CharField( write_only=True, trim_whitespace=False, style={"input_type": "password"}, ) def validate(self, attrs): if attrs["new_password"] != attrs["new_password_confirm"]: raise serializers.ValidationError( {"new_password_confirm": "Подтверждение пароля не совпадает."} ) return attrs class PasswordResetConfirmResponseSerializer(serializers.Serializer): ok = serializers.BooleanField(read_only=True) class PhoneOrEmailTokenObtainPairSerializer(TokenObtainPairSerializer): """ Принимает email ИЛИ phone/phone_number + password. Если пришёл телефон — ищем пользователя и подставляем его email. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # <- КЛЮЧЕВОЕ: email (username_field) не обязателен self.fields[self.username_field].required = False self.fields[self.username_field].help_text = ( "Email пользователя. Можно передать либо email, либо телефон." ) # добавляем телефонные поля self.fields["phone"] = serializers.CharField( required=False, allow_blank=True, help_text="Номер телефона пользователя в произвольном формате.", ) self.fields["phone_number"] = serializers.CharField( required=False, allow_blank=True, help_text="Альтернативное имя поля для номера телефона.", ) def validate(self, attrs): raw_phone = ( self.initial_data.get("phone") or self.initial_data.get("phone_number") ) if raw_phone and not attrs.get(self.username_field): qfield = PHONE_FIELD or "phone_number" norm = _normalize_phone(raw_phone) or str(raw_phone).strip() try: user = Employee.objects.get(**{qfield: norm}) # подставляем email в стандартный механизм SimpleJWT attrs[self.username_field] = user.email except Employee.DoesNotExist: pass # базовая валидация ниже вернёт 401 # Проверяем что есть email (после подстановки из телефона или напрямую) if not attrs.get(self.username_field): raise AuthenticationFailed( "Email or phone number required", code="no_credentials", ) super().validate(attrs) # не выдаём токены, если email не подтверждён/аккаунт не активен if not self.user.email_verified or not self.user.is_active: raise AuthenticationFailed( "email_not_verified", code="email_not_verified", ) request = self.context.get("request") if request is None: raise AuthenticationFailed("request_context_missing") session = create_auth_session(user=self.user, request=request) refresh = self.get_token(self.user) refresh[SESSION_ID_CLAIM] = str(session.session_id) access = refresh.access_token access[SESSION_ID_CLAIM] = str(session.session_id) session.refresh_token_hash = hash_refresh_token(str(refresh)) session.save(update_fields=["refresh_token_hash"]) return { "refresh": str(refresh), "access": str(access), } class SessionTokenRefreshSerializer(TokenRefreshSerializer): def validate(self, attrs): raw_refresh = attrs["refresh"] refresh = self.token_class(raw_refresh) session = validate_refresh_session( refresh, raw_refresh=raw_refresh, request=self.context.get("request"), ) access = refresh.access_token access[SESSION_ID_CLAIM] = str(session.session_id) return {"access": str(access)}