diff --git a/logs_collector/collector/migrations/0008_alter_ticket_attempts_alter_ticket_token.py b/logs_collector/collector/migrations/0008_alter_ticket_attempts_alter_ticket_token.py new file mode 100644 index 0000000..7e705c6 --- /dev/null +++ b/logs_collector/collector/migrations/0008_alter_ticket_attempts_alter_ticket_token.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2 on 2023-08-10 03:24 + +import django.core.validators +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('collector', '0007_rename_upload_ticket_attempts'), + ] + + operations = [ + migrations.AlterField( + model_name='ticket', + name='attempts', + field=models.IntegerField(default=5, validators=[django.core.validators.MaxValueValidator(10), django.core.validators.MinValueValidator(0)]), + ), + migrations.AlterField( + model_name='ticket', + name='token', + field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True), + ), + ] diff --git a/logs_collector/collector/models.py b/logs_collector/collector/models.py index cd8808f..7c4f712 100644 --- a/logs_collector/collector/models.py +++ b/logs_collector/collector/models.py @@ -41,7 +41,7 @@ class Archive(models.Model): ) def save(self, *args, **kwargs): - # calculate sha 1 hash sum and write md5 field to db + # calculate md5 hash sum and write md5 field to db with self.file.open('rb') as f: md5 = hashlib.md5() for byte_block in iter(lambda: f.read(4096), b""): @@ -72,7 +72,7 @@ class Ticket(models.Model): number = models.IntegerField(unique=True, db_index=True) resolved = models.BooleanField(default=False) note = models.TextField(blank=True) - token = models.UUIDField(default=uuid.uuid4, editable=False) + token = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) attempts = models.IntegerField(default=5, validators=[ MaxValueValidator(10), MinValueValidator(0) diff --git a/logs_collector/collector/permissions.py b/logs_collector/collector/permissions.py new file mode 100644 index 0000000..7248552 --- /dev/null +++ b/logs_collector/collector/permissions.py @@ -0,0 +1,13 @@ +from rest_framework import permissions + + +class IsGuestUpload(permissions.BasePermission): + """ + Special permission class for the ability to upload attachments + to an unauthorized user using a ticket token + """ + def has_permission(self, request, view): + if request.method in ('HEAD', 'OPTIONS', 'POST',): + return True + + return request.user.is_authenticated diff --git a/logs_collector/collector/serializers.py b/logs_collector/collector/serializers.py index 95a03a9..452715e 100644 --- a/logs_collector/collector/serializers.py +++ b/logs_collector/collector/serializers.py @@ -1,11 +1,59 @@ from rest_framework import serializers -from .models import Archive +from .models import Archive, Platform, Ticket + + +class TimestampField(serializers.Field): + def to_representation(self, value): + return value.timestamp() + + +class JsTimestampField(serializers.Field): + def to_representation(self, value): + return round(value.timestamp()*1000) class PublicArchiveUploadSerializer(serializers.ModelSerializer): - ticket = serializers.ReadOnlyField(source='ticket.token') - class Meta: model = Archive fields = ['file', 'ticket'] + + +class ArchiveSerializer(serializers.ModelSerializer): + time_create = JsTimestampField(read_only=True) + + class Meta: + model = Archive + fields = ['id', 'file', 'ticket', 'time_create'] + + def to_representation(self, instance): + print(int(round(instance.time_create.timestamp()))) + return super().to_representation(instance) + + +class PlatformSerializer(serializers.ModelSerializer): + + class Meta: + model = Platform + fields = ['id', 'name', 'pretty_name'] + + +class TicketSerializer(serializers.ModelSerializer): + time_create = JsTimestampField(read_only=True) + time_update = JsTimestampField(read_only=True) + token = serializers.UUIDField(read_only=True) + user = serializers.ReadOnlyField(source='user.username') + + class Meta: + model = Ticket + fields = [ + 'id', + 'number', + 'resolved', + 'token', + 'attempts', + 'platform', + 'time_create', + 'time_update', + 'user' + ] diff --git a/logs_collector/collector/urls.py b/logs_collector/collector/urls.py index 1ae3b39..cdf05b3 100644 --- a/logs_collector/collector/urls.py +++ b/logs_collector/collector/urls.py @@ -7,7 +7,9 @@ from . import views app_name = 'collector' router = routers.DefaultRouter() -router.register(r'archives', views.PublicArchiveUploadViewSet) +router.register(r'archives', views.ArchiveViewSet) +router.register(r'platforms', views.PlatformViewSet) +router.register(r'tickets', views.TicketViewSet) urlpatterns = [ diff --git a/logs_collector/collector/utils.py b/logs_collector/collector/utils.py index d8650da..0d752dc 100644 --- a/logs_collector/collector/utils.py +++ b/logs_collector/collector/utils.py @@ -3,8 +3,8 @@ import os def logs_dir_path(instance, filename): # file will be uploaded to - # MEDIA_ROOT_FOR_SENSITIVE_FILES// - return f'{instance.ticket}/{filename}' + # MEDIA_ROOT_FOR_SENSITIVE_FILES// + return f'{instance.ticket.number}/{filename}' def get_file_size(file_path, unit='bytes'): diff --git a/logs_collector/collector/views.py b/logs_collector/collector/views.py index 2d1843a..3c66ef0 100644 --- a/logs_collector/collector/views.py +++ b/logs_collector/collector/views.py @@ -1,5 +1,5 @@ import json -from django.core.exceptions import ValidationError +from django.core.exceptions import ValidationError, ObjectDoesNotExist from django.contrib.auth.mixins import LoginRequiredMixin from django.http import FileResponse, JsonResponse from django.views import generic @@ -8,17 +8,25 @@ from django.urls import reverse_lazy from django.db.models import Q from rest_framework import status -from rest_framework.response import Response +# from rest_framework.decorators import action from rest_framework.parsers import FormParser, MultiPartParser +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response -from rest_framework import mixins -from rest_framework.viewsets import GenericViewSet +# from rest_framework import mixins +from rest_framework import viewsets -from .models import Archive, Ticket +from .models import Archive, Ticket, Platform from .forms import TicketForm from .utils import PageTitleViewMixin, is_ajax +from .permissions import IsGuestUpload -from .serializers import PublicArchiveUploadSerializer +from .serializers import ( + PublicArchiveUploadSerializer, + ArchiveSerializer, + PlatformSerializer, + TicketSerializer +) class ArchiveHandlerView(LoginRequiredMixin, SingleObjectMixin, generic.View): @@ -174,15 +182,16 @@ class DeleteTicketHandler(SingleObjectMixin, generic.View): ) -class PublicArchiveUploadViewSet(mixins.CreateModelMixin, GenericViewSet): +class ArchiveViewSet(viewsets.ModelViewSet): queryset = Archive.objects.order_by('-time_create') - serializer_class = PublicArchiveUploadSerializer + serializer_class = ArchiveSerializer parser_classes = (MultiPartParser, FormParser) + permission_classes = (IsGuestUpload, ) def create(self, request, *args, **kwargs): # ! upload-token protection: upload_token = request.headers.get('upload-token', '') - if upload_token: + if not request.user.is_authenticated and upload_token: try: bound_ticket = Ticket.objects.get(token=upload_token) if bound_ticket.resolved: @@ -198,13 +207,15 @@ class PublicArchiveUploadViewSet(mixins.CreateModelMixin, GenericViewSet): bound_ticket.attempts -= 1 bound_ticket.save() # ? mixin bound ticket to request.data from user - request.data['ticket'] = bound_ticket - except ValidationError: + request.data['ticket'] = bound_ticket.number + # ? change serializer for guest user + self.serializer_class = PublicArchiveUploadSerializer + except (ValidationError, ObjectDoesNotExist,): return Response( {'error': f'token {upload_token} is not valid'}, status=status.HTTP_403_FORBIDDEN ) - else: + elif not request.user.is_authenticated: return Response( {'error': 'Header Upload-Token is required'}, status=status.HTTP_401_UNAUTHORIZED @@ -220,5 +231,16 @@ class PublicArchiveUploadViewSet(mixins.CreateModelMixin, GenericViewSet): headers=headers ) - def perform_create(self, serializer): - serializer.save(ticket=self.request.data['ticket']) + +class PlatformViewSet(viewsets.ModelViewSet): + queryset = Platform.objects.all() + lookup_field = 'name' + serializer_class = PlatformSerializer + permission_classes = (IsAuthenticated, ) + + +class TicketViewSet(viewsets.ModelViewSet): + queryset = Ticket.objects.order_by('-time_create') + lookup_field = 'number' + serializer_class = TicketSerializer + permission_classes = (IsAuthenticated, ) diff --git a/logs_collector/logs_collector/settings.py b/logs_collector/logs_collector/settings.py index dee3a4a..52e1cf2 100644 --- a/logs_collector/logs_collector/settings.py +++ b/logs_collector/logs_collector/settings.py @@ -135,3 +135,18 @@ MEDIA_URL_FOR_SENSITIVE_FILES = '/archives/' CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" CRISPY_TEMPLATE_PACK = "bootstrap5" + +# https://www.django-rest-framework.org/api-guide/settings/ +REST_FRAMEWORK = { + 'DEFAULT_RENDERER_CLASSES': [ + 'rest_framework.renderers.JSONRenderer', + 'rest_framework.renderers.BrowsableAPIRenderer', + ], + 'DEFAULT_PARSER_CLASSES': [ + 'rest_framework.parsers.JSONParser', + 'rest_framework.renderers.BrowsableAPIRenderer', + 'rest_framework.parsers.MultiPartParser' + ], + # 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', # noqa:E501 + # 'PAGE_SIZE': 3, +}