feat / AEB-26 login page
This commit is contained in:
0
backend/api/account/__init__.py
Normal file
0
backend/api/account/__init__.py
Normal file
30
backend/api/account/serializers/UserSerializer.py
Normal file
30
backend/api/account/serializers/UserSerializer.py
Normal 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
|
||||
0
backend/api/account/serializers/__init__.py
Normal file
0
backend/api/account/serializers/__init__.py
Normal file
24
backend/api/account/urls.py
Normal file
24
backend/api/account/urls.py
Normal 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"),
|
||||
]
|
||||
62
backend/api/account/views/UserDataView.py
Normal file
62
backend/api/account/views/UserDataView.py
Normal 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
|
||||
)
|
||||
3
backend/api/account/views/__init__.py
Normal file
3
backend/api/account/views/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Логика работы с аккаунтом пользователя
|
||||
"""
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
49
backend/api/migrations/0001_initial.py
Normal file
49
backend/api/migrations/0001_initial.py
Normal 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': 'Профили пользователей',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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'))
|
||||
]
|
||||
75
backend/api/utils/decorators.py
Normal file
75
backend/api/utils/decorators.py
Normal 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
|
||||
@@ -92,6 +92,7 @@ SPECTACULAR_SETTINGS = {
|
||||
'TAGS': [
|
||||
{'name': 'Логаут', 'description': 'Метод для работы с логаутом'},
|
||||
{'name': 'Логин', 'description': 'Методы для работы с логином'},
|
||||
{'name': 'Профиль', 'description': 'Методы для получения данных профиля пользователя'},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -100,7 +101,6 @@ MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 5.2.5 on 2025-09-01 08:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sitemanagement', '0002_alter_alert_options_alter_channel_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='metric',
|
||||
index=models.Index(fields=['timestamp'], name='sitemanagem_timesta_ac22b7_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='metric',
|
||||
index=models.Index(fields=['sensor'], name='sitemanagem_sensor__8d94ff_idx'),
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user