diff --git a/api/__pycache__/models.cpython-313.pyc b/api/__pycache__/models.cpython-313.pyc index e6a5112..2531206 100644 Binary files a/api/__pycache__/models.cpython-313.pyc and b/api/__pycache__/models.cpython-313.pyc differ diff --git a/api/__pycache__/urls.cpython-313.pyc b/api/__pycache__/urls.cpython-313.pyc index 597ee42..e6fab15 100644 Binary files a/api/__pycache__/urls.cpython-313.pyc and b/api/__pycache__/urls.cpython-313.pyc differ diff --git a/api/__pycache__/views.cpython-313.pyc b/api/__pycache__/views.cpython-313.pyc index 0c47532..f3d50be 100644 Binary files a/api/__pycache__/views.cpython-313.pyc and b/api/__pycache__/views.cpython-313.pyc differ diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py index 8c138ed..26e6db6 100644 --- a/api/migrations/0001_initial.py +++ b/api/migrations/0001_initial.py @@ -1,9 +1,12 @@ -# Generated by Django 4.2.20 on 2025-04-05 15:24 +# Generated by Django 5.1.7 on 2025-04-05 16:49 +import django.contrib.auth.models +import django.contrib.auth.validators +import django.db.models.deletion +import django.utils.timezone +import uuid from django.conf import settings from django.db import migrations, models -import django.db.models.deletion -import uuid class Migration(migrations.Migration): @@ -11,20 +14,10 @@ class Migration(migrations.Migration): initial = True dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('auth', '0012_alter_user_first_name_max_length'), ] operations = [ - migrations.CreateModel( - name='Prediction', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('result', models.JSONField()), - ('deleted_at', models.DateTimeField(blank=True, null=True)), - ], - ), migrations.CreateModel( name='Satellite', fields=[ @@ -33,18 +26,41 @@ class Migration(migrations.Migration): ], ), migrations.CreateModel( - name='User', + name='Prediction', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('result', models.JSONField()), + ('deleted_at', models.DateTimeField(blank=True, null=True)), + ('satellite', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='predictions', to='api.satellite')), ], ), migrations.CreateModel( - name='UserPrediction', + name='User', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField()), - ('prediction', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.prediction')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ('satellites', models.ManyToManyField(related_name='users', to='api.satellite')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), ], ), migrations.CreateModel( @@ -60,4 +76,13 @@ class Migration(migrations.Migration): ('satellite', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='telemetry', to='api.satellite')), ], ), + migrations.CreateModel( + name='UserPrediction', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField()), + ('prediction', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.prediction')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), ] diff --git a/api/models.py b/api/models.py index 3bb0c21..bb29c77 100644 --- a/api/models.py +++ b/api/models.py @@ -1,10 +1,14 @@ import uuid from django.db import models from django.contrib.auth import get_user_model -class User(models.Model): - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) +from django.contrib.auth.models import AbstractUser + +class User(AbstractUser): + satellites = models.ManyToManyField("Satellite", related_name="users") + class Prediction(models.Model): + satellite = models.ForeignKey('Satellite', on_delete=models.CASCADE, related_name='predictions', null=True) id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -12,7 +16,6 @@ class Prediction(models.Model): deleted_at = models.DateTimeField(null=True, blank=True) - class UserPrediction(models.Model): user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) prediction = models.ForeignKey("Prediction", on_delete=models.CASCADE) diff --git a/api/urls.py b/api/urls.py index 74dfdab..32a2a2b 100644 --- a/api/urls.py +++ b/api/urls.py @@ -12,7 +12,7 @@ from rest_framework.authtoken.views import obtain_auth_token from .views import TelemetryListCreateView urlpatterns = [ path('predictions', PredictionCreateView.as_view(), name='create_prediction'), - path('predictions', PredictionListView.as_view(), name='get_predictions'), + path('predictions/list/', PredictionListView.as_view(), name='get_predictions'), path('token', obtain_auth_token, name = 'get_token'), path("history", PredictionHistoryListView.as_view(), name='view_history_list'), path("history//", PredictionHistoryDetailView.as_view(), name='view_history_detail'), diff --git a/api/views.py b/api/views.py index a0e4d91..8c1469c 100644 --- a/api/views.py +++ b/api/views.py @@ -20,6 +20,9 @@ from rest_framework.authentication import SessionAuthentication, BasicAuthentica from django.contrib.auth import authenticate, login, logout import json from django.middleware.csrf import get_token +from rest_framework.decorators import api_view, permission_classes, authentication_classes +from drf_spectacular.utils import extend_schema +from django.utils.dateparse import parse_datetime User = get_user_model() @@ -50,7 +53,6 @@ class PredictionCreateView(APIView): try: prediction_result = TawhiriClient.get_prediction(validated_data) - print(prediction_result) except requests.RequestException as e: print("Tawhiri error:", str(e), e.response.text if e.response else "no response") return Response({"error": f"Tawhiri error: {str(e)}"}, status=status.HTTP_502_BAD_GATEWAY) @@ -65,19 +67,35 @@ class PredictionCreateView(APIView): class PredictionListView(APIView): + permission_classes = [IsAuthenticated] + def get(self, request): - user_id = request.query_params.get('user_id') + user = request.user + satellite_id = request.query_params.get('satellite_id') created_from = request.query_params.get('created_from') created_till = request.query_params.get('created_till') - predictions = Prediction.objects.filter( - id__in=UserPrediction.objects.filter(user_id=user_id).values_list('prediction_id'), - created_at__gte=created_from, - created_at__lte=created_till, - deleted_at__isnull=True - ) - return Response(PredictionSerializer(predictions, many=True).data) + # Проверка доступа к спутнику + if satellite_id and not user.satellites.filter(id=satellite_id).exists(): + return Response({'detail': 'Access to this satellite is forbidden.'}, status=403) + filters = { + 'id__in': UserPrediction.objects.filter(user=user).values_list('prediction_id', flat=True), + 'deleted_at__isnull': True + } + print(filters) + print(Prediction.objects.filter(**filters)) + if created_from: + filters['created_at__gte'] = parse_datetime(created_from) + if created_till: + filters['created_at__lte'] = parse_datetime(created_till) + if satellite_id: + filters['satellite_id'] = satellite_id + + predictions = Prediction.objects.filter(**filters) + return Response(PredictionSerializer(predictions, many=True).data) + + class PredictionHistoryListView(generics.ListAPIView): permission_classes = [permissions.IsAuthenticated] @@ -172,12 +190,20 @@ class WhoAmIView(APIView): return JsonResponse({'username': request.user.username}) +@extend_schema(methods=["GET"], description="Get CSRF token") +@api_view(["GET"]) +@permission_classes([AllowAny]) def get_csrf(request): response = JsonResponse({'detail': 'CSRF cookie set'}) response['X-CSRFToken'] = get_token(request) return response +@extend_schema(methods=["POST"], description="Login user") +@csrf_exempt +@api_view(["POST"]) +@authentication_classes([]) +@permission_classes([AllowAny]) def login_view(request): data = json.loads(request.body) username = data.get('username') @@ -187,7 +213,6 @@ def login_view(request): return JsonResponse({'detail': 'Please provide username and password.'}, status=400) user = authenticate(username=username, password=password) - if user is None: return JsonResponse({'detail': 'Invalid credentials.'}, status=400) @@ -195,6 +220,9 @@ def login_view(request): return JsonResponse({'detail': 'Successfully logged in.'}) +@extend_schema(methods=["POST"], description="Logout user") +@api_view(["POST"]) +@permission_classes([AllowAny]) def logout_view(request): if not request.user.is_authenticated: return JsonResponse({'detail': 'You\'re not logged in.'}, status=400) diff --git a/testapi/__pycache__/settings.cpython-311.pyc b/testapi/__pycache__/settings.cpython-311.pyc index 92a452d..db428a7 100644 Binary files a/testapi/__pycache__/settings.cpython-311.pyc and b/testapi/__pycache__/settings.cpython-311.pyc differ diff --git a/testapi/__pycache__/settings.cpython-313.pyc b/testapi/__pycache__/settings.cpython-313.pyc index 6e62f8d..41aa746 100644 Binary files a/testapi/__pycache__/settings.cpython-313.pyc and b/testapi/__pycache__/settings.cpython-313.pyc differ diff --git a/testapi/settings.py b/testapi/settings.py index 2d38109..d8715c1 100644 --- a/testapi/settings.py +++ b/testapi/settings.py @@ -89,16 +89,24 @@ WSGI_APPLICATION = 'testapi.wsgi.application' # Database # https://docs.djangoproject.com/en/5.1/ref/settings/#databases -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.environ.get("DB_NAME", "drfapi"), - 'USER': os.environ.get("DB_USER", "postgres"), - 'PASSWORD': os.environ.get("DB_PASSWORD", "1235"), - 'HOST': os.environ.get("DB_HOST", "localhost"), - 'PORT': os.environ.get("DB_PORT", "5432"), +if DEBUG: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } + } +else: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'drfapi', # Your database name + 'USER': 'postgres', # Your PostgreSQL username + 'PASSWORD': '1235', # Your PostgreSQL password + 'HOST': 'localhost', # Or your DB server's IP + 'PORT': '5432', # Default PostgreSQL port + } } -} # Password validation # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators @@ -140,27 +148,30 @@ STATIC_URL = 'static/' # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +AUTH_USER_MODEL = 'api.User' -REST_FRAMEWORK = { - # ВАШИ НАСТРОЙКИ +REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 100, - 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', + + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', + 'DEFAULT_AUTHENTICATION_CLASSES': [ - 'rest_framework.authentication.TokenAuthentication', + 'rest_framework.authentication.TokenAuthentication', + 'rest_framework.authentication.SessionAuthentication', ], + 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated', - #'rest_framework.permissions.AllowAny', + # 'rest_framework.permissions.AllowAny', ], + 'DEFAULT_RENDERER_CLASSES': [ 'rest_framework.renderers.JSONRenderer', ], - 'DEFAULT_AUTHENTICATION_CLASSES': [ - 'rest_framework.authentication.SessionAuthentication', - ], } + CSRF_COOKIE_SAMESITE = 'Lax' SESSION_COOKIE_SAMESITE = 'Lax' CSRF_COOKIE_HTTPONLY = True