feat: implement CI/CD pipeline with Gitea Actions
- Add Gitea Actions workflow with 4 stages: lint, test, build, push - Configure ruff linting and formatting checks - Set up Django tests with PostgreSQL and Redis services - Implement Docker image building for web and celery services - Add requirements.txt and requirements-dev.txt generation - Fix ipdb compatibility issues in test runner - Update ruff configuration for Django compatibility - Add comprehensive CI/CD documentation
This commit is contained in:
@@ -1,127 +1,124 @@
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.hashers import check_password
|
||||
from rest_framework import status, generics, permissions
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework import generics, status
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
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 drf_yasg.utils import swagger_auto_schema
|
||||
from drf_yasg import openapi
|
||||
|
||||
from .models import User
|
||||
from .services import UserService, ProfileService
|
||||
from .serializers import (
|
||||
LoginSerializer,
|
||||
PasswordChangeSerializer,
|
||||
ProfileUpdateSerializer,
|
||||
TokenSerializer,
|
||||
UserRegistrationSerializer,
|
||||
UserSerializer,
|
||||
LoginSerializer,
|
||||
TokenSerializer,
|
||||
PasswordChangeSerializer,
|
||||
UserUpdateSerializer,
|
||||
ProfileUpdateSerializer
|
||||
)
|
||||
from .services import ProfileService, UserService
|
||||
|
||||
|
||||
class RegisterView(APIView):
|
||||
"""Регистрация нового пользователя"""
|
||||
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@swagger_auto_schema(
|
||||
request_body=UserRegistrationSerializer,
|
||||
responses={201: UserSerializer}
|
||||
request_body=UserRegistrationSerializer, responses={201: UserSerializer}
|
||||
)
|
||||
def post(self, request):
|
||||
serializer = UserRegistrationSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
# Убираем password_confirm из данных для создания пользователя
|
||||
user_data = serializer.validated_data.copy()
|
||||
user_data.pop('password_confirm', None)
|
||||
|
||||
user_data.pop("password_confirm", None)
|
||||
|
||||
user = UserService.create_user(**user_data)
|
||||
user_serializer = UserSerializer(user)
|
||||
tokens = UserService.get_tokens_for_user(user)
|
||||
|
||||
return Response({
|
||||
'user': user_serializer.data,
|
||||
'tokens': tokens
|
||||
}, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
return Response(
|
||||
{"user": user_serializer.data, "tokens": tokens},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class LoginView(APIView):
|
||||
"""Вход пользователя"""
|
||||
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@swagger_auto_schema(
|
||||
request_body=LoginSerializer,
|
||||
responses={200: TokenSerializer}
|
||||
)
|
||||
@swagger_auto_schema(request_body=LoginSerializer, responses={200: TokenSerializer})
|
||||
def post(self, request):
|
||||
serializer = LoginSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
email = serializer.validated_data['email']
|
||||
password = serializer.validated_data['password']
|
||||
|
||||
email = serializer.validated_data["email"]
|
||||
password = serializer.validated_data["password"]
|
||||
|
||||
user = authenticate(email=email, password=password)
|
||||
if user:
|
||||
tokens = UserService.get_tokens_for_user(user)
|
||||
return Response(tokens, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response(
|
||||
{'error': 'Неверные учетные данные'},
|
||||
status=status.HTTP_401_UNAUTHORIZED
|
||||
{"error": "Неверные учетные данные"},
|
||||
status=status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class LogoutView(APIView):
|
||||
"""Выход пользователя"""
|
||||
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
'Authorization',
|
||||
"Authorization",
|
||||
openapi.IN_HEADER,
|
||||
description="Bearer <token>",
|
||||
type=openapi.TYPE_STRING,
|
||||
required=True
|
||||
required=True,
|
||||
)
|
||||
],
|
||||
responses={200: 'Успешный выход'}
|
||||
responses={200: "Успешный выход"},
|
||||
)
|
||||
def post(self, request):
|
||||
try:
|
||||
refresh_token = request.data.get('refresh')
|
||||
refresh_token = request.data.get("refresh")
|
||||
if refresh_token:
|
||||
token = RefreshToken(refresh_token)
|
||||
token.blacklist()
|
||||
return Response({'message': 'Успешный выход'}, status=status.HTTP_200_OK)
|
||||
return Response({"message": "Успешный выход"}, status=status.HTTP_200_OK)
|
||||
except Exception:
|
||||
return Response({'error': 'Неверный токен'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
{"error": "Неверный токен"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
class CurrentUserView(APIView):
|
||||
"""Получение данных текущего пользователя"""
|
||||
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
'Authorization',
|
||||
"Authorization",
|
||||
openapi.IN_HEADER,
|
||||
description="Bearer <token>",
|
||||
type=openapi.TYPE_STRING,
|
||||
required=True
|
||||
required=True,
|
||||
)
|
||||
],
|
||||
responses={200: UserSerializer}
|
||||
responses={200: UserSerializer},
|
||||
)
|
||||
def get(self, request):
|
||||
serializer = UserSerializer(request.user)
|
||||
@@ -130,42 +127,38 @@ class CurrentUserView(APIView):
|
||||
|
||||
class UserUpdateView(APIView):
|
||||
"""Обновление данных пользователя"""
|
||||
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
request_body=UserUpdateSerializer,
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
'Authorization',
|
||||
"Authorization",
|
||||
openapi.IN_HEADER,
|
||||
description="Bearer <token>",
|
||||
type=openapi.TYPE_STRING,
|
||||
required=True
|
||||
required=True,
|
||||
)
|
||||
],
|
||||
responses={200: UserSerializer}
|
||||
responses={200: UserSerializer},
|
||||
)
|
||||
def patch(self, request):
|
||||
serializer = UserUpdateSerializer(request.user, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
user = UserService.update_user(
|
||||
request.user.id,
|
||||
**serializer.validated_data
|
||||
)
|
||||
user = UserService.update_user(request.user.id, **serializer.validated_data)
|
||||
if user:
|
||||
user_serializer = UserSerializer(user)
|
||||
return Response(user_serializer.data)
|
||||
return Response(
|
||||
{'error': 'Пользователь не найден'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
{"error": "Пользователь не найден"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class ProfileDetailView(generics.RetrieveUpdateAPIView):
|
||||
"""Получение и обновление профиля пользователя"""
|
||||
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = ProfileUpdateSerializer
|
||||
|
||||
@@ -174,17 +167,18 @@ class ProfileDetailView(generics.RetrieveUpdateAPIView):
|
||||
if not profile:
|
||||
# Если профиль не существует, создаем его
|
||||
from .models import Profile
|
||||
|
||||
profile = Profile.objects.create(user=self.request.user)
|
||||
return profile
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
'Authorization',
|
||||
"Authorization",
|
||||
openapi.IN_HEADER,
|
||||
description="Bearer <token>",
|
||||
type=openapi.TYPE_STRING,
|
||||
required=True
|
||||
required=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -197,116 +191,110 @@ class ProfileDetailView(generics.RetrieveUpdateAPIView):
|
||||
request_body=ProfileUpdateSerializer,
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
'Authorization',
|
||||
"Authorization",
|
||||
openapi.IN_HEADER,
|
||||
description="Bearer <token>",
|
||||
type=openapi.TYPE_STRING,
|
||||
required=True
|
||||
required=True,
|
||||
)
|
||||
]
|
||||
],
|
||||
)
|
||||
def patch(self, request, *args, **kwargs):
|
||||
profile = self.get_object()
|
||||
serializer = self.get_serializer(profile, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
updated_profile = ProfileService.update_profile(
|
||||
request.user.id,
|
||||
**serializer.validated_data
|
||||
request.user.id, **serializer.validated_data
|
||||
)
|
||||
if updated_profile:
|
||||
return Response(ProfileUpdateSerializer(updated_profile).data)
|
||||
return Response(
|
||||
{'error': 'Профиль не найден'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
{"error": "Профиль не найден"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class PasswordChangeView(APIView):
|
||||
"""Смена пароля"""
|
||||
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
request_body=PasswordChangeSerializer,
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
'Authorization',
|
||||
"Authorization",
|
||||
openapi.IN_HEADER,
|
||||
description="Bearer <token>",
|
||||
type=openapi.TYPE_STRING,
|
||||
required=True
|
||||
required=True,
|
||||
)
|
||||
],
|
||||
responses={200: 'Пароль успешно изменен'}
|
||||
responses={200: "Пароль успешно изменен"},
|
||||
)
|
||||
def post(self, request):
|
||||
serializer = PasswordChangeSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
user = request.user
|
||||
old_password = serializer.validated_data['old_password']
|
||||
|
||||
old_password = serializer.validated_data["old_password"]
|
||||
|
||||
if check_password(old_password, user.password):
|
||||
new_password = serializer.validated_data['new_password']
|
||||
new_password = serializer.validated_data["new_password"]
|
||||
user.set_password(new_password)
|
||||
user.save()
|
||||
return Response(
|
||||
{'message': 'Пароль успешно изменен'},
|
||||
status=status.HTTP_200_OK
|
||||
{"message": "Пароль успешно изменен"}, status=status.HTTP_200_OK
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
{'error': 'Неверный старый пароль'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
{"error": "Неверный старый пароль"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@api_view(["GET"])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def user_profile_detail(request):
|
||||
"""Получение полных данных профиля пользователя"""
|
||||
profile_data = ProfileService.get_full_profile_data(request.user.id)
|
||||
if profile_data:
|
||||
return Response(profile_data)
|
||||
return Response(
|
||||
{'error': 'Профиль не найден'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Профиль не найден"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
class TokenRefreshView(APIView):
|
||||
"""Обновление access токена через refresh токен"""
|
||||
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@swagger_auto_schema(
|
||||
request_body=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
'refresh': openapi.Schema(type=openapi.TYPE_STRING, description='Refresh token')
|
||||
"refresh": openapi.Schema(
|
||||
type=openapi.TYPE_STRING, description="Refresh token"
|
||||
)
|
||||
},
|
||||
required=['refresh']
|
||||
required=["refresh"],
|
||||
),
|
||||
responses={200: TokenSerializer}
|
||||
responses={200: TokenSerializer},
|
||||
)
|
||||
def post(self, request):
|
||||
refresh_token = request.data.get('refresh')
|
||||
refresh_token = request.data.get("refresh")
|
||||
if not refresh_token:
|
||||
return Response(
|
||||
{'error': 'Refresh token обязателен'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
{"error": "Refresh token обязателен"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
refresh = RefreshToken(refresh_token)
|
||||
return Response({
|
||||
'access': str(refresh.access_token),
|
||||
'refresh': str(refresh)
|
||||
})
|
||||
return Response(
|
||||
{"access": str(refresh.access_token), "refresh": str(refresh)}
|
||||
)
|
||||
except Exception:
|
||||
return Response(
|
||||
{'error': 'Неверный refresh token'},
|
||||
status=status.HTTP_401_UNAUTHORIZED
|
||||
)
|
||||
{"error": "Неверный refresh token"}, status=status.HTTP_401_UNAUTHORIZED
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user