diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4bb0ce1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,58 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +*.pyd +*.so +*.egg-info/ +*.egg +*.log + +# Environments +.env +.env.* +venv/ +venv*/ +ENV/ +.envrc + +# Django +*.sqlite3 +db.sqlite3 +media/ +staticfiles/ +*.pot +*.pyc + +# Migrations (если хочешь игнорировать сгенерированные миграции) +# migrations/ +# */migrations/ + +# IDEs +.vscode/ +.idea/ +*.swp + +# OS +.DS_Store +Thumbs.db + +# Tests +.coverage +htmlcov/ +*.cover +*.py,cover +.cache/ +.tox/ + +# Git +*.orig + +# Docker +*.pid +*.pid.lock +docker-compose.override.yml + +# Build +dist/ +build/ diff --git a/api/__pycache__/admin.cpython-313.pyc b/api/__pycache__/admin.cpython-313.pyc index a19afdd..57f224a 100644 Binary files a/api/__pycache__/admin.cpython-313.pyc and b/api/__pycache__/admin.cpython-313.pyc differ diff --git a/api/__pycache__/models.cpython-313.pyc b/api/__pycache__/models.cpython-313.pyc index 23cd358..e6a5112 100644 Binary files a/api/__pycache__/models.cpython-313.pyc and b/api/__pycache__/models.cpython-313.pyc differ diff --git a/api/__pycache__/serializers.cpython-313.pyc b/api/__pycache__/serializers.cpython-313.pyc index 5099a66..f2d087b 100644 Binary files a/api/__pycache__/serializers.cpython-313.pyc and b/api/__pycache__/serializers.cpython-313.pyc differ diff --git a/api/__pycache__/urls.cpython-313.pyc b/api/__pycache__/urls.cpython-313.pyc index 7ace33c..597ee42 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 a7c45b3..0c47532 100644 Binary files a/api/__pycache__/views.cpython-313.pyc and b/api/__pycache__/views.cpython-313.pyc differ diff --git a/api/admin.py b/api/admin.py index c47772a..77550fc 100644 --- a/api/admin.py +++ b/api/admin.py @@ -1,7 +1,9 @@ from django.contrib import admin -from .models import User, Prediction, UserPrediction +from .models import User, Prediction, UserPrediction, Satellite, TelemetryPacket admin.site.register(User) admin.site.register(Prediction) -admin.site.register(UserPrediction) \ No newline at end of file +admin.site.register(UserPrediction) +admin.site.register(Satellite) +admin.site.register(TelemetryPacket) \ No newline at end of file diff --git a/api/migrations/0004_satellite_telemetrypacket.py b/api/migrations/0004_satellite_telemetrypacket.py new file mode 100644 index 0000000..d7f36cd --- /dev/null +++ b/api/migrations/0004_satellite_telemetrypacket.py @@ -0,0 +1,35 @@ +# Generated by Django 5.1.7 on 2025-04-05 13:30 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0003_alter_userprediction_unique_together_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Satellite', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name='TelemetryPacket', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('timestamp', models.BigIntegerField()), + ('lat', models.FloatField()), + ('lon', models.FloatField()), + ('alt', models.FloatField()), + ('payload', models.JSONField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('satellite', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='telemetry', to='api.satellite')), + ], + ), + ] diff --git a/api/models.py b/api/models.py index 61621af..3bb0c21 100644 --- a/api/models.py +++ b/api/models.py @@ -18,3 +18,16 @@ class UserPrediction(models.Model): prediction = models.ForeignKey("Prediction", on_delete=models.CASCADE) created_at = models.DateTimeField() +class Satellite(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + name = models.CharField(max_length=100) + +class TelemetryPacket(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + satellite = models.ForeignKey(Satellite, on_delete=models.CASCADE, related_name="telemetry") + timestamp = models.BigIntegerField() # unix time + lat = models.FloatField() + lon = models.FloatField() + alt = models.FloatField() + payload = models.JSONField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) \ No newline at end of file diff --git a/api/permissions.py b/api/permissions.py new file mode 100644 index 0000000..20785ca --- /dev/null +++ b/api/permissions.py @@ -0,0 +1,8 @@ +from rest_framework.permissions import BasePermission, SAFE_METHODS + +class ReadOnlyOrAuthenticated(BasePermission): + def has_permission(self, request, view): + return ( + request.method in SAFE_METHODS or + request.user and request.user.is_authenticated + ) diff --git a/api/serializers.py b/api/serializers.py index 4823d4f..31e9554 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -84,3 +84,13 @@ class PredictionDetailSerializer(serializers.ModelSerializer): class Meta: model = Prediction fields = ["id", "created_at", "updated_at", "result"] + + +from rest_framework import serializers +from .models import TelemetryPacket + +class TelemetryPacketSerializer(serializers.ModelSerializer): + class Meta: + model = TelemetryPacket + fields = ['id', 'timestamp', 'lat', 'lon', 'alt', 'payload'] + diff --git a/api/urls.py b/api/urls.py index eaff5cb..74dfdab 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,16 +1,26 @@ from django.urls import path -from .views import (PredictionCreateView, PredictionListView, PredictionDeleteView, +from .views import (PredictionCreateView, PredictionListView, PredictionHistoryListView, PredictionHistoryDetailView, - PredictionHistoryDeleteView,) + PredictionHistoryDeleteView, + SessionView, + WhoAmIView, + get_csrf, + login_view, + logout_view) 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/', PredictionDeleteView.as_view(), name='delete_prediction'), - path('token/', obtain_auth_token), - path("history/", PredictionHistoryListView.as_view()), - path("history//", PredictionHistoryDetailView.as_view()), - path("history//delete/", PredictionHistoryDeleteView.as_view()), + 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'), + path("history//delete/", PredictionHistoryDeleteView.as_view(), name='delete_history'), + path("/telemetry/", TelemetryListCreateView.as_view(), name="create_telemetry"), + path('csrf/', get_csrf, name='api-csrf'), + path('login/', login_view, name='api-login'), + path('logout/', logout_view, name='api-logout'), + path('session/', SessionView.as_view(), name='api-session'), # new + path('whoami/', WhoAmIView.as_view(), name='api-whoami'), # new ] diff --git a/api/views.py b/api/views.py index a4420fc..a0e4d91 100644 --- a/api/views.py +++ b/api/views.py @@ -11,6 +11,15 @@ from django.utils.decorators import method_decorator from rest_framework.permissions import AllowAny from .services.tawhiri import TawhiriClient from django.contrib.auth import get_user_model +from .models import Satellite, TelemetryPacket +from .serializers import TelemetryPacketSerializer +from .permissions import ReadOnlyOrAuthenticated +import time +from django.http import JsonResponse +from rest_framework.authentication import SessionAuthentication, BasicAuthentication +from django.contrib.auth import authenticate, login, logout +import json +from django.middleware.csrf import get_token User = get_user_model() @@ -24,6 +33,8 @@ def get_prediction_from_tawhiri(params): else: raise Exception(f"Tawhiri error: {response.status_code} {response.text}") + + class PredictionCreateView(APIView): permission_classes = [IsAuthenticated] @@ -39,19 +50,20 @@ 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") + 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) prediction = Prediction.objects.create(result=prediction_result) UserPrediction.objects.create(user=request.user, prediction=prediction, created_at=timezone.now()) - return Response({ "id": prediction.id, "created_at": prediction.created_at, "result": prediction_result }, status=status.HTTP_201_CREATED) + class PredictionListView(APIView): def get(self, request): user_id = request.query_params.get('user_id') @@ -66,17 +78,6 @@ class PredictionListView(APIView): ) return Response(PredictionSerializer(predictions, many=True).data) -class PredictionDeleteView(APIView): - def delete(self, request, pk): - try: - prediction = Prediction.objects.get(pk=pk) - prediction.deleted_at = timezone.now() - prediction.save() - return Response({"deleted": True}) - except Prediction.DoesNotExist: - return Response({"error": "Not found"}, status=404) - - class PredictionHistoryListView(generics.ListAPIView): permission_classes = [permissions.IsAuthenticated] @@ -89,6 +90,7 @@ class PredictionHistoryListView(generics.ListAPIView): ) + class PredictionHistoryDetailView(generics.RetrieveAPIView): permission_classes = [permissions.IsAuthenticated] serializer_class = PredictionDetailSerializer @@ -100,6 +102,7 @@ class PredictionHistoryDetailView(generics.RetrieveAPIView): ) + class PredictionHistoryDeleteView(generics.DestroyAPIView): permission_classes = [permissions.IsAuthenticated] @@ -114,5 +117,103 @@ class PredictionHistoryDeleteView(generics.DestroyAPIView): instance.save() + +class TelemetryListCreateView(generics.ListCreateAPIView): + serializer_class = TelemetryPacketSerializer + permission_classes = [permissions.AllowAny] + + def get_queryset(self): + qs = TelemetryPacket.objects.filter(satellite_id=self.kwargs["pk"]) + + from_ts = self.request.query_params.get("from") + till_ts = self.request.query_params.get("till") + + if from_ts: + qs = qs.filter(timestamp__gte=int(from_ts)) + if till_ts: + qs = qs.filter(timestamp__lte=int(till_ts)) + + return qs.order_by("-timestamp") + + + + def post(self, request, pk): + serializer = TelemetryPacketSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + validated_data = serializer.validated_data + + TelemetryPacket.objects.create(timestamp = time.time(), + satellite=Satellite.objects.get(id=pk), + lat=validated_data["lat"], + lon=validated_data["lon"], + alt=validated_data["alt"], + payload=validated_data['payload'], + ) + return Response(serializer.errors, status=status.HTTP_201_CREATED) + + +class SessionView(APIView): + authentication_classes = [SessionAuthentication, BasicAuthentication] + permission_classes = [IsAuthenticated] + + @staticmethod + def get(request, format=None): + return JsonResponse({'isAuthenticated': True}) + + +class WhoAmIView(APIView): + authentication_classes = [SessionAuthentication, BasicAuthentication] + permission_classes = [IsAuthenticated] + + @staticmethod + def get(request, format=None): + return JsonResponse({'username': request.user.username}) + + +def get_csrf(request): + response = JsonResponse({'detail': 'CSRF cookie set'}) + response['X-CSRFToken'] = get_token(request) + return response + + +def login_view(request): + data = json.loads(request.body) + username = data.get('username') + password = data.get('password') + + if username is None or password is None: + 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) + + login(request, user) + return JsonResponse({'detail': 'Successfully logged in.'}) + + +def logout_view(request): + if not request.user.is_authenticated: + return JsonResponse({'detail': 'You\'re not logged in.'}, status=400) + + logout(request) + return JsonResponse({'detail': 'Successfully logged out.'}) + + + #class PredictionCreateView(APIView): - #permission_classes = [IsAuthenticated] \ No newline at end of file + #permission_classes = [IsAuthenticated] + +# class TelemetryPacket(models.Model): +# id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) +# satellite = models.ForeignKey(Satellite, on_delete=models.CASCADE, related_name="telemetry") +# timestamp = models.BigIntegerField() # unix time +# lat = models.FloatField() +# lon = models.FloatField() +# alt = models.FloatField() +# payload = models.JSONField(null=True, blank=True) +# created_at = models.DateTimeField(auto_now_add=True) +# fields = ['id', 'timestamp', 'lat', 'lon', 'alt', 'payload'] \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 index 41a74d0..cd0c5f4 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/testapi/__pycache__/settings.cpython-313.pyc b/testapi/__pycache__/settings.cpython-313.pyc index b7622bf..0b832d5 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 9409c61..ba898ed 100644 --- a/testapi/settings.py +++ b/testapi/settings.py @@ -41,7 +41,7 @@ INSTALLED_APPS = [ 'rest_framework.authtoken', 'drf_spectacular', 'corsheaders', - 'api' + 'api.apps.ApiConfig', ] MIDDLEWARE = [ @@ -55,6 +55,13 @@ MIDDLEWARE = [ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] +CORS_ALLOWED_ORIGINS = [ + 'http://localhost:5173', + 'http://127.0.0.1:5173', +] + +CORS_EXPOSE_HEADERS = ['Content-Type', 'X-CSRFToken'] +CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_ALL_ORIGINS = True ROOT_URLCONF = 'testapi.urls' @@ -140,6 +147,8 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' REST_FRAMEWORK = { # ВАШИ НАСТРОЙКИ + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 100, 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.TokenAuthentication', @@ -147,5 +156,17 @@ REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated', #'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 +SESSION_COOKIE_HTTPONLY = True +CSRF_TRUSTED_ORIGINS = ['http://localhost:5173'] \ No newline at end of file