register logic
This commit is contained in:
@@ -10,6 +10,8 @@ python-dotenv = "*"
|
|||||||
psycopg2 = "*"
|
psycopg2 = "*"
|
||||||
pillow = "*"
|
pillow = "*"
|
||||||
transliterate = "*"
|
transliterate = "*"
|
||||||
|
djangorestframework-simplejwt = "*"
|
||||||
|
django-cors-headers = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
|
|
||||||
|
|||||||
28
backend/Pipfile.lock
generated
28
backend/Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "c254f2788ffdf030b1b0fbe90f05c0c47f93704170c42e2607f2a77b22f6ede2"
|
"sha256": "2620449b3502893be81fa9ab722c0beac295dd1dcad39705d6b962eddf45cc21"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -33,6 +33,15 @@
|
|||||||
"markers": "python_version >= '3.10'",
|
"markers": "python_version >= '3.10'",
|
||||||
"version": "==5.2.1"
|
"version": "==5.2.1"
|
||||||
},
|
},
|
||||||
|
"django-cors-headers": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6fdf31bf9c6d6448ba09ef57157db2268d515d94fc5c89a0a1028e1fc03ee52b",
|
||||||
|
"sha256:f1c125dcd58479fe7a67fe2499c16ee38b81b397463cf025f0e2c42937421070"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==4.7.0"
|
||||||
|
},
|
||||||
"djangorestframework": {
|
"djangorestframework": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:bea7e9f6b96a8584c5224bfb2e4348dfb3f8b5e34edbecb98da258e892089361",
|
"sha256:bea7e9f6b96a8584c5224bfb2e4348dfb3f8b5e34edbecb98da258e892089361",
|
||||||
@@ -42,6 +51,15 @@
|
|||||||
"markers": "python_version >= '3.9'",
|
"markers": "python_version >= '3.9'",
|
||||||
"version": "==3.16.0"
|
"version": "==3.16.0"
|
||||||
},
|
},
|
||||||
|
"djangorestframework-simplejwt": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:474a1b737067e6462b3609627a392d13a4da8a08b1f0574104ac6d7b1406f90e",
|
||||||
|
"sha256:4ef6b38af20cdde4a4a51d1fd8e063cbbabb7b45f149cc885d38d905c5a62edb"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==5.5.0"
|
||||||
|
},
|
||||||
"pillow": {
|
"pillow": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928",
|
"sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928",
|
||||||
@@ -147,6 +165,14 @@
|
|||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==2.9.10"
|
"version": "==2.9.10"
|
||||||
},
|
},
|
||||||
|
"pyjwt": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850",
|
||||||
|
"sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==2.9.0"
|
||||||
|
},
|
||||||
"python-dotenv": {
|
"python-dotenv": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5",
|
"sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5",
|
||||||
|
|||||||
0
backend/api/account/__init__.py
Normal file
0
backend/api/account/__init__.py
Normal file
0
backend/api/account/client/__init__.py
Normal file
0
backend/api/account/client/__init__.py
Normal file
0
backend/api/account/client/serializers.py
Normal file
0
backend/api/account/client/serializers.py
Normal file
35
backend/api/account/client/views.py
Normal file
35
backend/api/account/client/views.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.viewsets import ViewSet
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from api.auth.serializers import UserResponseSerializer
|
||||||
|
|
||||||
|
from api.models import UserProfile
|
||||||
|
|
||||||
|
from api.utils.decorators import handle_exceptions
|
||||||
|
|
||||||
|
class UserDataView(ViewSet):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def initial(self, request, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
super().initial(request, *args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Authentication error: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'])
|
||||||
|
@handle_exceptions
|
||||||
|
def user_data(self, request):
|
||||||
|
user = request.user
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_data = UserResponseSerializer(user).data
|
||||||
|
return Response(user_data, status=status.HTTP_200_OK)
|
||||||
|
except UserProfile.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "User profile not found"},
|
||||||
|
status=status.HTTP_404_NOT_FOUND
|
||||||
|
)
|
||||||
@@ -5,7 +5,7 @@ from django.db.utils import IntegrityError
|
|||||||
|
|
||||||
from api.models import UserProfile
|
from api.models import UserProfile
|
||||||
|
|
||||||
class UserResponceSerializer(serializers.Serializer):
|
class UserResponseSerializer(serializers.Serializer):
|
||||||
id = serializers.IntegerField()
|
id = serializers.IntegerField()
|
||||||
email = serializers.EmailField()
|
email = serializers.EmailField()
|
||||||
name = serializers.CharField(source='first_name')
|
name = serializers.CharField(source='first_name')
|
||||||
@@ -24,16 +24,17 @@ class ClientRegisterSerializer(serializers.ModelSerializer):
|
|||||||
phone_number = serializers.CharField(max_length=13)
|
phone_number = serializers.CharField(max_length=13)
|
||||||
privacy_accepted = serializers.BooleanField()
|
privacy_accepted = serializers.BooleanField()
|
||||||
email = serializers.EmailField(required=True)
|
email = serializers.EmailField(required=True)
|
||||||
username = serializers.CharField(source='first_name')
|
name = serializers.CharField(required=True)
|
||||||
|
surname = serializers.CharField(required=True)
|
||||||
uuid = serializers.SerializerMethodField()
|
uuid = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
fields = ['id', 'uuid', 'username', 'email', 'password', 'phone_number', 'privacy_accepted']
|
fields = ['id', 'uuid', 'name', 'surname', 'email', 'password', 'phone_number', 'privacy_accepted']
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'password' : {'write_only':True},
|
'password': {'write_only': True},
|
||||||
'email' : {'required' : True, 'unique': True},
|
'email': {'required': True},
|
||||||
'phone_number' : {'required' : True, 'unique': True},
|
'phone_number': {'required': True},
|
||||||
}
|
}
|
||||||
|
|
||||||
def validate_phone_number(self, value):
|
def validate_phone_number(self, value):
|
||||||
@@ -44,18 +45,21 @@ class ClientRegisterSerializer(serializers.ModelSerializer):
|
|||||||
def validate_privacy_accepted(self, value):
|
def validate_privacy_accepted(self, value):
|
||||||
if not value:
|
if not value:
|
||||||
raise serializers.ValidationError("Необходимо принять условия политики конфиденциальности")
|
raise serializers.ValidationError("Необходимо принять условия политики конфиденциальности")
|
||||||
|
return value
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
privacy_accepted = validated_data.pop('privacy_accepted')
|
privacy_accepted = validated_data.pop('privacy_accepted')
|
||||||
phone_number = validated_data.pop('phone_number')
|
phone_number = validated_data.pop('phone_number')
|
||||||
name = validated_data.pop('first_name')
|
name = validated_data.pop('name')
|
||||||
|
surname = validated_data.pop('surname')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = User.objects.create_user(
|
user = User.objects.create_user(
|
||||||
username=validated_data['name'],
|
username=validated_data['email'], # используем email как username
|
||||||
email=validated_data['email'],
|
email=validated_data['email'],
|
||||||
password=validated_data['password'],
|
password=validated_data['password'],
|
||||||
first_name = name,
|
first_name=name,
|
||||||
|
last_name=surname
|
||||||
)
|
)
|
||||||
|
|
||||||
UserProfile.objects.create(
|
UserProfile.objects.create(
|
||||||
|
|||||||
@@ -1,6 +1,171 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import status
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
import traceback
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.conf import settings
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.db import IntegrityError, transaction
|
||||||
|
|
||||||
|
from .serializers import ClientRegisterSerializer, UserResponseSerializer
|
||||||
|
|
||||||
|
from api.utils.cookiesSet import AuthBaseViewSet
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterViewSet(AuthBaseViewSet):
|
||||||
|
"""Регистрация клиента"""
|
||||||
|
@action(detail=False, methods=['post'], url_path="clients")
|
||||||
|
def register_client(self, request):
|
||||||
|
serializer = ClientRegisterSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
user = serializer.save()
|
||||||
|
refresh = RefreshToken.for_user(user)
|
||||||
|
|
||||||
|
user_data = UserResponseSerializer(user).data
|
||||||
|
|
||||||
|
response = Response(
|
||||||
|
{
|
||||||
|
"access": str(refresh.access_token),
|
||||||
|
"refresh": str(refresh),
|
||||||
|
"user": user_data
|
||||||
|
},
|
||||||
|
status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
|
|
||||||
|
# используем метод из базового класса вместо прямой установки куки
|
||||||
|
return self._set_auth_cookies(response, refresh)
|
||||||
|
except IntegrityError as e:
|
||||||
|
return Response(
|
||||||
|
{"error": "Пользователь с таким email уже существует"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return Response(
|
||||||
|
{"error": str(e)},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
# ошибка валидации
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "Ошибка валидации",
|
||||||
|
"details": serializer.errors
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class LoginViewSet(AuthBaseViewSet):
|
||||||
|
"""Логин для клиента"""
|
||||||
|
|
||||||
|
@action(detail=False, methods=['post'], url_path="clients")
|
||||||
|
def login_client(self, request):
|
||||||
|
try:
|
||||||
|
email = request.data.get("email")
|
||||||
|
password = request.data.get("password")
|
||||||
|
|
||||||
|
if not email or not password:
|
||||||
|
return Response(
|
||||||
|
{"error": "Email и пароль обязательны"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = User.objects.get(email=email)
|
||||||
|
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
|
||||||
|
user_data["userType"] = "client"
|
||||||
|
|
||||||
|
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):
|
||||||
|
"""Логаут"""
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
class RefreshTokenView(APIView):
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
refresh_token = request.data.get('refresh')
|
||||||
|
|
||||||
|
if not refresh_token:
|
||||||
|
return Response(
|
||||||
|
{'error': 'Refresh token is required'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
token = RefreshToken(refresh_token)
|
||||||
|
|
||||||
|
# Сохраняем user_type при обновлении токена
|
||||||
|
if 'user_type' in token:
|
||||||
|
token.access_token['user_type'] = token['user_type']
|
||||||
|
|
||||||
|
# Добавляем точное время истечения токена
|
||||||
|
expires_at = datetime.now() + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME']
|
||||||
|
|
||||||
|
response_data = {
|
||||||
|
'access': str(token.access_token),
|
||||||
|
'refresh': str(token),
|
||||||
|
'expires_at': datetime.timestamp(expires_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response(response_data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Более подробное логирование ошибок
|
||||||
|
|
||||||
|
print(f"Token refresh error: {str(e)}")
|
||||||
|
print(traceback.format_exc())
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{'error': f'Invalid refresh token: {str(e)}'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return Response(
|
||||||
|
{'error': f'Token refresh failed: {str(e)}'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
@@ -17,4 +17,4 @@ class UserProfile(models.Model):
|
|||||||
additionalDetails = models.TextField(null=True, blank=True, verbose_name="Дополнительные детали")
|
additionalDetails = models.TextField(null=True, blank=True, verbose_name="Дополнительные детали")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return {self.phone_number}
|
return str(self.phone_number)
|
||||||
@@ -2,7 +2,22 @@ from django.urls import path
|
|||||||
|
|
||||||
from api.main.views import FAQView, NewsView
|
from api.main.views import FAQView, NewsView
|
||||||
|
|
||||||
|
from api.auth.views import (
|
||||||
|
RegisterViewSet,
|
||||||
|
LoginViewSet,
|
||||||
|
LogoutView,
|
||||||
|
RefreshTokenView)
|
||||||
|
|
||||||
|
from api.account.client.views import UserDataView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("v1/faq/", FAQView.as_view(), name='faqMain'),
|
path("v1/faq/", FAQView.as_view(), name='faqMain'),
|
||||||
path("v1/news/", NewsView.as_view(), name="newsmain")
|
path("v1/news/", NewsView.as_view(), name="newsmain"),
|
||||||
|
|
||||||
|
path("auth/refresh/", RefreshTokenView.as_view(), name="token-refresh"),
|
||||||
|
path("register/clients/", RegisterViewSet.as_view({'post': 'register_client'}), name="register-client"),
|
||||||
|
path("auth/login/clients/", LoginViewSet.as_view({'post': 'login_client'}), name="login-client"),
|
||||||
|
path("v1/logout", LogoutView.as_view(), name="logout"),
|
||||||
|
|
||||||
|
path ("v1/user/", UserDataView.as_view({'get': 'user_data'}), name="user"),
|
||||||
]
|
]
|
||||||
65
backend/api/utils/cookiesSet.py
Normal file
65
backend/api/utils/cookiesSet.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
from rest_framework.viewsets import ViewSet
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from datetime import datetime
|
||||||
|
from django.conf import settings
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
|
||||||
|
class AuthBaseViewSet(ViewSet):
|
||||||
|
"""Базовый класс для аутентификации с общими методами"""
|
||||||
|
|
||||||
|
def _set_auth_cookies(self, response, refresh):
|
||||||
|
"""Устанавливает куки для токенов аутентификации"""
|
||||||
|
response.set_cookie(
|
||||||
|
'access_token',
|
||||||
|
str(refresh.access_token),
|
||||||
|
httponly=True,
|
||||||
|
secure=True,
|
||||||
|
samesite='Lax',
|
||||||
|
max_age=300
|
||||||
|
)
|
||||||
|
response.set_cookie(
|
||||||
|
'refresh_token',
|
||||||
|
str(refresh),
|
||||||
|
httponly=True,
|
||||||
|
secure=True,
|
||||||
|
samesite='Lax',
|
||||||
|
max_age=86400
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
@action(detail=False, methods=['post'], url_path="refresh")
|
||||||
|
def refresh_token(self, request):
|
||||||
|
try:
|
||||||
|
refresh_token = request.data.get('refresh')
|
||||||
|
|
||||||
|
if not refresh_token:
|
||||||
|
return Response(
|
||||||
|
{'error': 'Refresh token is required'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
token = RefreshToken(refresh_token)
|
||||||
|
response_data = {
|
||||||
|
'access': str(token.access_token),
|
||||||
|
'refresh': str(token),
|
||||||
|
'expires_at': datetime.timestamp(
|
||||||
|
datetime.now() + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME']
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response(response_data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return Response(
|
||||||
|
{'error': f'Invalid refresh token: {str(e)}'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return Response(
|
||||||
|
{'error': f'Token refresh failed: {str(e)}'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
load_dotenv(dotenv_path=BASE_DIR / './.env')
|
load_dotenv(dotenv_path=BASE_DIR / './.env')
|
||||||
@@ -8,11 +9,16 @@ load_dotenv(dotenv_path=BASE_DIR / './.env')
|
|||||||
BASE_URL = os.environ.get("BASE_URL")
|
BASE_URL = os.environ.get("BASE_URL")
|
||||||
MEDIA_URL = '/media/'
|
MEDIA_URL = '/media/'
|
||||||
MEDIA_ROOT = os.environ.get("MEDIA_ROOT")
|
MEDIA_ROOT = os.environ.get("MEDIA_ROOT")
|
||||||
# MEDIA_ROOT = '/root/tripwb/uploads/' -- закинь в .env.production
|
# !MEDIA_ROOT = '/root/tripwb/uploads/' -- закинь в .env.production
|
||||||
|
|
||||||
SECRET_KEY = os.environ.get("SECRET_KEY")
|
SECRET_KEY = os.environ.get("SECRET_KEY")
|
||||||
DEBUG = os.environ.get("DEBUG_MODE")
|
DEBUG = os.environ.get("DEBUG_MODE")
|
||||||
|
|
||||||
|
GOOGLE_CLIENT_ID = os.environ.get("CLIENT_ID")
|
||||||
|
|
||||||
|
BOT_TOKEN = os.environ.get("BOT_TOKEN")
|
||||||
|
CHAT_ID = os.environ.get("CHAT_ID")
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['localhost', 'tripwb.com', '127.0.0.1', 'tripwb-backend-app', 'v2.tripwb.com']
|
ALLOWED_HOSTS = ['localhost', 'tripwb.com', '127.0.0.1', 'tripwb-backend-app', 'v2.tripwb.com']
|
||||||
|
|
||||||
CSRF_TRUSTED_ORIGINS = [
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
@@ -22,6 +28,7 @@ CSRF_TRUSTED_ORIGINS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
'corsheaders',
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
'django.contrib.contenttypes',
|
'django.contrib.contenttypes',
|
||||||
@@ -32,9 +39,11 @@ INSTALLED_APPS = [
|
|||||||
'routes.apps.RoutesConfig',
|
'routes.apps.RoutesConfig',
|
||||||
'sitemanagement.apps.SitemanagementConfig',
|
'sitemanagement.apps.SitemanagementConfig',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
|
"rest_framework_simplejwt",
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
'corsheaders.middleware.CorsMiddleware',
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
@@ -44,6 +53,20 @@ MIDDLEWARE = [
|
|||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
SIMPLE_JWT = {
|
||||||
|
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
|
||||||
|
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
|
||||||
|
'ROTATE_REFRESH_TOKENS': True,
|
||||||
|
'BLACKLIST_AFTER_ROTATION': True,
|
||||||
|
'UPDATE_LAST_LOGIN': True,
|
||||||
|
'ALGORITHM': 'HS256',
|
||||||
|
'SIGNING_KEY': SECRET_KEY,
|
||||||
|
'VERIFYING_KEY': None,
|
||||||
|
'AUTH_HEADER_TYPES': ('Bearer',),
|
||||||
|
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
|
||||||
|
}
|
||||||
|
|
||||||
ROOT_URLCONF = 'base.urls'
|
ROOT_URLCONF = 'base.urls'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
@@ -74,8 +97,7 @@ DATABASES = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BOT_TOKEN = os.environ.get("BOT_TOKEN")
|
|
||||||
CHAT_ID = os.environ.get("CHAT_ID")
|
|
||||||
|
|
||||||
CORS_ALLOW_CREDENTIALS = True # для разрешения cookie
|
CORS_ALLOW_CREDENTIALS = True # для разрешения cookie
|
||||||
|
|
||||||
|
|||||||
11
backend/requirements.txt
Normal file
11
backend/requirements.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
asgiref==3.8.1
|
||||||
|
Django==5.2.1
|
||||||
|
djangorestframework==3.16.0
|
||||||
|
djangorestframework_simplejwt==5.5.0
|
||||||
|
pillow==11.2.1
|
||||||
|
psycopg2==2.9.10
|
||||||
|
PyJWT==2.9.0
|
||||||
|
python-dotenv==1.1.0
|
||||||
|
six==1.17.0
|
||||||
|
sqlparse==0.5.3
|
||||||
|
transliterate==1.10.2
|
||||||
7
frontend/app/(urls)/account/page.tsx
Normal file
7
frontend/app/(urls)/account/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const page = () => {
|
||||||
|
return <div>page</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default page
|
||||||
@@ -43,7 +43,8 @@ export default function ClientRegistrationForm() {
|
|||||||
const result = await signIn('register-credentials', {
|
const result = await signIn('register-credentials', {
|
||||||
email: values.email,
|
email: values.email,
|
||||||
password: values.password,
|
password: values.password,
|
||||||
username: values.name,
|
name: values.name,
|
||||||
|
surname: values.surname,
|
||||||
phone_number: values.phone_number,
|
phone_number: values.phone_number,
|
||||||
privacy_accepted: values.privacy_accepted.toString(),
|
privacy_accepted: values.privacy_accepted.toString(),
|
||||||
redirect: false,
|
redirect: false,
|
||||||
|
|||||||
@@ -45,35 +45,56 @@ interface GoogleToken extends JWT {
|
|||||||
const authOptions: NextAuthOptions = {
|
const authOptions: NextAuthOptions = {
|
||||||
providers: [
|
providers: [
|
||||||
//google login flow
|
//google login flow
|
||||||
GoogleProvider({
|
// GoogleProvider({
|
||||||
clientId: process.env.GOOGLE_CLIENT_ID!,
|
// clientId: process.env.GOOGLE_CLIENT_ID!,
|
||||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
// clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
||||||
}),
|
// }),
|
||||||
|
|
||||||
//регистрация клиента
|
//регистрация клиента
|
||||||
CredentialsProvider({
|
CredentialsProvider({
|
||||||
id: 'register-credentials',
|
id: 'register-credentials',
|
||||||
name: 'Register',
|
name: 'Register',
|
||||||
credentials: {
|
credentials: {
|
||||||
|
name: { label: 'Name', type: 'text' },
|
||||||
|
surname: { label: 'Surname', type: 'text' },
|
||||||
email: { label: 'Email', type: 'email' },
|
email: { label: 'Email', type: 'email' },
|
||||||
password: { label: 'Password', type: 'password' },
|
password: { label: 'Password', type: 'password' },
|
||||||
name: { label: 'Name', type: 'text' },
|
|
||||||
phone_number: { label: 'Phone Number', type: 'tel' },
|
phone_number: { label: 'Phone Number', type: 'tel' },
|
||||||
privacy_accepted: { label: 'Privacy Accepted', type: 'boolean' },
|
privacy_accepted: { label: 'Privacy Accepted', type: 'boolean' },
|
||||||
},
|
},
|
||||||
async authorize(credentials) {
|
async authorize(credentials) {
|
||||||
try {
|
try {
|
||||||
|
if (
|
||||||
|
!credentials?.email ||
|
||||||
|
!credentials?.password ||
|
||||||
|
!credentials?.name ||
|
||||||
|
!credentials?.phone_number ||
|
||||||
|
!credentials?.privacy_accepted ||
|
||||||
|
!credentials?.surname
|
||||||
|
) {
|
||||||
|
throw new Error('Все поля обязательны для заполнения')
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('Registration data:', {
|
||||||
|
// email: credentials.email,
|
||||||
|
// name: credentials.name,
|
||||||
|
// surname: credentials.surname,
|
||||||
|
// phone_number: credentials.phone_number,
|
||||||
|
// privacy_accepted: credentials.privacy_accepted,
|
||||||
|
// })
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${process.env.BACKEND_URL}/register/clients/`,
|
`${process.env.BACKEND_URL}/register/clients/`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
email: credentials?.email,
|
email: credentials.email,
|
||||||
password: credentials?.password,
|
password: credentials.password,
|
||||||
name: credentials?.name,
|
name: credentials.name,
|
||||||
phone_number: credentials?.phone_number,
|
surname: credentials.surname,
|
||||||
privacy_accepted: credentials?.privacy_accepted === 'true',
|
phone_number: credentials.phone_number,
|
||||||
|
privacy_accepted: credentials.privacy_accepted === 'true',
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -81,21 +102,25 @@ const authOptions: NextAuthOptions = {
|
|||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(
|
console.error('Registration error response:', data)
|
||||||
data.error || data.details?.toString() || 'Registration failed'
|
const errorMessage =
|
||||||
)
|
typeof data === 'object'
|
||||||
|
? data.error || Object.values(data).flat().join(', ')
|
||||||
|
: 'Registration failed'
|
||||||
|
throw new Error(errorMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: data.user.id.toString(),
|
id: data.user.id.toString(),
|
||||||
email: data.user.email,
|
email: data.user.email,
|
||||||
name: data.user.firstName,
|
name: data.user.name,
|
||||||
|
surname: data.user.surname,
|
||||||
accessToken: data.access,
|
accessToken: data.access,
|
||||||
refreshToken: data.refresh,
|
refreshToken: data.refresh,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Registration error:', error)
|
console.error('Registration error:', error)
|
||||||
return null
|
throw error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import type { Metadata } from 'next'
|
|||||||
import './globals.css'
|
import './globals.css'
|
||||||
import Header from '@/components/Header'
|
import Header from '@/components/Header'
|
||||||
import Footer from '@/components/Footer'
|
import Footer from '@/components/Footer'
|
||||||
|
import { Toaster } from 'react-hot-toast'
|
||||||
|
import { Providers } from './providers/Providers'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Отправка посылок в любую точку мира | TripWB',
|
title: 'Отправка посылок в любую точку мира | TripWB',
|
||||||
@@ -17,9 +19,12 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className="min-h-screen flex flex-col font-dispay">
|
<body className="min-h-screen flex flex-col font-dispay">
|
||||||
<Header />
|
<Providers>
|
||||||
<main className="flex-grow">{children}</main>
|
<Header />
|
||||||
<Footer />
|
<main className="flex-grow">{children}</main>
|
||||||
|
<Toaster />
|
||||||
|
<Footer />
|
||||||
|
</Providers>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
|||||||
62
frontend/app/providers/AuthProvider.tsx
Normal file
62
frontend/app/providers/AuthProvider.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useSession } from 'next-auth/react'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import useUserStore from '@/app/store/userStore'
|
||||||
|
|
||||||
|
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const { data: session } = useSession()
|
||||||
|
const { setUser, setAuthenticated } = useUserStore()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUserData = async () => {
|
||||||
|
if (!session?.accessToken) {
|
||||||
|
setUser(null)
|
||||||
|
setAuthenticated(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const API_URL = process.env.NEXT_PUBLIC_API_URL
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/user/`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${session.accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text()
|
||||||
|
console.error('Error response:', errorText)
|
||||||
|
throw new Error(
|
||||||
|
`Error fetching user data: ${response.status} ${errorText}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = await response.json()
|
||||||
|
|
||||||
|
setUser({
|
||||||
|
id: userData.id,
|
||||||
|
uuid: userData.uuid,
|
||||||
|
name: userData.name || session.user.name || '',
|
||||||
|
surname: userData.surname || '',
|
||||||
|
email: userData.email || session.user.email || '',
|
||||||
|
phone_number: userData.phone_number,
|
||||||
|
image: userData.image,
|
||||||
|
country: userData.country,
|
||||||
|
city: userData.city,
|
||||||
|
plan: userData.plan,
|
||||||
|
account_type: userData.account_type,
|
||||||
|
})
|
||||||
|
setAuthenticated(true)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in fetchUserData:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchUserData()
|
||||||
|
}, [session, setUser, setAuthenticated])
|
||||||
|
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
17
frontend/app/providers/Providers.tsx
Normal file
17
frontend/app/providers/Providers.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { SessionProvider } from 'next-auth/react'
|
||||||
|
import { AuthProvider } from './AuthProvider'
|
||||||
|
|
||||||
|
export function Providers({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<SessionProvider
|
||||||
|
refetchInterval={0}
|
||||||
|
refetchOnWindowFocus={false}
|
||||||
|
refetchWhenOffline={false}
|
||||||
|
basePath="/api/auth"
|
||||||
|
>
|
||||||
|
<AuthProvider>{children}</AuthProvider>
|
||||||
|
</SessionProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user