register logic

This commit is contained in:
2025-05-18 13:37:27 +03:00
parent be46e09aeb
commit 695c29ab62
19 changed files with 498 additions and 36 deletions

View File

@@ -10,6 +10,8 @@ python-dotenv = "*"
psycopg2 = "*"
pillow = "*"
transliterate = "*"
djangorestframework-simplejwt = "*"
django-cors-headers = "*"
[dev-packages]

28
backend/Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "c254f2788ffdf030b1b0fbe90f05c0c47f93704170c42e2607f2a77b22f6ede2"
"sha256": "2620449b3502893be81fa9ab722c0beac295dd1dcad39705d6b962eddf45cc21"
},
"pipfile-spec": 6,
"requires": {
@@ -33,6 +33,15 @@
"markers": "python_version >= '3.10'",
"version": "==5.2.1"
},
"django-cors-headers": {
"hashes": [
"sha256:6fdf31bf9c6d6448ba09ef57157db2268d515d94fc5c89a0a1028e1fc03ee52b",
"sha256:f1c125dcd58479fe7a67fe2499c16ee38b81b397463cf025f0e2c42937421070"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==4.7.0"
},
"djangorestframework": {
"hashes": [
"sha256:bea7e9f6b96a8584c5224bfb2e4348dfb3f8b5e34edbecb98da258e892089361",
@@ -42,6 +51,15 @@
"markers": "python_version >= '3.9'",
"version": "==3.16.0"
},
"djangorestframework-simplejwt": {
"hashes": [
"sha256:474a1b737067e6462b3609627a392d13a4da8a08b1f0574104ac6d7b1406f90e",
"sha256:4ef6b38af20cdde4a4a51d1fd8e063cbbabb7b45f149cc885d38d905c5a62edb"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==5.5.0"
},
"pillow": {
"hashes": [
"sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928",
@@ -147,6 +165,14 @@
"markers": "python_version >= '3.8'",
"version": "==2.9.10"
},
"pyjwt": {
"hashes": [
"sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850",
"sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"
],
"markers": "python_version >= '3.8'",
"version": "==2.9.0"
},
"python-dotenv": {
"hashes": [
"sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5",

View File

View File

View 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
)

View File

@@ -5,7 +5,7 @@ from django.db.utils import IntegrityError
from api.models import UserProfile
class UserResponceSerializer(serializers.Serializer):
class UserResponseSerializer(serializers.Serializer):
id = serializers.IntegerField()
email = serializers.EmailField()
name = serializers.CharField(source='first_name')
@@ -24,16 +24,17 @@ class ClientRegisterSerializer(serializers.ModelSerializer):
phone_number = serializers.CharField(max_length=13)
privacy_accepted = serializers.BooleanField()
email = serializers.EmailField(required=True)
username = serializers.CharField(source='first_name')
name = serializers.CharField(required=True)
surname = serializers.CharField(required=True)
uuid = serializers.SerializerMethodField()
class Meta:
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 = {
'password' : {'write_only':True},
'email' : {'required' : True, 'unique': True},
'phone_number' : {'required' : True, 'unique': True},
'password': {'write_only': True},
'email': {'required': True},
'phone_number': {'required': True},
}
def validate_phone_number(self, value):
@@ -44,18 +45,21 @@ class ClientRegisterSerializer(serializers.ModelSerializer):
def validate_privacy_accepted(self, value):
if not value:
raise serializers.ValidationError("Необходимо принять условия политики конфиденциальности")
return value
def create(self, validated_data):
privacy_accepted = validated_data.pop('privacy_accepted')
phone_number = validated_data.pop('phone_number')
name = validated_data.pop('first_name')
name = validated_data.pop('name')
surname = validated_data.pop('surname')
try:
user = User.objects.create_user(
username=validated_data['name'],
username=validated_data['email'], # используем email как username
email=validated_data['email'],
password=validated_data['password'],
first_name = name,
first_name=name,
last_name=surname
)
UserProfile.objects.create(

View File

@@ -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.conf import settings
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
)

View File

@@ -17,4 +17,4 @@ class UserProfile(models.Model):
additionalDetails = models.TextField(null=True, blank=True, verbose_name="Дополнительные детали")
def __str__(self):
return {self.phone_number}
return str(self.phone_number)

View File

@@ -2,7 +2,22 @@ from django.urls import path
from api.main.views import FAQView, NewsView
from api.auth.views import (
RegisterViewSet,
LoginViewSet,
LogoutView,
RefreshTokenView)
from api.account.client.views import UserDataView
urlpatterns = [
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"),
]

View 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
)

View File

@@ -1,6 +1,7 @@
import os
from pathlib import Path
from dotenv import load_dotenv
from datetime import timedelta
BASE_DIR = Path(__file__).resolve().parent.parent
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")
MEDIA_URL = '/media/'
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")
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']
CSRF_TRUSTED_ORIGINS = [
@@ -22,6 +28,7 @@ CSRF_TRUSTED_ORIGINS = [
]
INSTALLED_APPS = [
'corsheaders',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
@@ -32,9 +39,11 @@ INSTALLED_APPS = [
'routes.apps.RoutesConfig',
'sitemanagement.apps.SitemanagementConfig',
'rest_framework',
"rest_framework_simplejwt",
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
@@ -44,6 +53,20 @@ MIDDLEWARE = [
'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'
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

11
backend/requirements.txt Normal file
View 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

View File

@@ -0,0 +1,7 @@
import React from 'react'
const page = () => {
return <div>page</div>
}
export default page

View File

@@ -43,7 +43,8 @@ export default function ClientRegistrationForm() {
const result = await signIn('register-credentials', {
email: values.email,
password: values.password,
username: values.name,
name: values.name,
surname: values.surname,
phone_number: values.phone_number,
privacy_accepted: values.privacy_accepted.toString(),
redirect: false,

View File

@@ -45,35 +45,56 @@ interface GoogleToken extends JWT {
const authOptions: NextAuthOptions = {
providers: [
//google login flow
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
// GoogleProvider({
// clientId: process.env.GOOGLE_CLIENT_ID!,
// clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
// }),
//регистрация клиента
CredentialsProvider({
id: 'register-credentials',
name: 'Register',
credentials: {
name: { label: 'Name', type: 'text' },
surname: { label: 'Surname', type: 'text' },
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
name: { label: 'Name', type: 'text' },
phone_number: { label: 'Phone Number', type: 'tel' },
privacy_accepted: { label: 'Privacy Accepted', type: 'boolean' },
},
async authorize(credentials) {
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(
`${process.env.BACKEND_URL}/register/clients/`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: credentials?.email,
password: credentials?.password,
name: credentials?.name,
phone_number: credentials?.phone_number,
privacy_accepted: credentials?.privacy_accepted === 'true',
email: credentials.email,
password: credentials.password,
name: credentials.name,
surname: credentials.surname,
phone_number: credentials.phone_number,
privacy_accepted: credentials.privacy_accepted === 'true',
}),
}
)
@@ -81,21 +102,25 @@ const authOptions: NextAuthOptions = {
const data = await res.json()
if (!res.ok) {
throw new Error(
data.error || data.details?.toString() || 'Registration failed'
)
console.error('Registration error response:', data)
const errorMessage =
typeof data === 'object'
? data.error || Object.values(data).flat().join(', ')
: 'Registration failed'
throw new Error(errorMessage)
}
return {
id: data.user.id.toString(),
email: data.user.email,
name: data.user.firstName,
name: data.user.name,
surname: data.user.surname,
accessToken: data.access,
refreshToken: data.refresh,
}
} catch (error) {
console.error('Registration error:', error)
return null
throw error
}
},
}),

View File

@@ -2,6 +2,8 @@ import type { Metadata } from 'next'
import './globals.css'
import Header from '@/components/Header'
import Footer from '@/components/Footer'
import { Toaster } from 'react-hot-toast'
import { Providers } from './providers/Providers'
export const metadata: Metadata = {
title: 'Отправка посылок в любую точку мира | TripWB',
@@ -17,9 +19,12 @@ export default function RootLayout({
return (
<html lang="en">
<body className="min-h-screen flex flex-col font-dispay">
<Header />
<main className="flex-grow">{children}</main>
<Footer />
<Providers>
<Header />
<main className="flex-grow">{children}</main>
<Toaster />
<Footer />
</Providers>
</body>
</html>
)

View 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}</>
}

View 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>
)
}