login route

This commit is contained in:
Timofey
2025-08-29 14:44:26 +03:00
parent d314303066
commit 5a06a625fb
11 changed files with 701 additions and 5 deletions

View File

@@ -0,0 +1,62 @@
from typing import Any, Optional
from rest_framework import serializers
from django.conf import settings
from api.types import User
class UserResponseSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
email = serializers.EmailField(read_only=True)
account_type = serializers.CharField(
source='userprofile.account_type',
read_only=True
)
name = serializers.CharField(source='first_name', read_only=True)
surname = serializers.CharField(source='last_name', read_only=True)
imageURL = serializers.SerializerMethodField()
uuid = serializers.SerializerMethodField()
class Meta:
ref_name = "UserResponse" # для OpenAPI
def get_uuid(self, obj: User) -> Optional[str]:
"""Получает короткий UUID (первые 6 символов) из профиля пользователя"""
return obj.userprofile.short_uuid if hasattr(obj, 'userprofile') else None
def get_imageURL(self, obj: User) -> Optional[str]:
"""Получает полный URL для изображения профиля пользователя"""
try:
if not hasattr(obj, 'userprofile') or not obj.userprofile.imageURL:
return None
relative_url = obj.userprofile.imageURL.lstrip('/')
base_url = settings.BASE_URL.rstrip('/')
return f"{base_url}/{relative_url}"
except Exception:
return None
def to_representation(self, instance: User) -> dict[str, Any]:
"""Переопределяется для добавления проверки типа для вывода"""
data = super().to_representation(instance)
return {
'id': data['id'], # int
'email': data['email'], # str
'account_type': data['account_type'], # AccountTypeLiteral
'name': data['name'], # str
'surname': data['surname'], # str
'imageURL': data['imageURL'], # Optional[str]
'uuid': data['uuid'], # Optional[str]
}
class LoginRequestSerializer(serializers.Serializer):
"""Сериализатор для запроса авторизации"""
login = serializers.CharField(help_text="Логин пользователя")
password = serializers.CharField(help_text="Пароль пользователя", write_only=True)
class LoginResponseSerializer(serializers.Serializer):
"""Сериализатор для ответа при успешной авторизации"""
message = serializers.CharField()
access = serializers.CharField()
refresh = serializers.CharField()
user = UserResponseSerializer()

View File

@@ -1,6 +1,28 @@
from django.urls import path
# from . views import ()
from django.urls import path, include
from .views import LoginViewSet, LogoutView
from rest_framework.routers import DefaultRouter
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularSwaggerView,
SpectacularRedocView,
)
router = DefaultRouter()
router.register(r'', LoginViewSet, basename='auth')
urlpatterns = [
path('', include(router.urls)),
path('logout/', LogoutView.as_view(), name='auth-logout'),
path('schema/', SpectacularAPIView.as_view(), name='schema'),
path(
'docs/',
SpectacularSwaggerView.as_view(url_name='schema'),
name='swagger-ui',
),
# ReDoc UI - альтернативный вариант отображения доков:
path(
'redoc/',
SpectacularRedocView.as_view(url_name='schema'),
name='redoc',
),
]

View File

@@ -0,0 +1,167 @@
from rest_framework import status
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
from .serializers import (
UserResponseSerializer,
LoginRequestSerializer,
LoginResponseSerializer
)
from api.utils.cookies import AuthBaseViewSet
from api.types import User
class LoginViewSet(AuthBaseViewSet):
"""ViewSet для авторизации пользователей"""
serializer_class = LoginRequestSerializer
@extend_schema(
summary="Авторизация пользователя",
description="Эндпоинт для авторизации пользователя по логину и паролю",
request=LoginRequestSerializer,
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 = User.objects.get(login=login)
except User.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
)
class LogoutView(APIView):
"""ViewSet для выхода из системы"""
@extend_schema(
summary="Выход из системы",
description="Эндпоинт для выхода из системы, очищает все токены и куки",
responses={
200: OpenApiResponse(
description="Успешный выход",
response=OpenApiTypes.OBJECT,
examples=[
OpenApiExample(
'Успешный выход',
value={"message": "Logged out"}
)
]
)
}
)
def post(self, request):
response = Response({'message': 'Logged out'}, status=status.HTTP_200_OK)
# чистим куки и sessionID
response.delete_cookie('access_token')
response.delete_cookie('refresh_token')
response.delete_cookie('sessionid')
return response