from rest_framework import status from datetime import datetime import traceback from django.conf import settings from rest_framework.decorators import action from rest_framework.response import Response from rest_framework_simplejwt.tokens import RefreshToken from rest_framework.views import APIView from drf_spectacular.utils import extend_schema, OpenApiResponse, OpenApiExample from drf_spectacular.types import OpenApiTypes from django.contrib.auth.models import User as DjangoUser from .serializers import ( UserResponseSerializer, LoginRequestSerializer, LoginResponseSerializer, LogoutResponseSerializer, RefreshTokenRequestSerializer, RefreshTokenResponseSerializer ) from api.utils.cookies import AuthBaseViewSet @extend_schema(tags=['Логин']) class LoginViewSet(AuthBaseViewSet): """ViewSet для авторизации пользователей""" serializer_class = LoginRequestSerializer @extend_schema( summary="Авторизация пользователя", description="Эндпоинт для авторизации пользователя по логину и паролю", responses={ 200: OpenApiResponse( response=LoginResponseSerializer, description="Успешная авторизация", examples=[ OpenApiExample( 'Успешный ответ', value={ "message": "Успешная авторизация", "access": "eyJ0eXAiOiJKV1QiLCJhbGc...", "refresh": "eyJ0eXAiOiJKV1QiLCJhbGc...", "user": { "id": 1, "email": "user@example.com", "account_type": "engieneer", "name": "Иван", "surname": "Иванов", "imageURL": "https://example.com/avatar.jpg", "uuid": "abc123" } } ) ] ), 400: OpenApiResponse( description="Неверные параметры запроса", response=OpenApiTypes.OBJECT, examples=[ OpenApiExample( 'Отсутствуют обязательные поля', value={"error": "Логин и пароль обязательны"} ) ] ), 403: OpenApiResponse( description="Неверный пароль", response=OpenApiTypes.OBJECT, examples=[ OpenApiExample( 'Неверный пароль', value={"error": "Неверный пароль"} ) ] ), 404: OpenApiResponse( description="Пользователь не найден", response=OpenApiTypes.OBJECT, examples=[ OpenApiExample( 'Пользователь не найден', value={"error": "Пользователь не найден"} ) ] ), 500: OpenApiResponse( description="Внутренняя ошибка сервера", response=OpenApiTypes.OBJECT, examples=[ OpenApiExample( 'Ошибка сервера', value={"error": "Ошибка авторизации"} ) ] ) } ) @action(detail=False, methods=['post'], url_path="login") def login_client(self, request): try: login = request.data.get("login") password = request.data.get("password") if not login or not password: return Response( {"error": "Логин и пароль обязательны"}, status=status.HTTP_400_BAD_REQUEST ) try: user = DjangoUser.objects.get(username=login) except DjangoUser.DoesNotExist: return Response( {"error": "Пользователь не найден"}, status=status.HTTP_404_NOT_FOUND ) if not user.check_password(password): return Response( {"error": "Неверный пароль"}, status=status.HTTP_403_FORBIDDEN ) refresh = RefreshToken.for_user(user) user_data = UserResponseSerializer(user).data response = Response({ "message": "Успешная авторизация", "access": str(refresh.access_token), "refresh": str(refresh), "user": user_data }, status=status.HTTP_200_OK) return self._set_auth_cookies(response, refresh) except Exception as e: return Response( {"error": "Ошибка авторизации"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) @extend_schema(tags=['Логин']) class RefreshTokenView(APIView): """View для обновления JWT токенов""" serializer_class = RefreshTokenRequestSerializer @extend_schema( summary="Обновление токенов", description="Эндпоинт для обновления JWT токенов. Принимает refresh token и возвращает новую пару токенов.", request=RefreshTokenRequestSerializer, responses={ 200: OpenApiResponse( response=RefreshTokenResponseSerializer, description="Токены успешно обновлены", examples=[ OpenApiExample( 'Успешное обновление', value={ "access": "eyJ0eXAiOiJKV1QiLCJhbGc...", "refresh": "eyJ0eXAiOiJKV1QiLCJhbGc...", "expires_at": 1679831642.0 } ) ] ), 400: OpenApiResponse( description="Ошибка обновления токена", response=OpenApiTypes.OBJECT, examples=[ OpenApiExample( 'Отсутствует refresh token', value={"error": "Refresh token is required"} ), OpenApiExample( 'Невалидный refresh token', value={"error": "Invalid refresh token: Token is invalid or expired"} ) ] ) } ) def post(self, request): try: refresh_token = request.data.get('refresh') if not refresh_token: return Response( {'error': 'Требуется refresh token'}, status=status.HTTP_400_BAD_REQUEST ) try: token = RefreshToken(refresh_token) # точное время истечения токена expires_at = datetime.now() + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'] response_data = { 'access': str(token.access_token), 'refresh': str(token), 'expires_at': datetime.timestamp(expires_at) } return Response(response_data) except Exception as e: # логируем ошибки print(f"Token refresh error: {str(e)}") print(traceback.format_exc()) return Response( {'error': f'Невалидный refresh token: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST ) except Exception as e: return Response( {'error': f'Ошибка обновления токена: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST ) @extend_schema(tags=['Логаут']) class LogoutView(APIView): """View для выхода из системы""" serializer_class = LogoutResponseSerializer @extend_schema( summary="Выход из системы", description="Эндпоинт для выхода из системы. Очищает JWT токены и сессионные куки.", responses={ 200: OpenApiResponse( response=LogoutResponseSerializer, description="Успешный выход из системы", examples=[ OpenApiExample( 'Успешный выход', value={"message": "Успешный выход из системы"} ) ] ) } ) def post(self, request): """Выход из системы с очисткой всех токенов и куки""" response = Response( {'message': 'Успешный выход из системы'}, status=status.HTTP_200_OK ) response.delete_cookie('access_token') response.delete_cookie('refresh_token') response.delete_cookie('sessionid') return response