feat / AEB-26 login page

This commit is contained in:
Timofey Syrokvashko
2025-09-01 11:34:30 +03:00
parent 38a31824bc
commit 65a63235e5
37 changed files with 1185 additions and 49 deletions

View File

View File

@@ -0,0 +1,30 @@
from rest_framework import serializers
from django.conf import settings
class UserResponseSerializer(serializers.Serializer):
id = serializers.IntegerField()
email = serializers.EmailField()
name = serializers.CharField(source='first_name')
surname = serializers.CharField(source='last_name')
image = serializers.SerializerMethodField()
uuid = serializers.SerializerMethodField()
account_type = serializers.CharField(source='userprofile.account_type')
def get_uuid(self, obj):
try:
return str(obj.userprofile.uuid)[:6]
except Exception as e:
return None
def get_image(self, obj):
try:
if obj.userprofile.image:
relative_url = obj.userprofile.image.url
base_url = settings.BASE_URL
base_url = base_url.rstrip('/')
relative_url = relative_url.lstrip('/')
full_url = f"{base_url}/{relative_url}"
return full_url
return None
except Exception as e:
return None

View File

@@ -0,0 +1,24 @@
from django.urls import path
from .views.UserDataView import UserDataView
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularSwaggerView,
SpectacularRedocView,
)
urlpatterns = [
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',
),
path("user/", UserDataView.as_view(), name="user-data"),
]

View File

@@ -0,0 +1,62 @@
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from drf_spectacular.utils import extend_schema, OpenApiResponse, OpenApiExample
from api.auth.serializers import UserResponseSerializer
from api.models import UserProfile
from api.utils.decorators import handle_exceptions
@extend_schema(tags=['Профиль'])
class UserDataView(APIView):
"""View для получения данных текущего пользователя"""
permission_classes = [IsAuthenticated]
serializer_class = UserResponseSerializer
@extend_schema(
summary="Получение данных пользователя",
description="Получение данных авторизованного пользователя для инициализации клиентского состояния",
responses={
200: OpenApiResponse(
response=UserResponseSerializer,
description="Данные пользователя успешно получены",
examples=[
OpenApiExample(
'Успешный ответ',
value={
"id": 1,
"email": "user@example.com",
"account_type": "engieneer",
"name": "Иван",
"surname": "Иванов",
"imageURL": "https://example.com/avatar.jpg",
"uuid": "abc123"
}
)
]
),
404: OpenApiResponse(
description="Профиль пользователя не найден",
examples=[
OpenApiExample(
'Профиль не найден',
value={"error": "Профиль пользователя не найден"}
)
]
)
}
)
@handle_exceptions
def get(self, request):
"""Получение данных текущего пользователя"""
try:
user_data = UserResponseSerializer(request.user).data
return Response(user_data, status=status.HTTP_200_OK)
except UserProfile.DoesNotExist:
return Response(
{"error": "Профиль пользователя не найден"},
status=status.HTTP_404_NOT_FOUND
)

View File

@@ -0,0 +1,3 @@
"""
Логика работы с аккаунтом пользователя
"""

View File

@@ -1,3 +1,18 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import User
from api.models import UserProfile
# Register your models here.
class UserProfileInline(admin.StackedInline):
model = UserProfile
can_delete = False
verbose_name = 'Профиль пользователя'
verbose_name_plural = 'Профили пользователей'
fields = ('uuid', 'account_type', 'imageURL')
readonly_fields = ('uuid',)
class UserAdmin(BaseUserAdmin):
inlines = (UserProfileInline,)
admin.site.unregister(User)
admin.site.register(User, UserAdmin)

View File

