Рефакторинг инфраструктуры и конфигурации проекта
Some checks failed
CI/CD Pipeline / Code Quality Checks (push) Successful in 1m52s
CI/CD Pipeline / Run Tests (push) Failing after 2m2s
CI/CD Pipeline / Build & Push Images (push) Has been skipped

- Перенесена структура Django-конфига в src/core и src/settings

- Унифицирована Docker-сборка и docker-compose для dev/prod

- Добавлены startup-checks (DB/Redis) и обновлены env-шаблоны

- Расширена OpenAPI-документация и ответы API

- Удалены устаревшие deploy/requirements/служебные скрипты

- Обновлены CI/CD, README и тесты
This commit is contained in:
2026-02-18 13:25:01 +01:00
parent 0f4af561de
commit d5d184537f
71 changed files with 1253 additions and 2318 deletions

View File

@@ -1,5 +1,4 @@
from django.urls import path
from rest_framework_simplejwt.views import TokenVerifyView
from . import views
@@ -11,7 +10,7 @@ urlpatterns = [
path("login/", views.LoginView.as_view(), name="login"),
path("logout/", views.LogoutView.as_view(), name="logout"),
path("token/refresh/", views.TokenRefreshView.as_view(), name="token_refresh"),
path("token/verify/", TokenVerifyView.as_view(), name="token_verify"),
path("token/verify/", views.TokenVerifySwaggerView.as_view(), name="token_verify"),
# Пользовательские данные
path("me/", views.CurrentUserView.as_view(), name="current_user"),
path("me/update/", views.UserUpdateView.as_view(), name="user_update"),

View File

@@ -1,3 +1,4 @@
from apps.core.openapi import CommonResponses, ErrorResponses, swagger_tag
from django.contrib.auth import authenticate
from django.contrib.auth.hashers import check_password
from drf_yasg import openapi
@@ -8,6 +9,7 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.views import TokenVerifyView as SimpleJWTTokenVerifyView
from .serializers import (
LoginSerializer,
@@ -21,8 +23,8 @@ from .serializers import (
from .services import ProfileService, UserService
# Swagger теги для группировки
AUTH_TAG = "Аутентификация"
USER_TAG = "Пользователь"
AUTH_TAG = swagger_tag("Аутентификация", "authentication")
USER_TAG = swagger_tag("Пользователь", "user")
class RegisterView(APIView):
@@ -39,7 +41,11 @@ class RegisterView(APIView):
operation_summary="Регистрация",
operation_description="Создание новой учётной записи пользователя.",
request_body=UserRegistrationSerializer,
responses={201: UserSerializer},
responses={
201: UserSerializer,
400: CommonResponses.BAD_REQUEST,
**ErrorResponses.PUBLIC,
},
)
def post(self, request):
serializer = UserRegistrationSerializer(data=request.data)
@@ -74,7 +80,12 @@ class LoginView(APIView):
operation_summary="Вход",
operation_description="Аутентификация по email и паролю. Возвращает JWT токены.",
request_body=LoginSerializer,
responses={200: TokenSerializer},
responses={
200: TokenSerializer,
400: CommonResponses.BAD_REQUEST,
401: CommonResponses.UNAUTHORIZED,
**ErrorResponses.PUBLIC,
},
)
def post(self, request):
serializer = LoginSerializer(data=request.data)
@@ -108,7 +119,10 @@ class LogoutView(APIView):
tags=[AUTH_TAG],
operation_summary="Выход",
operation_description="Выход из системы (удаление токенов на клиенте).",
responses={200: "Успешный выход"},
responses={
200: "Успешный выход",
**ErrorResponses.AUTHENTICATED,
},
)
def post(self, request):
# Для JWT логаут означает удаление токенов на клиенте.
@@ -125,7 +139,10 @@ class CurrentUserView(APIView):
tags=[USER_TAG],
operation_summary="Текущий пользователь",
operation_description="Возвращает данные авторизованного пользователя.",
responses={200: UserSerializer},
responses={
200: UserSerializer,
**ErrorResponses.AUTHENTICATED,
},
)
def get(self, request):
serializer = UserSerializer(request.user)
@@ -142,7 +159,10 @@ class UserUpdateView(APIView):
operation_summary="Обновить данные",
operation_description="Частичное обновление данных пользователя.",
request_body=UserUpdateSerializer,
responses={200: UserSerializer},
responses={
200: UserSerializer,
**ErrorResponses.AUTHENTICATED_VALIDATION,
},
)
def patch(self, request):
serializer = UserUpdateSerializer(request.user, data=request.data, partial=True)
@@ -158,6 +178,7 @@ class ProfileDetailView(generics.RetrieveUpdateAPIView):
permission_classes = [IsAuthenticated]
serializer_class = ProfileUpdateSerializer
http_method_names = ["get", "patch", "head", "options"]
def get_object(self):
profile = ProfileService.get_profile_by_user_id_or_none(self.request.user.id)
@@ -172,6 +193,10 @@ class ProfileDetailView(generics.RetrieveUpdateAPIView):
tags=[USER_TAG],
operation_summary="Получить профиль",
operation_description="Возвращает профиль текущего пользователя.",
responses={
200: ProfileUpdateSerializer,
**ErrorResponses.AUTHENTICATED,
},
)
def get(self, request, *args, **kwargs):
profile = self.get_object()
@@ -183,6 +208,10 @@ class ProfileDetailView(generics.RetrieveUpdateAPIView):
operation_summary="Обновить профиль",
operation_description="Частичное обновление профиля пользователя.",
request_body=ProfileUpdateSerializer,
responses={
200: ProfileUpdateSerializer,
**ErrorResponses.AUTHENTICATED_VALIDATION,
},
)
def patch(self, request, *args, **kwargs):
profile = self.get_object()
@@ -205,7 +234,10 @@ class PasswordChangeView(APIView):
operation_summary="Сменить пароль",
operation_description="Смена пароля. Требуется текущий пароль для подтверждения.",
request_body=PasswordChangeSerializer,
responses={200: "Пароль успешно изменен"},
responses={
200: "Пароль успешно изменен",
**ErrorResponses.AUTHENTICATED_VALIDATION,
},
)
def post(self, request):
serializer = PasswordChangeSerializer(data=request.data)
@@ -234,6 +266,10 @@ class PasswordChangeView(APIView):
tags=[USER_TAG],
operation_summary="Полный профиль",
operation_description="Расширенная информация о пользователе и профиле.",
responses={
200: ProfileUpdateSerializer,
**ErrorResponses.AUTHENTICATED,
},
)
@api_view(["GET"])
@permission_classes([IsAuthenticated])
@@ -261,7 +297,12 @@ class TokenRefreshView(APIView):
},
required=["refresh"],
),
responses={200: TokenSerializer},
responses={
200: TokenSerializer,
400: CommonResponses.BAD_REQUEST,
401: CommonResponses.UNAUTHORIZED,
**ErrorResponses.PUBLIC,
},
)
def post(self, request):
refresh_token = request.data.get("refresh")
@@ -280,3 +321,21 @@ class TokenRefreshView(APIView):
return Response(
{"error": "Неверный refresh token"}, status=status.HTTP_401_UNAUTHORIZED
)
class TokenVerifySwaggerView(SimpleJWTTokenVerifyView):
"""Проверка валидности access токена."""
@swagger_auto_schema(
tags=[AUTH_TAG],
operation_summary="Проверить токен",
operation_description="Проверяет валидность JWT токена.",
responses={
200: "Токен валиден",
400: CommonResponses.BAD_REQUEST,
401: CommonResponses.UNAUTHORIZED,
**ErrorResponses.PUBLIC,
},
)
def post(self, request, *args, **kwargs):
return super().post(request, *args, **kwargs)