@@ -9,7 +9,7 @@ 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 django.contrib.auth.models import User as DjangoUser
from .serializers import (
UserResponseSerializer,
@@ -21,7 +21,6 @@ from .serializers import (
)
from api.utils.cookies import AuthBaseViewSet
from api.types import User
@extend_schema(tags=['Логин'])
@@ -111,12 +110,13 @@ class LoginViewSet(AuthBaseViewSet):
)
try:
user = User.objects.get(login=login)
except User.DoesNotExist:
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": "Неверный пароль"},
@@ -134,7 +134,6 @@ class LoginViewSet(AuthBaseViewSet):
"user": user_data
}, status=status.HTTP_200_OK)
# сеттим куки
return self._set_auth_cookies(response, refresh)
except Exception as e:

View File

@@ -0,0 +1,49 @@
# Generated by Django 5.2.5 on 2025-09-01 08:20
import django.contrib.auth.models
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('user_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
bases=('auth.user',),
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, null=True, unique=True)),
('account_type', models.CharField(choices=[('engineer', 'Инженер'), ('operator', 'Оператор'), ('admin', 'Администратор')], db_index=True, max_length=10, verbose_name='Тип аккаунта')),
('imageURL', models.CharField(blank=True, max_length=255, null=True, verbose_name='URL изображения профиля')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='userprofile', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Профиль пользователя',
'verbose_name_plural': 'Профили пользователей',
},
),
]

View File

@@ -1,5 +1,6 @@
from django.urls import path, include
urlpatterns = [
path('v1/auth/', include('api.auth.urls'))
path('v1/auth/', include('api.auth.urls')),
path('v1/account/', include('api.account.urls'))
]

View File

@@ -0,0 +1,75 @@
from functools import wraps
from rest_framework.response import Response
from rest_framework import status
from django.core.exceptions import ValidationError, PermissionDenied
from django.http import Http404
from rest_framework.exceptions import APIException
import traceback
from datetime import datetime
def handle_exceptions(func):
"""
Обработчик ошибок для API endpoints
Обрабатывает различные типы исключений и возвращает соответствующие HTTP статусы
Текущие обрабоки:
- Ошибки валидации - HTTP400
- Объект не найден - HTTP404
- Ошибки доступа - HTTP403
- Обработки DRF исключений - вернется статус код от DRF
- Необработанные исключения - HTTP500
Автоматически выводит все ошибки в консоль с временной меткой.
"""
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except ValidationError as e:
# ошибки валидации
print(f"\n[{datetime.now()}] VALIDATION ERROR in {func.__name__}:")
print(f"Error details: {e.messages if hasattr(e, 'messages') else str(e)}\n")
return Response(
{"error": e.messages if hasattr(e, 'messages') else str(e)},
status=status.HTTP_400_BAD_REQUEST
)
except Http404 as e:
# объект не найден
print(f"\n[{datetime.now()}] NOT FOUND ERROR in {func.__name__}:")
print(f"Error details: {str(e) or 'Запрашиваемый ресурс не найден'}\n")
return Response(
{"error": str(e) or "Запрашиваемый ресурс не найден"},
status=status.HTTP_404_NOT_FOUND
)
except PermissionDenied as e:
# ошибки доступа
print(f"\n[{datetime.now()}] PERMISSION ERROR in {func.__name__}:")
print(f"Error details: {str(e) or 'У вас нет прав для выполнения этого действия'}\n")
return Response(
{"error": str(e) or "У вас нет прав для выполнения этого действия"},
status=status.HTTP_403_FORBIDDEN
)
except APIException as e:
# обработка DRF исключений
print(f"\n[{datetime.now()}] API ERROR in {func.__name__}:")
print(f"Error details: {str(e)}")
print(f"Status code: {e.status_code}\n")
return Response(
{"error": str(e)},
status=e.status_code
)
except Exception as e:
# необработанные исключения
print(f"\n[{datetime.now()}] UNHANDLED ERROR in {func.__name__}:")
print(f"Error type: {type(e).__name__}")
print(f"Error details: {str(e)}")
print("Traceback:")
print(traceback.format_exc())
print() # пустая строка для разделения
return Response(
{
"error": "Произошла внутренняя ошибка сервера",
"detail": str(e)
},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
return wrapper